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
17package androidx.leanback.widget;
18
19import static androidx.recyclerview.widget.RecyclerView.LayoutManager;
20import static androidx.recyclerview.widget.RecyclerView.OnScrollListener;
21import static androidx.recyclerview.widget.RecyclerView.ViewHolder;
22
23import android.graphics.Rect;
24import android.util.Property;
25import android.view.View;
26
27import 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 */
34public 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
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         */
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         */
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         */
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         */
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         */
120        public int getAdapterPosition() {
121            return mAdapterPosition;
122        }
123
124        /**
125         * Returns view Id of a descendant of recyclerview child to track.
126         */
127        public int getViewId() {
128            return mViewId;
129        }
130
131        /**
132         * Returns offset in pixels added to the view's start position.
133         */
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         */
143        public float getFraction() {
144            return mFraction;
145        }
146
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
201    public ChildPositionProperty createProperty(String name, int index) {
202        return new ChildPositionProperty(name, index);
203    }
204
205    @Override
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     */
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
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     */
250    public RecyclerView getRecyclerView() {
251        return mRecylerView;
252    }
253}
254