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