/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.core.view; import static androidx.core.view.ViewCompat.TYPE_NON_TOUCH; import static androidx.core.view.ViewCompat.TYPE_TOUCH; import android.view.View; import android.view.ViewParent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat.NestedScrollType; import androidx.core.view.ViewCompat.ScrollAxis; /** * Helper class for implementing nested scrolling child views compatible with Android platform * versions earlier than Android 5.0 Lollipop (API 21). * *

{@link android.view.View View} subclasses should instantiate a final instance of this * class as a field at construction. For each View method that has a matching * method signature in this class, delegate the operation to the helper instance in an overridden * method implementation. This implements the standard framework policy for nested scrolling.

* *

Views invoking nested scrolling functionality should always do so from the relevant * {@link androidx.core.view.ViewCompat}, {@link androidx.core.view.ViewGroupCompat} or * {@link androidx.core.view.ViewParentCompat} compatibility * shim static methods. This ensures interoperability with nested scrolling views on Android * 5.0 Lollipop and newer.

*/ public class NestedScrollingChildHelper { private ViewParent mNestedScrollingParentTouch; private ViewParent mNestedScrollingParentNonTouch; private final View mView; private boolean mIsNestedScrollingEnabled; private int[] mTempNestedScrollConsumed; /** * Construct a new helper for a given view. */ public NestedScrollingChildHelper(@NonNull View view) { mView = view; } /** * Enable nested scrolling. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @param enabled true to enable nested scrolling dispatch from this view, false otherwise */ public void setNestedScrollingEnabled(boolean enabled) { if (mIsNestedScrollingEnabled) { ViewCompat.stopNestedScroll(mView); } mIsNestedScrollingEnabled = enabled; } /** * Check if nested scrolling is enabled for this view. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if nested scrolling is enabled for this view */ public boolean isNestedScrollingEnabled() { return mIsNestedScrollingEnabled; } /** * Check if this view has a nested scrolling parent view currently receiving events for * a nested scroll in progress with the type of touch. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if this view has a nested scrolling parent, false otherwise */ public boolean hasNestedScrollingParent() { return hasNestedScrollingParent(TYPE_TOUCH); } /** * Check if this view has a nested scrolling parent view currently receiving events for * a nested scroll in progress with the given type. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if this view has a nested scrolling parent, false otherwise */ public boolean hasNestedScrollingParent(@NestedScrollType int type) { return getNestedScrollingParentForType(type) != null; } /** * Start a new nested scroll for this view. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @param axes Supported nested scroll axes. * See {@link androidx.core.view.NestedScrollingChild#startNestedScroll(int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(@ScrollAxis int axes) { return startNestedScroll(axes, TYPE_TOUCH); } /** * Start a new nested scroll for this view. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same * signature to implement the standard policy.

* * @param axes Supported nested scroll axes. * See {@link androidx.core.view.NestedScrollingChild2#startNestedScroll(int, * int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { if (hasNestedScrollingParent(type)) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) { setNestedScrollingParentForType(type, p); ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; } /** * Stop a nested scroll in progress. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

*/ public void stopNestedScroll() { stopNestedScroll(TYPE_TOUCH); } /** * Stop a nested scroll in progress. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same * signature to implement the standard policy.

*/ public void stopNestedScroll(@NestedScrollType int type) { ViewParent parent = getNestedScrollingParentForType(type); if (parent != null) { ViewParentCompat.onStopNestedScroll(parent, mView, type); setNestedScrollingParentForType(type, null); } } /** * Dispatch one step of a nested scrolling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed any of the nested scroll */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, TYPE_TOUCH); } /** * Dispatch one step of a nested scrolling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed any of the nested scroll */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) { if (isNestedScrollingEnabled()) { final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; } if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } ViewParentCompat.onNestedScroll(parent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return true; } else if (offsetInWindow != null) { // No motion, no dispatch. Keep offsetInWindow up to date. offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; } /** * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed any of the nested scroll */ public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH); } /** * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild2} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed any of the nested scroll */ public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) { if (isNestedScrollingEnabled()) { final ViewParent parent = getNestedScrollingParentForType(type); if (parent == null) { return false; } if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { if (mTempNestedScrollConsumed == null) { mTempNestedScrollConsumed = new int[2]; } consumed = mTempNestedScrollConsumed; } consumed[0] = 0; consumed[1] = 0; ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return consumed[0] != 0 || consumed[1] != 0; } else if (offsetInWindow != null) { offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; } /** * Dispatch a nested fling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed the nested fling */ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { if (isNestedScrollingEnabled()) { ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH); if (parent != null) { return ViewParentCompat.onNestedFling(parent, mView, velocityX, velocityY, consumed); } } return false; } /** * Dispatch a nested pre-fling operation to the current nested scrolling parent. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @return true if the parent consumed the nested fling */ public boolean dispatchNestedPreFling(float velocityX, float velocityY) { if (isNestedScrollingEnabled()) { ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH); if (parent != null) { return ViewParentCompat.onNestedPreFling(parent, mView, velocityX, velocityY); } } return false; } /** * View subclasses should always call this method on their * NestedScrollingChildHelper when detached from a window. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

*/ public void onDetachedFromWindow() { ViewCompat.stopNestedScroll(mView); } /** * Called when a nested scrolling child stops its current nested scroll operation. * *

This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link androidx.core.view.NestedScrollingChild} interface method with the same * signature to implement the standard policy.

* * @param child Child view stopping its nested scroll. This may not be a direct child view. */ public void onStopNestedScroll(@NonNull View child) { ViewCompat.stopNestedScroll(mView); } private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) { switch (type) { case TYPE_TOUCH: return mNestedScrollingParentTouch; case TYPE_NON_TOUCH: return mNestedScrollingParentNonTouch; } return null; } private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) { switch (type) { case TYPE_TOUCH: mNestedScrollingParentTouch = p; break; case TYPE_NON_TOUCH: mNestedScrollingParentNonTouch = p; break; } } }