• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.leanback.widget;
18 
19 import static androidx.recyclerview.widget.RecyclerView.LayoutManager;
20 import static androidx.recyclerview.widget.RecyclerView.OnScrollListener;
21 import static androidx.recyclerview.widget.RecyclerView.ViewHolder;
22 
23 import android.graphics.Rect;
24 import android.util.Property;
25 import android.view.View;
26 
27 import androidx.recyclerview.widget.RecyclerView;
28 
29 /**
30  * Implementation of {@link Parallax} class for {@link RecyclerView}. This class
31  * allows users to track position of specific views inside {@link RecyclerView} relative to
32  * itself. @see {@link ChildPositionProperty} for details.
33  */
34 public class RecyclerViewParallax extends Parallax<RecyclerViewParallax.ChildPositionProperty> {
35     RecyclerView mRecylerView;
36     boolean mIsVertical;
37 
38     OnScrollListener mOnScrollListener = new OnScrollListener() {
39         @Override
40         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
41             updateValues();
42         }
43     };
44 
45     View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() {
46         @Override
47         public void onLayoutChange(View view, int l, int t, int r, int b,
48                 int oldL, int oldT, int oldR, int oldB) {
49             updateValues();
50         }
51     };
52 
53     /**
54      * Subclass of {@link Parallax.IntProperty}. Using this Property, users can track a
55      * RecylerView child's position inside recyclerview. i.e.
56      *
57      * tracking_pos = view.top + fraction * view.height() + offset
58      *
59      * This way we can track top using fraction 0 and bottom using fraction 1.
60      */
61     public static final class ChildPositionProperty extends Parallax.IntProperty {
62         int mAdapterPosition;
63         int mViewId;
64         int mOffset;
65         float mFraction;
66 
ChildPositionProperty(String name, int index)67         ChildPositionProperty(String name, int index) {
68             super(name, index);
69         }
70 
71         /**
72          * Sets adapter position of the recyclerview child to track.
73          *
74          * @param adapterPosition Zero based position in adapter.
75          * @return This ChildPositionProperty object.
76          */
adapterPosition(int adapterPosition)77         public ChildPositionProperty adapterPosition(int adapterPosition) {
78             mAdapterPosition = adapterPosition;
79             return this;
80         };
81 
82         /**
83          * Sets view Id of a descendant of recyclerview child to track.
84          *
85          * @param viewId Id of a descendant of recyclerview child.
86          * @return This ChildPositionProperty object.
87          */
viewId(int viewId)88         public ChildPositionProperty viewId(int viewId) {
89             mViewId = viewId;
90             return this;
91         }
92 
93         /**
94          * Sets offset in pixels added to the view's start position.
95          *
96          * @param offset Offset in pixels added to the view's start position.
97          * @return This ChildPositionProperty object.
98          */
offset(int offset)99         public ChildPositionProperty offset(int offset) {
100             mOffset = offset;
101             return this;
102         }
103 
104         /**
105          * Sets fraction of size to be added to view's start position.  e.g. to track the
106          * center position of the view, use fraction 0.5; to track the end position of the view
107          * use fraction 1.
108          *
109          * @param fraction Fraction of size of the view.
110          * @return This ChildPositionProperty object.
111          */
fraction(float fraction)112         public ChildPositionProperty fraction(float fraction) {
113             mFraction = fraction;
114             return this;
115         }
116 
117         /**
118          * Returns adapter position of the recyclerview child to track.
119          */
getAdapterPosition()120         public int getAdapterPosition() {
121             return mAdapterPosition;
122         }
123 
124         /**
125          * Returns view Id of a descendant of recyclerview child to track.
126          */
getViewId()127         public int getViewId() {
128             return mViewId;
129         }
130 
131         /**
132          * Returns offset in pixels added to the view's start position.
133          */
getOffset()134         public int getOffset() {
135             return mOffset;
136         }
137 
138         /**
139          * Returns fraction of size to be added to view's start position.  e.g. to track the
140          * center position of the view, use fraction 0.5; to track the end position of the view
141          * use fraction 1.
142          */
getFraction()143         public float getFraction() {
144             return mFraction;
145         }
146 
updateValue(RecyclerViewParallax source)147         void updateValue(RecyclerViewParallax source) {
148             RecyclerView recyclerView = source.mRecylerView;
149             ViewHolder viewHolder = recyclerView == null ? null
150                     : recyclerView.findViewHolderForAdapterPosition(mAdapterPosition);
151             if (viewHolder == null) {
152                 if (recyclerView == null || recyclerView.getLayoutManager().getChildCount() == 0) {
153                     source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER);
154                     return;
155                 }
156                 View firstChild = recyclerView.getLayoutManager().getChildAt(0);
157                 ViewHolder vh = recyclerView.findContainingViewHolder(firstChild);
158                 int firstPosition = vh.getAdapterPosition();
159                 if (firstPosition < mAdapterPosition) {
160                     source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_AFTER);
161                 } else {
162                     source.setIntPropertyValue(getIndex(), IntProperty.UNKNOWN_BEFORE);
163                 }
164             } else {
165                 View trackingView = viewHolder.itemView.findViewById(mViewId);
166                 if (trackingView == null) {
167                     return;
168                 }
169 
170                 Rect rect = new Rect(
171                         0, 0, trackingView.getWidth(), trackingView.getHeight());
172                 recyclerView.offsetDescendantRectToMyCoords(trackingView, rect);
173                 // Slide transition may change the trackingView's translationX/translationY,
174                 // add up translation values in parent.
175                 float tx = 0, ty = 0;
176                 while (trackingView != recyclerView && trackingView != null) {
177                     // In RecyclerView dispatchLayout() it may call onScrolled(0) with a move
178                     // ItemAnimation just created. We don't have any way to track the ItemAnimation
179                     // update listener, and in ideal use case, the tracking view should not be
180                     // animated in RecyclerView. Do not apply translation value for this case.
181                     if (!(trackingView.getParent() == recyclerView && recyclerView.isAnimating())) {
182                         tx += trackingView.getTranslationX();
183                         ty += trackingView.getTranslationY();
184                     }
185                     trackingView = (View) trackingView.getParent();
186                 }
187                 rect.offset((int) tx, (int) ty);
188                 if (source.mIsVertical) {
189                     source.setIntPropertyValue(getIndex(), rect.top + mOffset
190                             + (int) (mFraction * rect.height()));
191                 } else {
192                     source.setIntPropertyValue(getIndex(), rect.left + mOffset
193                             + (int) (mFraction * rect.width()));
194                 }
195             }
196         }
197     }
198 
199 
200     @Override
createProperty(String name, int index)201     public ChildPositionProperty createProperty(String name, int index) {
202         return new ChildPositionProperty(name, index);
203     }
204 
205     @Override
getMaxValue()206     public float getMaxValue() {
207         if (mRecylerView == null) {
208             return 0;
209         }
210         return mIsVertical ? mRecylerView.getHeight() : mRecylerView.getWidth();
211     }
212 
213     /**
214      * Set RecyclerView that this Parallax will register onScrollListener.
215      * @param recyclerView RecyclerView to register onScrollListener.
216      */
setRecyclerView(RecyclerView recyclerView)217     public void setRecyclerView(RecyclerView recyclerView) {
218         if (mRecylerView == recyclerView) {
219             return;
220         }
221         if (mRecylerView != null) {
222             mRecylerView.removeOnScrollListener(mOnScrollListener);
223             mRecylerView.removeOnLayoutChangeListener(mOnLayoutChangeListener);
224         }
225         mRecylerView = recyclerView;
226         if (mRecylerView != null) {
227             LayoutManager.Properties properties = mRecylerView.getLayoutManager()
228                     .getProperties(mRecylerView.getContext(), null, 0, 0);
229             mIsVertical = properties.orientation == RecyclerView.VERTICAL;
230             mRecylerView.addOnScrollListener(mOnScrollListener);
231             mRecylerView.addOnLayoutChangeListener(mOnLayoutChangeListener);
232         }
233     }
234 
235     /**
236      * Manually update values. This is used for changes not controlled by RecyclerView. E.g.
237      * called by a Slide transition that changes translation of the view.
238      */
239     @Override
updateValues()240     public void updateValues() {
241         for (Property prop: getProperties()) {
242             ((ChildPositionProperty) prop).updateValue(RecyclerViewParallax.this);
243         }
244         super.updateValues();
245     }
246 
247     /**
248      * @return Currently RecylerView that the source has registered onScrollListener.
249      */
getRecyclerView()250     public RecyclerView getRecyclerView() {
251         return mRecylerView;
252     }
253 }
254