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 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;
public HeaderPositionCalculator(StickyRecyclerHeadersAdapter adapter, HeaderProvider headerProvider,
OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator) {
mAdapter = adapter;
mHeaderProvider = headerProvider;
mOrientationProvider = orientationProvider;
mDimensionCalculator = dimensionCalculator;
}
/**
* 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
* @return true if this item has a different header than the previous item in the list
* @see {@link StickyRecyclerHeadersAdapter#getHeaderId(int)}
*/
public boolean hasNewHeader(int position) {
if (getFirstHeaderPosition() == position) {
return true;
}
if (mAdapter.getHeaderId(position) < 0 || indexOutOfBounds(position)) {
return false;
}
return mAdapter.getHeaderId(position) != mAdapter.getHeaderId(position - 1);
}
private boolean indexOutOfBounds(int position) {
return position < 0 || position >= mAdapter.getItemCount();
}
private int getFirstHeaderPosition() {
for (int i = 0; i < mAdapter.getItemCount(); i++) {
if (mAdapter.getHeaderId(i) >= 0) {
return i;
}
}
return -1;
}
public Rect getHeaderBounds(RecyclerView recyclerView, View header, View firstView, boolean firstHeader) {
int orientation = mOrientationProvider.getOrientation(recyclerView);
Rect bounds = getDefaultHeaderOffset(header, firstView, orientation);
if (firstHeader && isStickyHeaderBeingPushedOffscreen(recyclerView, header)) {
View viewAfterNextHeader = getFirstViewUnobscuredByHeader(recyclerView, header);
int firstViewUnderHeaderPosition = recyclerView.getChildPosition(viewAfterNextHeader);
View secondHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
translateHeaderWithNextHeader(recyclerView, mOrientationProvider.getOrientation(recyclerView), bounds,
header, viewAfterNextHeader, secondHeader);
}
return bounds;
}
private Rect getDefaultHeaderOffset(View header, View firstView, int orientation) {
int translationX, translationY;
Rect headerMargins = mDimensionCalculator.getMargins(header);
if (orientation == LinearLayoutManager.VERTICAL) {
translationX = firstView.getLeft() + headerMargins.left;
translationY = firstView.getTop() - header.getHeight() - headerMargins.bottom;
} else {
translationY = firstView.getTop() + headerMargins.top;
translationX = firstView.getLeft() - header.getWidth() - headerMargins.right;
}
return new Rect(translationX, translationY, translationX + header.getWidth(),
translationY + header.getHeight());
}
private boolean isStickyHeaderBeingPushedOffscreen(RecyclerView recyclerView, View stickyHeader) {
View viewAfterHeader = getFirstViewUnobscuredByHeader(recyclerView, stickyHeader);
int firstViewUnderHeaderPosition = recyclerView.getChildPosition(viewAfterHeader);
if (firstViewUnderHeaderPosition > 0 && hasNewHeader(firstViewUnderHeaderPosition)) {
View nextHeader = mHeaderProvider.getHeader(recyclerView, firstViewUnderHeaderPosition);
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
Rect headerMargins = mDimensionCalculator.getMargins(stickyHeader);
if (mOrientationProvider.getOrientation(recyclerView) == LinearLayoutManager.VERTICAL) {
int topOfNextHeader = viewAfterHeader.getTop() - nextHeaderMargins.bottom - nextHeader.getHeight() - nextHeaderMargins.top;
int bottomOfThisHeader = recyclerView.getPaddingTop() + stickyHeader.getBottom() + headerMargins.top + headerMargins.bottom;
if (topOfNextHeader < bottomOfThisHeader) {
return true;
}
} else {
int leftOfNextHeader = viewAfterHeader.getLeft() - nextHeaderMargins.right - nextHeader.getWidth() - nextHeaderMargins.left;
int rightOfThisHeader = recyclerView.getPaddingLeft() + stickyHeader.getRight() + headerMargins.left + headerMargins.right;
if (leftOfNextHeader < rightOfThisHeader) {
return true;
}
}
}
return false;
}
private void translateHeaderWithNextHeader(RecyclerView recyclerView, int orientation, Rect translation,
View currentHeader, View viewAfterNextHeader, View nextHeader) {
Rect nextHeaderMargins = mDimensionCalculator.getMargins(nextHeader);
Rect stickyHeaderMargins = mDimensionCalculator.getMargins(currentHeader);
if (orientation == LinearLayoutManager.VERTICAL) {
int topOfStickyHeader = getListTop(recyclerView) + stickyHeaderMargins.top + stickyHeaderMargins.bottom;
int shiftFromNextHeader = viewAfterNextHeader.getTop() - nextHeader.getHeight() - nextHeaderMargins.bottom - nextHeaderMargins.top - currentHeader.getHeight() - topOfStickyHeader;
if (shiftFromNextHeader < topOfStickyHeader) {
translation.top += shiftFromNextHeader;
}
} else {
int leftOfStickyHeader = getListLeft(recyclerView) + stickyHeaderMargins.left + stickyHeaderMargins.right;
int shiftFromNextHeader = viewAfterNextHeader.getLeft() - nextHeader.getWidth() - nextHeaderMargins.right - nextHeaderMargins.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) {
for (int i = 0; i < parent.getChildCount(); i++) {
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();
Rect headerMargins = mDimensionCalculator.getMargins(header);
if (mHeaderProvider.getHeader(parent, parent.getChildPosition(item)) != 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 = header.getBottom() + headerMargins.bottom + headerMargins.top;
if (itemTop > headerBottom) {
return false;
}
} else {
int itemLeft = item.getLeft() - layoutParams.leftMargin;
int headerRight = header.getRight() + headerMargins.right + headerMargins.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;
}
}
}