NestedScrollingChildHelper.java revision 76daed103193a1756535d1f59b165e98e1d17445
1/*
2 * Copyright (C) 2015 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
18package android.support.v4.view;
19
20import static android.support.v4.view.ViewCompat.TYPE_NON_TOUCH;
21import static android.support.v4.view.ViewCompat.TYPE_TOUCH;
22
23import android.support.annotation.NonNull;
24import android.support.annotation.Nullable;
25import android.support.v4.view.ViewCompat.NestedScrollType;
26import android.support.v4.view.ViewCompat.ScrollAxis;
27import android.view.View;
28import android.view.ViewParent;
29
30/**
31 * Helper class for implementing nested scrolling child views compatible with Android platform
32 * versions earlier than Android 5.0 Lollipop (API 21).
33 *
34 * <p>{@link android.view.View View} subclasses should instantiate a final instance of this
35 * class as a field at construction. For each <code>View</code> method that has a matching
36 * method signature in this class, delegate the operation to the helper instance in an overridden
37 * method implementation. This implements the standard framework policy for nested scrolling.</p>
38 *
39 * <p>Views invoking nested scrolling functionality should always do so from the relevant
40 * {@link android.support.v4.view.ViewCompat}, {@link android.support.v4.view.ViewGroupCompat} or
41 * {@link android.support.v4.view.ViewParentCompat} compatibility
42 * shim static methods. This ensures interoperability with nested scrolling views on Android
43 * 5.0 Lollipop and newer.</p>
44 */
45public class NestedScrollingChildHelper {
46    private ViewParent mNestedScrollingParentTouch;
47    private ViewParent mNestedScrollingParentNonTouch;
48    private final View mView;
49    private boolean mIsNestedScrollingEnabled;
50    private int[] mTempNestedScrollConsumed;
51
52    /**
53     * Construct a new helper for a given view.
54     */
55    public NestedScrollingChildHelper(@NonNull View view) {
56        mView = view;
57    }
58
59    /**
60     * Enable nested scrolling.
61     *
62     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
63     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
64     * signature to implement the standard policy.</p>
65     *
66     * @param enabled true to enable nested scrolling dispatch from this view, false otherwise
67     */
68    public void setNestedScrollingEnabled(boolean enabled) {
69        if (mIsNestedScrollingEnabled) {
70            ViewCompat.stopNestedScroll(mView);
71        }
72        mIsNestedScrollingEnabled = enabled;
73    }
74
75    /**
76     * Check if nested scrolling is enabled for this view.
77     *
78     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
79     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
80     * signature to implement the standard policy.</p>
81     *
82     * @return true if nested scrolling is enabled for this view
83     */
84    public boolean isNestedScrollingEnabled() {
85        return mIsNestedScrollingEnabled;
86    }
87
88    /**
89     * Check if this view has a nested scrolling parent view currently receiving events for
90     * a nested scroll in progress with the type of touch.
91     *
92     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
93     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
94     * signature to implement the standard policy.</p>
95     *
96     * @return true if this view has a nested scrolling parent, false otherwise
97     */
98    public boolean hasNestedScrollingParent() {
99        return hasNestedScrollingParent(TYPE_TOUCH);
100    }
101
102    /**
103     * Check if this view has a nested scrolling parent view currently receiving events for
104     * a nested scroll in progress with the given type.
105     *
106     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
107     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
108     * signature to implement the standard policy.</p>
109     *
110     * @return true if this view has a nested scrolling parent, false otherwise
111     */
112    public boolean hasNestedScrollingParent(@NestedScrollType int type) {
113        return getNestedScrollingParentForType(type) != null;
114    }
115
116    /**
117     * Start a new nested scroll for this view.
118     *
119     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
120     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
121     * signature to implement the standard policy.</p>
122     *
123     * @param axes Supported nested scroll axes.
124     *             See {@link android.support.v4.view.NestedScrollingChild#startNestedScroll(int)}.
125     * @return true if a cooperating parent view was found and nested scrolling started successfully
126     */
127    public boolean startNestedScroll(@ScrollAxis int axes) {
128        return startNestedScroll(axes, TYPE_TOUCH);
129    }
130
131    /**
132     * Start a new nested scroll for this view.
133     *
134     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
135     * method/{@link android.support.v4.view.NestedScrollingChild2} interface method with the same
136     * signature to implement the standard policy.</p>
137     *
138     * @param axes Supported nested scroll axes.
139     *             See {@link android.support.v4.view.NestedScrollingChild2#startNestedScroll(int,
140     *             int)}.
141     * @return true if a cooperating parent view was found and nested scrolling started successfully
142     */
143    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
144        if (hasNestedScrollingParent(type)) {
145            // Already in progress
146            return true;
147        }
148        if (isNestedScrollingEnabled()) {
149            ViewParent p = mView.getParent();
150            View child = mView;
151            while (p != null) {
152                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
153                    setNestedScrollingParentForType(type, p);
154                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
155                    return true;
156                }
157                if (p instanceof View) {
158                    child = (View) p;
159                }
160                p = p.getParent();
161            }
162        }
163        return false;
164    }
165
166    /**
167     * Stop a nested scroll in progress.
168     *
169     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
170     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
171     * signature to implement the standard policy.</p>
172     */
173    public void stopNestedScroll() {
174        stopNestedScroll(TYPE_TOUCH);
175    }
176
177    /**
178     * Stop a nested scroll in progress.
179     *
180     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
181     * method/{@link android.support.v4.view.NestedScrollingChild2} interface method with the same
182     * signature to implement the standard policy.</p>
183     */
184    public void stopNestedScroll(@NestedScrollType int type) {
185        ViewParent parent = getNestedScrollingParentForType(type);
186        if (parent != null) {
187            ViewParentCompat.onStopNestedScroll(parent, mView, type);
188            setNestedScrollingParentForType(type, null);
189        }
190    }
191
192    /**
193     * Dispatch one step of a nested scrolling operation to the current nested scrolling parent.
194     *
195     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
196     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
197     * signature to implement the standard policy.</p>
198     *
199     * @return true if the parent consumed any of the nested scroll
200     */
201    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
202            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
203        return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
204                offsetInWindow, TYPE_TOUCH);
205    }
206
207    /**
208     * Dispatch one step of a nested scrolling operation to the current nested scrolling parent.
209     *
210     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
211     * method/{@link android.support.v4.view.NestedScrollingChild2} interface method with the same
212     * signature to implement the standard policy.</p>
213     *
214     * @return true if the parent consumed any of the nested scroll
215     */
216    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
217            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
218            @NestedScrollType int type) {
219        if (isNestedScrollingEnabled()) {
220            final ViewParent parent = getNestedScrollingParentForType(type);
221            if (parent == null) {
222                return false;
223            }
224
225            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
226                int startX = 0;
227                int startY = 0;
228                if (offsetInWindow != null) {
229                    mView.getLocationInWindow(offsetInWindow);
230                    startX = offsetInWindow[0];
231                    startY = offsetInWindow[1];
232                }
233
234                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
235                        dyConsumed, dxUnconsumed, dyUnconsumed, type);
236
237                if (offsetInWindow != null) {
238                    mView.getLocationInWindow(offsetInWindow);
239                    offsetInWindow[0] -= startX;
240                    offsetInWindow[1] -= startY;
241                }
242                return true;
243            } else if (offsetInWindow != null) {
244                // No motion, no dispatch. Keep offsetInWindow up to date.
245                offsetInWindow[0] = 0;
246                offsetInWindow[1] = 0;
247            }
248        }
249        return false;
250    }
251
252    /**
253     * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
254     *
255     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
256     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
257     * signature to implement the standard policy.</p>
258     *
259     * @return true if the parent consumed any of the nested scroll
260     */
261    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
262            @Nullable int[] offsetInWindow) {
263        return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
264    }
265
266    /**
267     * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent.
268     *
269     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
270     * method/{@link android.support.v4.view.NestedScrollingChild2} interface method with the same
271     * signature to implement the standard policy.</p>
272     *
273     * @return true if the parent consumed any of the nested scroll
274     */
275    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
276            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
277        if (isNestedScrollingEnabled()) {
278            final ViewParent parent = getNestedScrollingParentForType(type);
279            if (parent == null) {
280                return false;
281            }
282
283            if (dx != 0 || dy != 0) {
284                int startX = 0;
285                int startY = 0;
286                if (offsetInWindow != null) {
287                    mView.getLocationInWindow(offsetInWindow);
288                    startX = offsetInWindow[0];
289                    startY = offsetInWindow[1];
290                }
291
292                if (consumed == null) {
293                    if (mTempNestedScrollConsumed == null) {
294                        mTempNestedScrollConsumed = new int[2];
295                    }
296                    consumed = mTempNestedScrollConsumed;
297                }
298                consumed[0] = 0;
299                consumed[1] = 0;
300                ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
301
302                if (offsetInWindow != null) {
303                    mView.getLocationInWindow(offsetInWindow);
304                    offsetInWindow[0] -= startX;
305                    offsetInWindow[1] -= startY;
306                }
307                return consumed[0] != 0 || consumed[1] != 0;
308            } else if (offsetInWindow != null) {
309                offsetInWindow[0] = 0;
310                offsetInWindow[1] = 0;
311            }
312        }
313        return false;
314    }
315
316    /**
317     * Dispatch a nested fling operation to the current nested scrolling parent.
318     *
319     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
320     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
321     * signature to implement the standard policy.</p>
322     *
323     * @return true if the parent consumed the nested fling
324     */
325    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
326        if (isNestedScrollingEnabled()) {
327            ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
328            if (parent != null) {
329                return ViewParentCompat.onNestedFling(parent, mView, velocityX,
330                        velocityY, consumed);
331            }
332        }
333        return false;
334    }
335
336    /**
337     * Dispatch a nested pre-fling operation to the current nested scrolling parent.
338     *
339     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
340     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
341     * signature to implement the standard policy.</p>
342     *
343     * @return true if the parent consumed the nested fling
344     */
345    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
346        if (isNestedScrollingEnabled()) {
347            ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
348            if (parent != null) {
349                return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
350                        velocityY);
351            }
352        }
353        return false;
354    }
355
356    /**
357     * View subclasses should always call this method on their
358     * <code>NestedScrollingChildHelper</code> when detached from a window.
359     *
360     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
361     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
362     * signature to implement the standard policy.</p>
363     */
364    public void onDetachedFromWindow() {
365        ViewCompat.stopNestedScroll(mView);
366    }
367
368    /**
369     * Called when a nested scrolling child stops its current nested scroll operation.
370     *
371     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
372     * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
373     * signature to implement the standard policy.</p>
374     *
375     * @param child Child view stopping its nested scroll. This may not be a direct child view.
376     */
377    public void onStopNestedScroll(@NonNull View child) {
378        ViewCompat.stopNestedScroll(mView);
379    }
380
381    private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
382        switch (type) {
383            case TYPE_TOUCH:
384                return mNestedScrollingParentTouch;
385            case TYPE_NON_TOUCH:
386                return mNestedScrollingParentNonTouch;
387        }
388        return null;
389    }
390
391    private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
392        switch (type) {
393            case TYPE_TOUCH:
394                mNestedScrollingParentTouch = p;
395                break;
396            case TYPE_NON_TOUCH:
397                mNestedScrollingParentNonTouch = p;
398                break;
399        }
400    }
401}
402