/** * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho.widget; import android.annotation.TargetApi; import android.os.Build; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.view.View; import com.facebook.litho.ComponentTree; import com.facebook.litho.LithoView; /** * Controller that handles sticky header logic. Depending on where the sticky item is located in the * list, we might either use first child as sticky header or use {@link RecyclerViewWrapper}'s * sticky header. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) class StickyHeaderController extends RecyclerView.OnScrollListener { static final String WRAPPER_ARGUMENT_NULL = "Cannot initialize with null RecyclerViewWrapper."; static final String WRAPPER_ALREADY_INITIALIZED = "RecyclerViewWrapper has already been initialized but never reset."; static final String WRAPPER_NOT_INITIALIZED = "RecyclerViewWrapper has not been set yet."; static final String LAYOUTMANAGER_NOT_INITIALIZED = "LayoutManager of RecyclerView is not initialized yet."; private final HasStickyHeader mHasStickyHeader; private RecyclerViewWrapper mRecyclerViewWrapper; private RecyclerView.LayoutManager mLayoutManager; private View lastTranslatedView; private int previousStickyHeaderPosition = RecyclerView.NO_POSITION; StickyHeaderController(HasStickyHeader hasStickyHeader) { mHasStickyHeader = hasStickyHeader; } void init(RecyclerViewWrapper recyclerViewWrapper) { if (recyclerViewWrapper == null) { throw new RuntimeException(WRAPPER_ARGUMENT_NULL); } if (mRecyclerViewWrapper != null) { throw new RuntimeException(WRAPPER_ALREADY_INITIALIZED); } mRecyclerViewWrapper = recyclerViewWrapper; mRecyclerViewWrapper.hideStickyHeader(); mLayoutManager = recyclerViewWrapper.getRecyclerView().getLayoutManager(); if (mLayoutManager == null) { throw new RuntimeException(LAYOUTMANAGER_NOT_INITIALIZED); } mRecyclerViewWrapper.getRecyclerView().addOnScrollListener(this); } void reset() { if (mRecyclerViewWrapper == null) { throw new IllegalStateException(WRAPPER_NOT_INITIALIZED); } mRecyclerViewWrapper.getRecyclerView().removeOnScrollListener(this); mLayoutManager = null; mRecyclerViewWrapper = null; } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { final int firstVisiblePosition = mHasStickyHeader.findFirstVisibleItemPosition(); if (firstVisiblePosition == RecyclerView.NO_POSITION) { return; } final int stickyHeaderPosition = findStickyHeaderPosition(firstVisiblePosition); final ComponentTree firstVisibleItemComponentTree = mHasStickyHeader.getComponentAt(firstVisiblePosition); if (lastTranslatedView != null && firstVisibleItemComponentTree != null && lastTranslatedView != firstVisibleItemComponentTree.getLithoView()) { // Reset previously modified view lastTranslatedView.setTranslationY(0); lastTranslatedView = null; } if (stickyHeaderPosition == RecyclerView.NO_POSITION || firstVisibleItemComponentTree == null) { // no sticky header above first visible position, reset the state mRecyclerViewWrapper.hideStickyHeader(); previousStickyHeaderPosition = RecyclerView.NO_POSITION; return; } if (firstVisiblePosition == stickyHeaderPosition) { final LithoView firstVisibleView = firstVisibleItemComponentTree.getLithoView(); // Translate first child, no need for sticky header firstVisibleView.setTranslationY(-firstVisibleView.getTop()); lastTranslatedView = firstVisibleView; mRecyclerViewWrapper.hideStickyHeader(); previousStickyHeaderPosition = RecyclerView.NO_POSITION; } else { if (mRecyclerViewWrapper.isStickyHeaderHidden() || stickyHeaderPosition != previousStickyHeaderPosition) { initStickyHeader(stickyHeaderPosition); mRecyclerViewWrapper.showStickyHeader(); } // Translate sticky header final int lastVisiblePosition = mHasStickyHeader.findLastVisibleItemPosition(); int translationY = 0; for (int i = firstVisiblePosition; i <= lastVisiblePosition; i++) { if (mHasStickyHeader.isSticky(i)) { final View nextStickyHeader = mLayoutManager.findViewByPosition(i); final int offsetBetweenStickyHeaders = nextStickyHeader.getTop() - mRecyclerViewWrapper.getStickyHeader().getBottom() + mRecyclerViewWrapper.getPaddingTop(); translationY = Math.min(offsetBetweenStickyHeaders, 0); break; } } mRecyclerViewWrapper.setStickyHeaderVerticalOffset(translationY); previousStickyHeaderPosition = stickyHeaderPosition; } } private void initStickyHeader(int stickyHeaderPosition) { mRecyclerViewWrapper.setStickyComponent(mHasStickyHeader.getComponentAt(stickyHeaderPosition)); } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) int findStickyHeaderPosition(int currentFirstVisiblePosition) { for (int i = currentFirstVisiblePosition; i >= 0; i--) { if (mHasStickyHeader.isSticky(i)) { return i; } } return RecyclerView.NO_POSITION; } }