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