package com.timehop.stickyheadersrecyclerview; import android.graphics.Canvas; import android.graphics.Rect; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.SparseArray; import android.view.View; import com.timehop.stickyheadersrecyclerview.caching.HeaderProvider; import com.timehop.stickyheadersrecyclerview.caching.HeaderViewCache; import com.timehop.stickyheadersrecyclerview.calculation.DimensionCalculator; import com.timehop.stickyheadersrecyclerview.rendering.HeaderRenderer; import com.timehop.stickyheadersrecyclerview.util.LinearLayoutOrientationProvider; import com.timehop.stickyheadersrecyclerview.util.OrientationProvider; public class StickyRecyclerHeadersDecoration extends RecyclerView.ItemDecoration { private final StickyRecyclerHeadersAdapter mAdapter; private final SparseArray<Rect> mHeaderRects = new SparseArray<>(); private final HeaderProvider mHeaderProvider; private final OrientationProvider mOrientationProvider; private final HeaderPositionCalculator mHeaderPositionCalculator; private final HeaderRenderer mRenderer; private final DimensionCalculator mDimensionCalculator; // TODO: Consider passing in orientation to simplify orientation accounting within calculation public StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter) { this(adapter, new LinearLayoutOrientationProvider(), new DimensionCalculator()); } private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator) { this(adapter, orientationProvider, dimensionCalculator, new HeaderRenderer(orientationProvider), new HeaderViewCache(adapter, orientationProvider)); } private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator, HeaderRenderer headerRenderer, HeaderProvider headerProvider) { this(adapter, headerRenderer, orientationProvider, dimensionCalculator, headerProvider, new HeaderPositionCalculator(adapter, headerProvider, orientationProvider, dimensionCalculator)); } private StickyRecyclerHeadersDecoration(StickyRecyclerHeadersAdapter adapter, HeaderRenderer headerRenderer, OrientationProvider orientationProvider, DimensionCalculator dimensionCalculator, HeaderProvider headerProvider, HeaderPositionCalculator headerPositionCalculator) { mAdapter = adapter; mHeaderProvider = headerProvider; mOrientationProvider = orientationProvider; mRenderer = headerRenderer; mDimensionCalculator = dimensionCalculator; mHeaderPositionCalculator = headerPositionCalculator; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int itemPosition = parent.getChildPosition(view); if (mHeaderPositionCalculator.hasNewHeader(itemPosition)) { View header = getHeaderView(parent, itemPosition); setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent)); } } /** * Sets the offsets for the first item in a section to make room for the header view * * @param itemOffsets rectangle to define offsets for the item * @param header view used to calculate offset for the item * @param orientation used to calculate offset for the item */ private void setItemOffsetsForHeader(Rect itemOffsets, View header, int orientation) { Rect headerMargins = mDimensionCalculator.getMargins(header); if (orientation == LinearLayoutManager.VERTICAL) { itemOffsets.top = header.getHeight() + headerMargins.top + headerMargins.bottom; } else { itemOffsets.left = header.getWidth() + headerMargins.left + headerMargins.right; } } @Override public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(canvas, parent, state); mHeaderRects.clear(); if (parent.getChildCount() <= 0 || mAdapter.getItemCount() <= 0) { return; } for (int i = 0; i < parent.getChildCount(); i++) { View itemView = parent.getChildAt(i); int position = parent.getChildPosition(itemView); if (hasStickyHeader(i, position) || mHeaderPositionCalculator.hasNewHeader(position)) { View header = mHeaderProvider.getHeader(parent, position); Rect headerOffset = mHeaderPositionCalculator.getHeaderBounds(parent, header, itemView, hasStickyHeader(i, position)); mRenderer.drawHeader(parent, canvas, header, headerOffset); mHeaderRects.put(position, headerOffset); } } } private boolean hasStickyHeader(int listChildPosition, int indexInList) { if (listChildPosition > 0 || mAdapter.getHeaderId(indexInList) < 0) { return false; } return true; } /** * Gets the position of the header under the specified (x, y) coordinates. * * @param x x-coordinate * @param y y-coordinate * @return position of header, or -1 if not found */ public int findHeaderPositionUnder(int x, int y) { for (int i = 0; i < mHeaderRects.size(); i++) { Rect rect = mHeaderRects.get(mHeaderRects.keyAt(i)); if (rect.contains(x, y)) { return mHeaderRects.keyAt(i); } } return -1; } /** * Gets the header view for the associated position. If it doesn't exist yet, it will be * created, measured, and laid out. * * @param parent * @param position * @return Header view */ public View getHeaderView(RecyclerView parent, int position) { return mHeaderProvider.getHeader(parent, position); } /** * Invalidates cached headers. This does not invalidate the recyclerview, you should do that manually after * calling this method. */ public void invalidateHeaders() { mHeaderProvider.invalidate(); } }