/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v17.leanback.widget; import static android.support.v7.widget.RecyclerView.LayoutManager; import static android.support.v7.widget.RecyclerView.OnScrollListener; import static android.support.v7.widget.RecyclerView.ViewHolder; import android.graphics.Rect; import android.support.v7.widget.RecyclerView; import android.util.Property; import android.view.View; /** * Implementation of {@link Parallax} class for {@link RecyclerView}. This class * allows users to track position of specific views inside {@link RecyclerView} relative to * itself. @see {@link ChildPositionProperty} for details. */ public class RecyclerViewParallax extends Parallax { RecyclerView mRecylerView; boolean mIsVertical; OnScrollListener mOnScrollListener = new OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { updateValues(); } }; View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int l, int t, int r, int b, int oldL, int oldT, int oldR, int oldB) { updateValues(); } }; /** * Subclass of {@link Parallax.IntProperty}. Using this Property, users can track a * RecylerView child's position inside recyclerview. i.e. * * tracking_pos = view.top + fraction * view.height() + offset * * This way we can track top using fraction 0 and bottom using fraction 1. */ public static final class ChildPositionProperty extends Parallax.IntProperty { int mAdapterPosition; int mViewId; int mOffset; float mFraction; ChildPositionProperty(String name, int index) { super(name, index); } /** * Sets adapter position of the recyclerview child to track. * * @param adapterPosition Zero based position in adapter. * @return This ChildPositionProperty object. */ public ChildPositionProperty adapterPosition(int adapterPosition) { mAdapterPosition = adapterPosition; return this; }; /** * Sets view Id of a descendant of recyclerview child to track. * * @param viewId Id of a descendant of recyclerview child. * @return This ChildPositionProperty object. */ public ChildPositionProperty viewId(int viewId) { mViewId = viewId; return this; } /** * Sets offset in pixels added to the view's start position. * * @param offset Offset in pixels added to the view's start position. * @return This ChildPositionProperty object. */ public ChildPositionProperty offset(int offset) { mOffset = offset; return this; } /** * Sets fraction of size to be added to view's start position. e.g. to track the * center position of the view, use fraction 0.5; to track the end position of the view * use fraction 1. * * @param fraction Fraction of size of the view. * @return This ChildPositionProperty object. */ public ChildPositionProperty fraction(float fraction) { mFraction = fraction; return this; } /** * Returns adapter position of the recyclerview child to track. */ public int getAdapterPosition() { return mAdapterPosition; } /** * Returns view Id of a descendant of recyclerview child to track. */ public int getViewId() { return mViewId; } /** * Returns offset in pixels added to the view's start position. */ public int getOffset() { return mOffset; } /** * Returns fraction of size to be added to view's start position. e.g. to track the * center position of the view, use fraction 0.5; to track the end position of the view * use fraction 1. */ public float getFraction() { return mFraction; } void updateValue(RecyclerViewParallax source) { RecyclerView recyclerView = source.mRecylerView; ViewHolder viewHolder = recyclerView == null ? null : recyclerView.findViewHolderForAdapterPosition(mAdapterPosition); if (viewHolder == null) { if (recyclerView == null || recyclerView.getLayoutManager().getChildCount() == 0) { source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER); return; } View firstChild = recyclerView.getLayoutManager().getChildAt(0); ViewHolder vh = recyclerView.findContainingViewHolder(firstChild); int firstPosition = vh.getAdapterPosition(); if (firstPosition < mAdapterPosition) { source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER); } else { source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_BEFORE); } } else { View trackingView = viewHolder.itemView.findViewById(mViewId); if (trackingView == null) { return; } Rect rect = new Rect( 0, 0, trackingView.getWidth(), trackingView.getHeight()); recyclerView.offsetDescendantRectToMyCoords(trackingView, rect); // Slide transition may change the trackingView's translationX/translationY, // add up translation values in parent. float tx = 0, ty = 0; while (trackingView != recyclerView && trackingView != null) { // In RecyclerView dispatchLayout() it may call onScrolled(0) with a move // ItemAnimation just created. We don't have any way to track the ItemAnimation // update listener, and in ideal use case, the tracking view should not be // animated in RecyclerView. Do not apply translation value for this case. if (!(trackingView.getParent() == recyclerView && recyclerView.isAnimating())) { tx += trackingView.getTranslationX(); ty += trackingView.getTranslationY(); } trackingView = (View) trackingView.getParent(); } rect.offset((int) tx, (int) ty); if (source.mIsVertical) { source.setIntPropertyValue(getIndex(), rect.top + mOffset + (int) (mFraction * rect.height())); } else { source.setIntPropertyValue(getIndex(), rect.left + mOffset + (int) (mFraction * rect.width())); } } } } @Override public ChildPositionProperty createProperty(String name, int index) { return new ChildPositionProperty(name, index); } @Override public float getMaxValue() { if (mRecylerView == null) { return 0; } return mIsVertical ? mRecylerView.getHeight() : mRecylerView.getWidth(); } /** * Set RecyclerView that this Parallax will register onScrollListener. * @param recyclerView RecyclerView to register onScrollListener. */ public void setRecyclerView(RecyclerView recyclerView) { if (mRecylerView == recyclerView) { return; } if (mRecylerView != null) { mRecylerView.removeOnScrollListener(mOnScrollListener); mRecylerView.removeOnLayoutChangeListener(mOnLayoutChangeListener); } mRecylerView = recyclerView; if (mRecylerView != null) { LayoutManager.Properties properties = mRecylerView.getLayoutManager() .getProperties(mRecylerView.getContext(), null, 0, 0); mIsVertical = properties.orientation == RecyclerView.VERTICAL; mRecylerView.addOnScrollListener(mOnScrollListener); mRecylerView.addOnLayoutChangeListener(mOnLayoutChangeListener); } } /** * Manually update values. This is used for changes not controlled by RecyclerView. E.g. * called by a Slide transition that changes translation of the view. */ @Override public void updateValues() { for (Property prop: getProperties()) { ((ChildPositionProperty) prop).updateValue(RecyclerViewParallax.this); } super.updateValues(); } /** * @return Currently RecylerView that the source has registered onScrollListener. */ public RecyclerView getRecyclerView() { return mRecylerView; } }