package com.timehop.stickyheadersrecyclerview; import android.graphics.Rect; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import com.timehop.stickyheadersrecyclerview.caching.HeaderProvider; import com.timehop.stickyheadersrecyclerview.calculation.DimensionCalculator; import com.timehop.stickyheadersrecyclerview.util.OrientationProvider; /** * Calculates the position and location of header views */ public class HeaderPositionCalculator { private final StickyRecyclerHeadersAdapter mAdapter; private final OrientationProvider mOrientationProvider; private final HeaderProvider mHeaderProvider; private final DimensionCalculator mDimensionCalculator; /** * The following fields are used as buffers for internal calculations. Their sole purpose is to avoid * allocating new Rect every time we need one. */ private final Rect mTempRect1 = new Rect(); private final Rect mTempRect2 = new Rect(); public HeaderPositionCalculator(StickyRecyclerHeadersAdapter adapter, HeaderProvider headerProvider, OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator) { mAdapter = adapter; mHeaderProvider = headerProvider; mOrientationProvider = orientationProvider; mDimensionCalculator = dimensionCalculator; } /** * Determines if a view should have a sticky header. * The view has a sticky header if: * 1. It is the first element in the recycler view * 2. It has a valid ID associated to its position * * @param itemView given by the RecyclerView * @param orientation of the Recyclerview * @param position of the list item in question * @return True if the view should have a sticky header */ public boolean hasStickyHeader(View itemView, int orientation, int position) { int offset, margin; mDimensionCalculator.initMargins(mTempRect1, itemView); if (orientation == LinearLayout.VERTICAL) { offset = itemView.getTop(); margin = mTempRect1.top; } else { offset = itemView.getLeft(); margin = mTempRect1.left; } return offset <= margin && mAdapter.getHeaderId(position) >= 0; } /** * Determines if an item in the list should have a header that is different than the item in the * list that immediately precedes it. Items with no headers will always return false. * * @param position of the list item in questions * @param isReverseLayout TRUE if layout manager has flag isReverseLayout * @return true if this item has a different header than the previous item in the list */ public boolean hasNewHeader(int position, boolean isReverseLayout) { if (indexOutOfBounds(position)) { return false; } long headerId = mAdapter.getHeaderId(position); if (headerId < 0) { return false; } long nextItemHeaderId = -1; int nextItemPosition = position + (isReverseLayout? 1: -1); if (!indexOutOfBounds(nextItemPosition)){ nextItemHeaderId = mAdapter.getHeaderId(nextItemPosition); } int firstItemPosition = isReverseLayout? mAdapter.getItemCount()-1 : 0; return position == firstItemPosition || headerId != nextItemHeaderId; } private boolean indexOutOfBounds(int position) { return position < 0 || position >= mAdapter.getItemCount(); } public void initHeaderBounds(Rect bounds, RecyclerView recyclerView, View header, View firstView, boolean firstHeader) { int orientation = mOrientationProvider.getOrientation(recyclerView); initDefaultHeaderOffset(bounds, recyclerView, header, firstView, orientation); if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) { View viewAfterNextHeader = getFirstViewUnobscuredByHeader(recyclerView, header); int firstViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterNextHeader); View secondHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition); translateHeaderWithNextHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds, header, viewAfterNextHeader, secondHeader); } } private void initDefaultHeaderOffset(Rect headerMargins, RecyclerView recyclerView, View header, View firstView, int orientation) { int translationX, translationY; mDimensionCalculator.initMargins(mTempRect1, header); ViewGroup.LayoutParams layoutParams = firstView.getLayoutParams(); int leftMargin = 0; int topMargin = 0; if (layoutParams instanceof ViewGroup.MarginLayoutParams) { ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; leftMargin = marginLayoutParams.leftMargin; topMargin = marginLayoutParams.topMargin; } if (orientation == LinearLayoutManager.VERTICAL) { translationX = firstView.getLeft() - leftMargin + mTempRect1.left; translationY = Math.max( firstView.getTop() - topMargin - header.getHeight() - mTempRect1.bottom, getListTop(recyclerView) + mTempRect1.top); } else { translationY = firstView.getTop() - topMargin + mTempRect1.top; translationX = Math.max( firstView.getLeft() - leftMargin - header.getWidth() - mTempRect1.right, getListLeft(recyclerView) + mTempRect1.left); } headerMargins.set(translationX, translationY, translationX + header.getWidth(), translationY + header.getHeight()); } private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, View stickyHeader) { View viewAfterHeader = getFirstViewUnobscuredByHeader(recyclerView, stickyHeader); int firstViewUnderHeaderPosition = recyclerView.getChildAdapterPosition(viewAfterHeader); if (firstViewUnderHeaderPosition == RecyclerView.NO_POSITION) { return false; } boolean isReverseLayout = mOrientationProvider.isReverseLayout(recyclerView); if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition, isReverseLayout)) { View nextHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition); mDimensionCalculator.initMargins(mTempRect1, nextHeader); mDimensionCalculator.initMargins(mTempRect2, stickyHeader); if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) { int topOfNextHeader = viewAfterHeader.getTop() - mTempRect1.bottom - nextHeader.getHeight() - mTempRect1.top; int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + mTempRect2.top + mTempRect2.bottom; if (topOfNextHeader < bottomOfThisHeader) { return true; } } else { int leftOfNextHeader = viewAfterHeader.getLeft() - mTempRect1.right - nextHeader.getWidth() - mTempRect1.left; int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + mTempRect2.left + mTempRect2.right; if (leftOfNextHeader < rightOfThisHeader) { return true; } } } return false; } private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orientation, Rect translation, View currentHeader, View viewAfterNextHeader, View nextHeader) { mDimensionCalculator.initMargins(mTempRect1, nextHeader); mDimensionCalculator.initMargins(mTempRect2, currentHeader); if (orientation == LinearLayoutManager.VERTICAL) { int topOfStickyHeader = getListTop(recyclerView) + mTempRect2.top + mTempRect2.bottom; int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - mTempRect1.bottom - mTempRect1.top - currentHeader.getHeight() - topOfStickyHeader; if (shiftFromNextHeader < topOfStickyHeader) { translation.top += shiftFromNextHeader; } } else { int leftOfStickyHeader = getListLeft(recyclerView) + mTempRect2.left + mTempRect2.right; int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - mTempRect1.right - mTempRect1.left - currentHeader.getWidth() - leftOfStickyHeader; if (shiftFromNextHeader < leftOfStickyHeader) { translation.left += shiftFromNextHeader; } } } /** * Returns the first item currently in the RecyclerView that is not obscured by a header. * * @param parent Recyclerview containing all the list items * @return first item that is fully beneath a header */ private View getFirstViewUnobscuredByHeader(RecyclerView parent, View firstHeader) { boolean isReverseLayout = mOrientationProvider.isReverseLayout(parent); int step = isReverseLayout? -1 : 1; int from = isReverseLayout? parent.getChildCount()-1 : 0; for (int i = from; i >= 0 && i <= parent.getChildCount() - 1; i += step) { View child = parent.getChildAt(i); if (!itemIsObscuredByHeader(parent, child, firstHeader, mOrientationProvider.getOrientation(parent))) { return child; } } return null; } /** * Determines if an item is obscured by a header * * * @param parent * @param item to determine if obscured by header * @param header that might be obscuring the item * @param orientation of the {@link RecyclerView} * @return true if the item view is obscured by the header view */ private boolean itemIsObscuredByHeader(RecyclerView parent, View item, View header, int orientation) { RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) item.getLayoutParams(); mDimensionCalculator.initMargins(mTempRect1, header); int adapterPosition = parent.getChildAdapterPosition(item); if (adapterPosition == RecyclerView.NO_POSITION || mHeaderProvider.getHeader(parent, adapterPosition) != header) { // Resolves https://github.com/timehop/sticky-headers-recyclerview/issues/36 // Handles an edge case where a trailing header is smaller than the current sticky header. return false; } if (orientation == LinearLayoutManager.VERTICAL) { int itemTop = item.getTop() - layoutParams.topMargin; int headerBottom = getListTop(parent) + header.getBottom() + mTempRect1.bottom + mTempRect1.top; if (itemTop >= headerBottom) { return false; } } else { int itemLeft = item.getLeft() - layoutParams.leftMargin; int headerRight = getListLeft(parent) + header.getRight() + mTempRect1.right + mTempRect1.left; if (itemLeft >= headerRight) { return false; } } return true; } private int getListTop(RecyclerView view) { if (view.getLayoutManager().getClipToPadding()) { return view.getPaddingTop(); } else { return 0; } } private int getListLeft(RecyclerView view) { if (view.getLayoutManager().getClipToPadding()) { return view.getPaddingLeft(); } else { return 0; } } }