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