/* * Copyright 2024 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 com.android.intentresolver.widget; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.hardware.SensorManager; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.FocusFinder; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.widget.EdgeEffect; import android.widget.FrameLayout; import android.widget.OverScroller; import android.widget.ScrollView; import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; import androidx.core.R; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.DifferentialMotionFlingController; import androidx.core.view.DifferentialMotionFlingTarget; import androidx.core.view.MotionEventCompat; import androidx.core.view.NestedScrollingChild3; import androidx.core.view.NestedScrollingChildHelper; import androidx.core.view.NestedScrollingParent3; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ScrollingView; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityRecordCompat; import androidx.core.widget.EdgeEffectCompat; import java.util.List; /** * A copy of the {@link androidx.core.widget.NestedScrollView} (from * prebuilts/sdk/current/androidx/m2repository/androidx/core/core/1.13.0-beta01/core-1.13.0-beta01-sources.jar) * without any functional changes with a pure refactoring of {@link #requestChildFocus(View, View)}: * the method's body is extracted into the new protected method, * {@link #onRequestChildFocus(View, View)}. *
* For the exact change see NestedScrollView.java.patch file. *
*/ public class NestedScrollView extends FrameLayout implements NestedScrollingParent3, NestedScrollingChild3, ScrollingView { static final int ANIMATED_SCROLL_GAP = 250; static final float MAX_SCROLL_FACTOR = 0.5f; private static final String TAG = "NestedScrollView"; private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 250; /** * The following are copied from OverScroller to determine how far a fling will go. */ private static final float SCROLL_FRICTION = 0.015f; private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); private final float mPhysicalCoeff; /** * When flinging the stretch towards scrolling content, it should destretch quicker than the * fling would normally do. The visual effect of flinging the stretch looks strange as little * appears to happen at first and then when the stretch disappears, the content starts * scrolling quickly. */ private static final float FLING_DESTRETCH_FACTOR = 4f; /** * Interface definition for a callback to be invoked when the scroll * X or Y positions of a view change. * *This version of the interface works on all versions of Android, back to API v4.
* * @see #setOnScrollChangeListener(OnScrollChangeListener) */ public interface OnScrollChangeListener { /** * Called when the scroll position of a view changes. * @param v The view whose scroll position has changed. * @param scrollX Current horizontal scroll origin. * @param scrollY Current vertical scroll origin. * @param oldScrollX Previous horizontal scroll origin. * @param oldScrollY Previous vertical scroll origin. */ void onScrollChange(@NonNull NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY); } private long mLastScroll; private final Rect mTempRect = new Rect(); private OverScroller mScroller; @RestrictTo(LIBRARY) @VisibleForTesting @NonNull public EdgeEffect mEdgeGlowTop; @RestrictTo(LIBRARY) @VisibleForTesting @NonNull public EdgeEffect mEdgeGlowBottom; /** * Position of the last motion event; only used with touch related events (usually to assist * in movement changes in a drag gesture). */ private int mLastMotionY; /** * True when the layout has changed but the traversal has not come through yet. * Ideally the view hierarchy would keep track of this for us. */ private boolean mIsLayoutDirty = true; private boolean mIsLaidOut = false; /** * The child to give focus to in the event that a child has requested focus while the * layout is dirty. This prevents the scroll from being wrong if the child has not been * laid out before requesting focus. */ private View mChildToScrollTo = null; /** * True if the user is currently dragging this ScrollView around. This is * not the same as 'is being flinged', which can be checked by * mScroller.isFinished() (flinging begins when the user lifts their finger). */ private boolean mIsBeingDragged = false; /** * Determines speed during touch scrolling */ private VelocityTracker mVelocityTracker; /** * When set to true, the scroll view measure its child to make it fill the currently * visible area. */ private boolean mFillViewport; /** * Whether arrow scrolling is animated. */ private boolean mSmoothScrollingEnabled = true; private int mTouchSlop; private int mMinimumVelocity; private int mMaximumVelocity; /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mActivePointerId = INVALID_POINTER; /** * Used during scrolling to retrieve the new offset within the window. Saves memory by saving * x, y changes to this array (0 position = x, 1 position = y) vs. reallocating an x and y * every time. */ private final int[] mScrollOffset = new int[2]; /* * Used during scrolling to retrieve the new consumed offset within the window. * Uses same memory saving strategy as mScrollOffset. */ private final int[] mScrollConsumed = new int[2]; // Used to track the position of the touch only events relative to the container. private int mNestedYOffset; private int mLastScrollerY; /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; private SavedState mSavedState; private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); private static final int[] SCROLLVIEW_STYLEABLE = new int[] { android.R.attr.fillViewport }; private final NestedScrollingParentHelper mParentHelper; private final NestedScrollingChildHelper mChildHelper; private float mVerticalScrollFactor; private OnScrollChangeListener mOnScrollChangeListener; @VisibleForTesting final DifferentialMotionFlingTargetImpl mDifferentialMotionFlingTarget = new DifferentialMotionFlingTargetImpl(); @VisibleForTesting DifferentialMotionFlingController mDifferentialMotionFlingController = new DifferentialMotionFlingController(getContext(), mDifferentialMotionFlingTarget); public NestedScrollView(@NonNull Context context) { this(context, null); } public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.nestedScrollViewStyle); } public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mEdgeGlowTop = EdgeEffectCompat.create(context, attrs); mEdgeGlowBottom = EdgeEffectCompat.create(context, attrs); final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi * 0.84f; // look and feel tuning initScrollView(); final TypedArray a = context.obtainStyledAttributes( attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); setFillViewport(a.getBoolean(0, false)); a.recycle(); mParentHelper = new NestedScrollingParentHelper(this); mChildHelper = new NestedScrollingChildHelper(this); // ...because why else would you be using this widget? setNestedScrollingEnabled(true); ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); } // NestedScrollingChild3 @Override public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) { mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); } // NestedScrollingChild2 @Override public boolean startNestedScroll(int axes, int type) { return mChildHelper.startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { mChildHelper.stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return mChildHelper.hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public boolean dispatchNestedPreScroll( int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type ) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return startNestedScroll(axes, ViewCompat.TYPE_TOUCH); } @Override public void stopNestedScroll() { stopNestedScroll(ViewCompat.TYPE_TOUCH); } @Override public boolean hasNestedScrollingParent() { return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } // NestedScrollingParent3 @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) { onNestedScrollInternal(dyUnconsumed, type, consumed); } private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) { final int oldScrollY = getScrollY(); scrollBy(0, dyUnconsumed); final int myConsumed = getScrollY() - oldScrollY; if (consumed != null) { consumed[1] += myConsumed; } final int myUnconsumed = dyUnconsumed - myConsumed; mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed); } // NestedScrollingParent2 @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mParentHelper.onNestedScrollAccepted(child, target, axes, type); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type); } @Override public void onStopNestedScroll(@NonNull View target, int type) { mParentHelper.onStopNestedScroll(target, type); stopNestedScroll(type); } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { onNestedScrollInternal(dyUnconsumed, type, null); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { dispatchNestedPreScroll(dx, dy, consumed, null, type); } // NestedScrollingParent @Override public boolean onStartNestedScroll( @NonNull View child, @NonNull View target, int axes) { return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScrollAccepted( @NonNull View child, @NonNull View target, int axes) { onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH); } @Override public void onStopNestedScroll(@NonNull View target) { onStopNestedScroll(target, ViewCompat.TYPE_TOUCH); } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null); } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) { onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH); } @Override public boolean onNestedFling( @NonNull View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { dispatchNestedFling(0, velocityY, true); fling((int) velocityY); return true; } return false; } @Override public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } // ScrollView import @Override public boolean shouldDelayChildPressedState() { return true; } @Override protected float getTopFadingEdgeStrength() { if (getChildCount() == 0) { return 0.0f; } final int length = getVerticalFadingEdgeLength(); final int scrollY = getScrollY(); if (scrollY < length) { return scrollY / (float) length; } return 1.0f; } @Override protected float getBottomFadingEdgeStrength() { if (getChildCount() == 0) { return 0.0f; } View child = getChildAt(0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int length = getVerticalFadingEdgeLength(); final int bottomEdge = getHeight() - getPaddingBottom(); final int span = child.getBottom() + lp.bottomMargin - getScrollY() - bottomEdge; if (span < length) { return span / (float) length; } return 1.0f; } /** * @return The maximum amount this scroll view will scroll in response to * an arrow event. */ public int getMaxScrollAmount() { return (int) (MAX_SCROLL_FACTOR * getHeight()); } private void initScrollView() { mScroller = new OverScroller(getContext()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } @Override public void addView(@NonNull View child) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index); } @Override public void addView(View child, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, params); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index, params); } /** * Register a callback to be invoked when the scroll X or Y positions of * this view change. *This version of the method works on all versions of Android, back to API v4.
* * @param l The listener to notify when the scroll X or Y position changes. * @see View#getScrollX() * @see View#getScrollY() */ public void setOnScrollChangeListener(@Nullable OnScrollChangeListener l) { mOnScrollChangeListener = l; } /** * @return Returns true this ScrollView can be scrolled */ private boolean canScroll() { if (getChildCount() > 0) { View child = getChildAt(0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin; int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom(); return childSize > parentSpace; } return false; } /** * Indicates whether this ScrollView's content is stretched to fill the viewport. * * @return True if the content fills the viewport, false otherwise. * * @attr name android:fillViewport */ public boolean isFillViewport() { return mFillViewport; } /** * Set whether this ScrollView should stretch its content height to fill the viewport or not. * * @param fillViewport True to stretch the content's height to the viewport's * boundaries, false otherwise. * * @attr name android:fillViewport */ public void setFillViewport(boolean fillViewport) { if (fillViewport != mFillViewport) { mFillViewport = fillViewport; requestLayout(); } } /** * @return Whether arrow scrolling will animate its transition. */ public boolean isSmoothScrollingEnabled() { return mSmoothScrollingEnabled; } /** * Set whether arrow scrolling will animate its transition. * @param smoothScrollingEnabled whether arrow scrolling will animate its transition */ public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { mSmoothScrollingEnabled = smoothScrollingEnabled; } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (mOnScrollChangeListener != null) { mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } if (getChildCount() > 0) { View child = getChildAt(0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childSize = child.getMeasuredHeight(); int parentSpace = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - lp.topMargin - lp.bottomMargin; if (childSize < parentSpace) { int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } @Override public boolean dispatchKeyEvent(KeyEvent event) { // Let the focused view and/or our descendants get the key first return super.dispatchKeyEvent(event) || executeKeyEvent(event); } /** * You can call this function yourself to have the scroll view perform * scrolling from a key event, just as if the event had been dispatched to * it by the view hierarchy. * * @param event The key event to execute. * @return Return true if the event was handled, else false. */ public boolean executeKeyEvent(@NonNull KeyEvent event) { mTempRect.setEmpty(); if (!canScroll()) { if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { View currentFocused = findFocus(); if (currentFocused == this) currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); } return false; } boolean handled = false; if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_UP: if (event.isAltPressed()) { handled = fullScroll(View.FOCUS_UP); } else { handled = arrowScroll(View.FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (event.isAltPressed()) { handled = fullScroll(View.FOCUS_DOWN); } else { handled = arrowScroll(View.FOCUS_DOWN); } break; case KeyEvent.KEYCODE_PAGE_UP: handled = fullScroll(View.FOCUS_UP); break; case KeyEvent.KEYCODE_PAGE_DOWN: handled = fullScroll(View.FOCUS_DOWN); break; case KeyEvent.KEYCODE_SPACE: pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); break; case KeyEvent.KEYCODE_MOVE_HOME: pageScroll(View.FOCUS_UP); break; case KeyEvent.KEYCODE_MOVE_END: pageScroll(View.FOCUS_DOWN); break; } } return handled; } private boolean inChild(int x, int y) { if (getChildCount() > 0) { final int scrollY = getScrollY(); final View child = getChildAt(0); return !(y < child.getTop() - scrollY || y >= child.getBottom() - scrollY || x < child.getLeft() || x >= child.getRight()); } return false; } private void initOrResetVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } } private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ /* * Shortcut the most recurring case: the user is in the dragging * state and they are moving their finger. We want to intercept this * motion. */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from their original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), y)) { mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished(); recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. We also want to catch the edge glow and start dragging * if one is being animated. We need to call computeScrollOffset() first so that * isFinished() is correct. */ mScroller.computeScrollOffset(); mIsBeingDragged = stopGlowAnimations(ev) || !mScroller.isFinished(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } stopNestedScroll(ViewCompat.TYPE_TOUCH); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; } @Override public boolean onTouchEvent(@NonNull MotionEvent motionEvent) { initVelocityTrackerIfNotExists(); final int actionMasked = motionEvent.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } MotionEvent velocityTrackerMotionEvent = MotionEvent.obtain(motionEvent); velocityTrackerMotionEvent.offsetLocation(0, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } // If additional fingers touch the screen while a drag is in progress, this block // of code will make sure the drag isn't interrupted. if (mIsBeingDragged) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged. */ if (!mScroller.isFinished()) { abortAnimatedScroll(); } initializeTouchDrag( (int) motionEvent.getY(), motionEvent.getPointerId(0) ); break; } case MotionEvent.ACTION_MOVE: { final int activePointerIndex = motionEvent.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } final int y = (int) motionEvent.getY(activePointerIndex); int deltaY = mLastMotionY - y; deltaY -= releaseVerticalGlow(deltaY, motionEvent.getX(activePointerIndex)); // Changes to dragged state if delta is greater than the slop (and not in // the dragged state). if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { final int x = (int) motionEvent.getX(activePointerIndex); int scrollOffset = scrollBy(deltaY, x, ViewCompat.TYPE_TOUCH, false); // Updates the global positions (used by later move events to properly scroll). mLastMotionY = y - scrollOffset; mNestedYOffset += scrollOffset; } break; } case MotionEvent.ACTION_UP: { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) >= mMinimumVelocity)) { if (!edgeEffectFling(initialVelocity) && !dispatchNestedPreFling(0, -initialVelocity)) { dispatchNestedFling(0, -initialVelocity, true); fling(-initialVelocity); } } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } endTouchDrag(); break; } case MotionEvent.ACTION_CANCEL: { if (mIsBeingDragged && getChildCount() > 0) { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } } endTouchDrag(); break; } case MotionEvent.ACTION_POINTER_DOWN: { final int index = motionEvent.getActionIndex(); mLastMotionY = (int) motionEvent.getY(index); mActivePointerId = motionEvent.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp(motionEvent); mLastMotionY = (int) motionEvent.getY(motionEvent.findPointerIndex(mActivePointerId)); break; } } if (mVelocityTracker != null) { mVelocityTracker.addMovement(velocityTrackerMotionEvent); } // Returns object back to be re-used by others. velocityTrackerMotionEvent.recycle(); return true; } private void initializeTouchDrag(int lastMotionY, int activePointerId) { mLastMotionY = lastMotionY; mActivePointerId = activePointerId; startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); } // Ends drag in a nested scroll. private void endTouchDrag() { mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; recycleVelocityTracker(); stopNestedScroll(ViewCompat.TYPE_TOUCH); mEdgeGlowTop.onRelease(); mEdgeGlowBottom.onRelease(); } /* * Handles scroll events for both touch and non-touch events (mouse scroll wheel, * rotary button, keyboard, etc.). * * Note: This function returns the total scroll offset for this scroll event which is required * for calculating the total scroll between multiple move events (touch). This returned value * is NOT needed for non-touch events since a scroll is a one time event (vs. touch where a * drag may be triggered multiple times with the movement of the finger). */ // TODO: You should rename this to nestedScrollBy() so it is different from View.scrollBy private int scrollBy( int verticalScrollDistance, int x, int touchType, boolean isSourceMouseOrKeyboard ) { int totalScrollOffset = 0; /* * Starts nested scrolling for non-touch events (mouse scroll wheel, rotary button, etc.). * This is in contrast to a touch event which would trigger the start of nested scrolling * with a touch down event outside of this method, since for a single gesture scrollBy() * might be called several times for a move event for a single drag gesture. */ if (touchType == ViewCompat.TYPE_NON_TOUCH) { startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, touchType); } // Dispatches scrolling delta amount available to parent (to consume what it needs). // Note: The amounts the parent consumes are saved in arrays named mScrollConsumed and // mScrollConsumed to save space. if (dispatchNestedPreScroll( 0, verticalScrollDistance, mScrollConsumed, mScrollOffset, touchType) ) { // Deducts the scroll amount (y) consumed by the parent (x in position 0, // y in position 1). Nested scroll only works with Y position (so we don't use x). verticalScrollDistance -= mScrollConsumed[1]; totalScrollOffset += mScrollOffset[1]; } // Retrieves the scroll y position (top position of this view) and scroll Y range (how far // the scroll can go). final int initialScrollY = getScrollY(); final int scrollRangeY = getScrollRange(); // Overscroll is for adding animations at the top/bottom of a view when the user scrolls // beyond the beginning/end of the view. Overscroll is not used with a mouse. boolean canOverscroll = canOverScroll() && !isSourceMouseOrKeyboard; // Scrolls content in the current View, but clamps it if it goes too far. boolean hitScrollBarrier = overScrollByCompat( 0, verticalScrollDistance, 0, initialScrollY, 0, scrollRangeY, 0, 0, true ) && !hasNestedScrollingParent(touchType); // The position may have been adjusted in the previous call, so we must revise our values. final int scrollYDelta = getScrollY() - initialScrollY; final int unconsumedY = verticalScrollDistance - scrollYDelta; // Reset the Y consumed scroll to zero mScrollConsumed[1] = 0; // Dispatch the unconsumed delta Y to the children to consume. dispatchNestedScroll( 0, scrollYDelta, 0, unconsumedY, mScrollOffset, touchType, mScrollConsumed ); totalScrollOffset += mScrollOffset[1]; // Handle overscroll of the children. verticalScrollDistance -= mScrollConsumed[1]; int newScrollY = initialScrollY + verticalScrollDistance; if (newScrollY < 0) { if (canOverscroll) { EdgeEffectCompat.onPullDistance( mEdgeGlowTop, (float) -verticalScrollDistance / getHeight(), (float) x / getWidth() ); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } } else if (newScrollY > scrollRangeY) { if (canOverscroll) { EdgeEffectCompat.onPullDistance( mEdgeGlowBottom, (float) verticalScrollDistance / getHeight(), 1.f - ((float) x / getWidth()) ); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } } if (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished()) { postInvalidateOnAnimation(); hitScrollBarrier = false; } if (hitScrollBarrier && (touchType == ViewCompat.TYPE_TOUCH)) { // Break our velocity if we hit a scroll barrier. if (mVelocityTracker != null) { mVelocityTracker.clear(); } } /* * Ends nested scrolling for non-touch events (mouse scroll wheel, rotary button, etc.). * As noted above, this is in contrast to a touch event. */ if (touchType == ViewCompat.TYPE_NON_TOUCH) { stopNestedScroll(touchType); // Required for scrolling with Rotary Device stretch top/bottom to work properly mEdgeGlowTop.onRelease(); mEdgeGlowBottom.onRelease(); } return totalScrollOffset; } /** * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should * animate with a fling. It will animate with a fling if the velocity will remove the * EdgeEffect through its normal operation. * * @param edgeEffect The EdgeEffect that might absorb the velocity. * @param velocity The velocity of the fling motion * @return true if the velocity should be absorbed or false if it should be flung. */ private boolean shouldAbsorb(@NonNull EdgeEffect edgeEffect, int velocity) { if (velocity > 0) { return true; } float distance = EdgeEffectCompat.getDistance(edgeEffect) * getHeight(); // This is flinging without the spring, so let's see if it will fling past the overscroll float flingDistance = getSplineFlingDistance(-velocity); return flingDistance < distance; } /** * If mTopGlow or mBottomGlow is currently active and the motion will remove some of the * stretch, this will consume any of unconsumedY that the glow can. If the motion would * increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed. * * @param unconsumedY The vertical delta that might be consumed by the vertical EdgeEffects * @return The remaining unconsumed delta after the edge effects have consumed. */ int consumeFlingInVerticalStretch(int unconsumedY) { int height = getHeight(); if (unconsumedY > 0 && EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0f) { float deltaDistance = -unconsumedY * FLING_DESTRETCH_FACTOR / height; int consumed = Math.round(-height / FLING_DESTRETCH_FACTOR * EdgeEffectCompat.onPullDistance(mEdgeGlowTop, deltaDistance, 0.5f)); if (consumed != unconsumedY) { mEdgeGlowTop.finish(); } return unconsumedY - consumed; } if (unconsumedY < 0 && EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0f) { float deltaDistance = unconsumedY * FLING_DESTRETCH_FACTOR / height; int consumed = Math.round(height / FLING_DESTRETCH_FACTOR * EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, deltaDistance, 0.5f)); if (consumed != unconsumedY) { mEdgeGlowBottom.finish(); } return unconsumedY - consumed; } return unconsumedY; } /** * Copied from OverScroller, this returns the distance that a fling with the given velocity * will go. * @param velocity The velocity of the fling * @return The distance that will be traveled by a fling of the given velocity. */ private float getSplineFlingDistance(int velocity) { final double l = Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * mPhysicalCoeff)); final double decelMinusOne = DECELERATION_RATE - 1.0; return (float) (SCROLL_FRICTION * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l)); } private boolean edgeEffectFling(int velocityY) { boolean consumed = true; if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) { if (shouldAbsorb(mEdgeGlowTop, velocityY)) { mEdgeGlowTop.onAbsorb(velocityY); } else { fling(-velocityY); } } else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) { if (shouldAbsorb(mEdgeGlowBottom, -velocityY)) { mEdgeGlowBottom.onAbsorb(-velocityY); } else { fling(-velocityY); } } else { consumed = false; } return consumed; } /** * This stops any edge glow animation that is currently running by applying a * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices, * this method does nothing, allowing any animating edge effect to continue animating and * returningfalse always.
*
* @param e The motion event to use to indicate the finger position for the displacement of
* the current pull.
* @return true if any edge effect had an existing effect to be drawn ond the
* animation was stopped or false if no edge effect had a value to display.
*/
private boolean stopGlowAnimations(MotionEvent e) {
boolean stopped = false;
if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
EdgeEffectCompat.onPullDistance(mEdgeGlowTop, 0, e.getX() / getWidth());
stopped = true;
}
if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, 0, 1 - e.getX() / getWidth());
stopped = true;
}
return stopped;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = (int) ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
@Override
public boolean onGenericMotionEvent(@NonNull MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_SCROLL && !mIsBeingDragged) {
final float verticalScroll;
final int x;
final int flingAxis;
if (MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_CLASS_POINTER)) {
verticalScroll = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
x = (int) motionEvent.getX();
flingAxis = MotionEvent.AXIS_VSCROLL;
} else if (
MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_ROTARY_ENCODER)
) {
verticalScroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL);
// Since a Wear rotary event doesn't have a true X and we want to support proper
// overscroll animations, we put the x at the center of the screen.
x = getWidth() / 2;
flingAxis = MotionEvent.AXIS_SCROLL;
} else {
verticalScroll = 0;
x = 0;
flingAxis = 0;
}
if (verticalScroll != 0) {
// Rotary and Mouse scrolls are inverted from a touch scroll.
final int invertedDelta = (int) (verticalScroll * getVerticalScrollFactorCompat());
final boolean isSourceMouse =
MotionEventCompat.isFromSource(motionEvent, InputDevice.SOURCE_MOUSE);
scrollBy(-invertedDelta, x, ViewCompat.TYPE_NON_TOUCH, isSourceMouse);
if (flingAxis != 0) {
mDifferentialMotionFlingController.onMotionEvent(motionEvent, flingAxis);
}
return true;
}
}
return false;
}
/**
* Returns true if the NestedScrollView supports over scroll.
*/
private boolean canOverScroll() {
final int mode = getOverScrollMode();
return mode == OVER_SCROLL_ALWAYS
|| (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && getScrollRange() > 0);
}
@VisibleForTesting
float getVerticalScrollFactorCompat() {
if (mVerticalScrollFactor == 0) {
TypedValue outValue = new TypedValue();
final Context context = getContext();
if (!context.getTheme().resolveAttribute(
android.R.attr.listPreferredItemHeight, outValue, true)) {
throw new IllegalStateException(
"Expected theme to define listPreferredItemHeight.");
}
mVerticalScrollFactor = outValue.getDimension(
context.getResources().getDisplayMetrics());
}
return mVerticalScrollFactor;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
super.scrollTo(scrollX, scrollY);
}
@SuppressWarnings({"SameParameterValue", "unused"})
boolean overScrollByCompat(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = getOverScrollMode();
final boolean canScrollHorizontal =
computeHorizontalScrollRange() > computeHorizontalScrollExtent();
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS
|| (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS
|| (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}
int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
maxOverScrollY = 0;
}
// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -maxOverScrollY;
final int bottom = maxOverScrollY + scrollRangeY;
boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}
boolean clampedY = false;
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}
if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange());
}
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
return clampedX || clampedY;
}
int getScrollRange() {
int scrollRange = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin;
int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom();
scrollRange = Math.max(0, childSize - parentSpace);
}
return scrollRange;
}
/**
* * Finds the next focusable component that fits in the specified bounds. *
* * @param topFocus look for a candidate is the one at the top of the bounds * if topFocus is true, or at the bottom of the bounds if topFocus is * false * @param top the top offset of the bounds in which a focusable must be * found * @param bottom the bottom offset of the bounds in which a focusable must * be found * @return the next focusable component in the bounds or null if none can * be found */ private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { ListHandles scrolling in response to a "page up/down" shortcut press. This * method will scroll the view by one page up or down and give the focus * to the topmost/bottommost component in the new visible area. If no * component is a good candidate for focus, this scrollview reclaims the * focus.
* * @param direction the scroll direction: {@link View#FOCUS_UP} * to go one page up or * {@link View#FOCUS_DOWN} to go one page down * @return true if the key event is consumed by this method, false otherwise */ public boolean pageScroll(int direction) { boolean down = direction == View.FOCUS_DOWN; int height = getHeight(); if (down) { mTempRect.top = getScrollY() + height; int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); LayoutParams lp = (LayoutParams) view.getLayoutParams(); int bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); if (mTempRect.top + height > bottom) { mTempRect.top = bottom - height; } } } else { mTempRect.top = getScrollY() - height; if (mTempRect.top < 0) { mTempRect.top = 0; } } mTempRect.bottom = mTempRect.top + height; return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); } /** *Handles scrolling in response to a "home/end" shortcut press. This * method will scroll the view to the top or bottom and give the focus * to the topmost/bottommost component in the new visible area. If no * component is a good candidate for focus, this scrollview reclaims the * focus.
* * @param direction the scroll direction: {@link View#FOCUS_UP} * to go the top of the view or * {@link View#FOCUS_DOWN} to go the bottom * @return true if the key event is consumed by this method, false otherwise */ public boolean fullScroll(int direction) { boolean down = direction == View.FOCUS_DOWN; int height = getHeight(); mTempRect.top = 0; mTempRect.bottom = height; if (down) { int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); LayoutParams lp = (LayoutParams) view.getLayoutParams(); mTempRect.bottom = view.getBottom() + lp.bottomMargin + getPaddingBottom(); mTempRect.top = mTempRect.bottom - height; } } return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); } /** *Scrolls the view to make the area defined by top and
* bottom visible. This method attempts to give the focus
* to a component visible in this area. If no component can be focused in
* the new visible area, the focus is reclaimed by this ScrollView.
The scroll range of a scroll view is the overall height of all of its * children.
*/ @Override public int computeVerticalScrollRange() { final int count = getChildCount(); final int parentSpace = getHeight() - getPaddingBottom() - getPaddingTop(); if (count == 0) { return parentSpace; } View child = getChildAt(0); LayoutParams lp = (LayoutParams) child.getLayoutParams(); int scrollRange = child.getBottom() + lp.bottomMargin; final int scrollY = getScrollY(); final int overscrollBottom = Math.max(0, scrollRange - parentSpace); if (scrollY < 0) { scrollRange -= scrollY; } else if (scrollY > overscrollBottom) { scrollRange += scrollY - overscrollBottom; } return scrollRange; } @Override public int computeVerticalScrollOffset() { return Math.max(0, super.computeVerticalScrollOffset()); } @Override public int computeVerticalScrollExtent() { return super.computeVerticalScrollExtent(); } @Override public int computeHorizontalScrollRange() { return super.computeHorizontalScrollRange(); } @Override public int computeHorizontalScrollOffset() { return super.computeHorizontalScrollOffset(); } @Override public int computeHorizontalScrollExtent() { return super.computeHorizontalScrollExtent(); } @Override protected void measureChild(@NonNull View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override public void computeScroll() { if (mScroller.isFinished()) { return; } mScroller.computeScrollOffset(); final int y = mScroller.getCurrY(); int unconsumed = consumeFlingInVerticalStretch(y - mLastScrollerY); mLastScrollerY = y; // Nested Scrolling Pre Pass mScrollConsumed[1] = 0; dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); unconsumed -= mScrollConsumed[1]; final int range = getScrollRange(); if (unconsumed != 0) { // Internal Scroll final int oldScrollY = getScrollY(); overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false); final int scrolledByMe = getScrollY() - oldScrollY; unconsumed -= scrolledByMe; // Nested Scrolling Post Pass mScrollConsumed[1] = 0; dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); unconsumed -= mScrollConsumed[1]; } if (unconsumed != 0) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if (canOverscroll) { if (unconsumed < 0) { if (mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } } else { if (mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); } } } abortAnimatedScroll(); } if (!mScroller.isFinished()) { postInvalidateOnAnimation(); } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } } /** * If either of the vertical edge glows are currently active, this consumes part or all of * deltaY on the edge glow. * * @param deltaY The pointer motion, in pixels, in the vertical direction, positive * for moving down and negative for moving up. * @param x The vertical position of the pointer. * @return The amount ofdeltaY that has been consumed by the
* edge glow.
*/
private int releaseVerticalGlow(int deltaY, float x) {
// First allow releasing existing overscroll effect:
float consumed = 0;
float displacement = x / getWidth();
float pullDistance = (float) deltaY / getHeight();
if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) {
consumed = -EdgeEffectCompat.onPullDistance(mEdgeGlowTop, -pullDistance, displacement);
if (EdgeEffectCompat.getDistance(mEdgeGlowTop) == 0) {
mEdgeGlowTop.onRelease();
}
} else if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) != 0) {
consumed = EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, pullDistance,
1 - displacement);
if (EdgeEffectCompat.getDistance(mEdgeGlowBottom) == 0) {
mEdgeGlowBottom.onRelease();
}
}
int pixelsConsumed = Math.round(consumed * getHeight());
if (pixelsConsumed != 0) {
invalidate();
}
return pixelsConsumed;
}
private void runAnimatedScroll(boolean participateInNestedScrolling) {
if (participateInNestedScrolling) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
} else {
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
mLastScrollerY = getScrollY();
postInvalidateOnAnimation();
}
private void abortAnimatedScroll() {
mScroller.abortAnimation();
stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
}
/**
* Scrolls the view to the given child.
*
* @param child the View to scroll to
*/
private void scrollToChild(View child) {
child.getDrawingRect(mTempRect);
/* Offset from child's local coordinates to ScrollView coordinates */
offsetDescendantRectToMyCoords(child, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
if (scrollDelta != 0) {
scrollBy(0, scrollDelta);
}
}
/**
* If rect is off screen, scroll just enough to get it (or at least the
* first screen size chunk of it) on screen.
*
* @param rect The rectangle.
* @param immediate True to scroll immediately without animation
* @return true if scrolling was performed
*/
private boolean scrollToChildRect(Rect rect, boolean immediate) {
final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
final boolean scroll = delta != 0;
if (scroll) {
if (immediate) {
scrollBy(0, delta);
} else {
smoothScrollBy(0, delta);
}
}
return scroll;
}
/**
* Compute the amount to scroll in the Y direction in order to get
* a rectangle completely on the screen (or, if taller than the screen,
* at least the first screen size chunk of it).
*
* @param rect The rect.
* @return The scroll delta.
*/
protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
if (getChildCount() == 0) return 0;
int height = getHeight();
int screenTop = getScrollY();
int screenBottom = screenTop + height;
int actualScreenBottom = screenBottom;
int fadingEdge = getVerticalFadingEdgeLength();
// TODO: screenTop should be incremented by fadingEdge * getTopFadingEdgeStrength (but for
// the target scroll distance).
// leave room for top fading edge as long as rect isn't at very top
if (rect.top > 0) {
screenTop += fadingEdge;
}
// TODO: screenBottom should be decremented by fadingEdge * getBottomFadingEdgeStrength (but
// for the target scroll distance).
// leave room for bottom fading edge as long as rect isn't at very bottom
View child = getChildAt(0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (rect.bottom < child.getHeight() + lp.topMargin + lp.bottomMargin) {
screenBottom -= fadingEdge;
}
int scrollYDelta = 0;
if (rect.bottom > screenBottom && rect.top > screenTop) {
// need to move down to get it in view: move down just enough so
// that the entire rectangle is in view (or at least the first
// screen size chunk).
if (rect.height() > height) {
// just enough to get screen size chunk on
scrollYDelta += (rect.top - screenTop);
} else {
// get entire rect at bottom of screen
scrollYDelta += (rect.bottom - screenBottom);
}
// make sure we aren't scrolling beyond the end of our content
int bottom = child.getBottom() + lp.bottomMargin;
int distanceToBottom = bottom - actualScreenBottom;
scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
} else if (rect.top < screenTop && rect.bottom < screenBottom) {
// need to move up to get it in view: move up just enough so that
// entire rectangle is in view (or at least the first screen
// size chunk of it).
if (rect.height() > height) {
// screen size chunk
scrollYDelta -= (screenBottom - rect.bottom);
} else {
// entire rect at top
scrollYDelta -= (screenTop - rect.top);
}
// make sure we aren't scrolling any further than the top our content
scrollYDelta = Math.max(scrollYDelta, -getScrollY());
}
return scrollYDelta;
}
@Override
public void requestChildFocus(View child, View focused) {
onRequestChildFocus(child, focused);
super.requestChildFocus(child, focused);
}
protected void onRequestChildFocus(View child, View focused) {
if (!mIsLayoutDirty) {
scrollToChild(focused);
} else {
// The child may not be laid out yet, we can't compute the scroll yet
mChildToScrollTo = focused;
}
}
/**
* When looking for focus in children of a scroll view, need to be a little
* more careful not to give focus to something that is scrolled off screen.
*
* This is more expensive than the default {@link ViewGroup}
* implementation, otherwise this behavior might have been made the default.
*/
@Override
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
// convert from forward / backward notation to up / down / left / right
// (ugh).
if (direction == View.FOCUS_FORWARD) {
direction = View.FOCUS_DOWN;
} else if (direction == View.FOCUS_BACKWARD) {
direction = View.FOCUS_UP;
}
final View nextFocus = previouslyFocusedRect == null
? FocusFinder.getInstance().findNextFocus(this, null, direction)
: FocusFinder.getInstance().findNextFocusFromRect(
this, previouslyFocusedRect, direction);
if (nextFocus == null) {
return false;
}
if (isOffScreen(nextFocus)) {
return false;
}
return nextFocus.requestFocus(direction, previouslyFocusedRect);
}
@Override
public boolean requestChildRectangleOnScreen(@NonNull View child, Rect rectangle,
boolean immediate) {
// offset into coordinate space of this scroll view
rectangle.offset(child.getLeft() - child.getScrollX(),
child.getTop() - child.getScrollY());
return scrollToChildRect(rectangle, immediate);
}
@Override
public void requestLayout() {
mIsLayoutDirty = true;
super.requestLayout();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
if (!mIsLaidOut) {
// If there is a saved state, scroll to the position saved in that state.
if (mSavedState != null) {
scrollTo(getScrollX(), mSavedState.scrollPosition);
mSavedState = null;
} // mScrollY default value is "0"
// Make sure current scrollY position falls into the scroll range. If it doesn't,
// scroll such that it does.
int childSize = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
childSize = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
int parentSpace = b - t - getPaddingTop() - getPaddingBottom();
int currentScrollY = getScrollY();
int newScrollY = clamp(currentScrollY, parentSpace, childSize);
if (newScrollY != currentScrollY) {
scrollTo(getScrollX(), newScrollY);
}
}
// Calling this with the present values causes it to re-claim them
scrollTo(getScrollX(), getScrollY());
mIsLaidOut = true;
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mIsLaidOut = false;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
View currentFocused = findFocus();
if (null == currentFocused || this == currentFocused) {
return;
}
// If the currently-focused view was visible on the screen when the
// screen was at the old height, then scroll the screen to make that
// view visible with the new screen height.
if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
currentFocused.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(currentFocused, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
doScrollY(scrollDelta);
}
}
/**
* Return true if child is a descendant of parent, (or equal to the parent).
*/
private static boolean isViewDescendantOf(View child, View parent) {
if (child == parent) {
return true;
}
final ViewParent theParent = child.getParent();
return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
}
/**
* Fling the scroll view
*
* @param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/cursor is moving down the screen,
* which means we want to scroll towards the top.
*/
public void fling(int velocityY) {
if (getChildCount() > 0) {
mScroller.fling(getScrollX(), getScrollY(), // start
0, velocityY, // velocities
0, 0, // x
Integer.MIN_VALUE, Integer.MAX_VALUE, // y
0, 0); // overscroll
runAnimatedScroll(true);
}
}
/**
* {@inheritDoc}
*
* This version also clamps the scrolling to the bounds of our child.
*/
@Override
public void scrollTo(int x, int y) {
// we rely on the fact the View.scrollBy calls scrollTo.
if (getChildCount() > 0) {
View child = getChildAt(0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int parentSpaceHorizontal = getWidth() - getPaddingLeft() - getPaddingRight();
int childSizeHorizontal = child.getWidth() + lp.leftMargin + lp.rightMargin;
int parentSpaceVertical = getHeight() - getPaddingTop() - getPaddingBottom();
int childSizeVertical = child.getHeight() + lp.topMargin + lp.bottomMargin;
x = clamp(x, parentSpaceHorizontal, childSizeHorizontal);
y = clamp(y, parentSpaceVertical, childSizeVertical);
if (x != getScrollX() || y != getScrollY()) {
super.scrollTo(x, y);
}
}
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
final int scrollY = getScrollY();
if (!mEdgeGlowTop.isFinished()) {
final int restoreCount = canvas.save();
int width = getWidth();
int height = getHeight();
int xTranslation = 0;
int yTranslation = Math.min(0, scrollY);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|| Api21Impl.getClipToPadding(this)) {
width -= getPaddingLeft() + getPaddingRight();
xTranslation += getPaddingLeft();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Api21Impl.getClipToPadding(this)) {
height -= getPaddingTop() + getPaddingBottom();
yTranslation += getPaddingTop();
}
canvas.translate(xTranslation, yTranslation);
mEdgeGlowTop.setSize(width, height);
if (mEdgeGlowTop.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeGlowBottom.isFinished()) {
final int restoreCount = canvas.save();
int width = getWidth();
int height = getHeight();
int xTranslation = 0;
int yTranslation = Math.max(getScrollRange(), scrollY) + height;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|| Api21Impl.getClipToPadding(this)) {
width -= getPaddingLeft() + getPaddingRight();
xTranslation += getPaddingLeft();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Api21Impl.getClipToPadding(this)) {
height -= getPaddingTop() + getPaddingBottom();
yTranslation -= getPaddingBottom();
}
canvas.translate(xTranslation - width, yTranslation);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
private static int clamp(int n, int my, int child) {
if (my >= child || n < 0) {
/* my >= child is this case:
* |--------------- me ---------------|
* |------ child ------|
* or
* |--------------- me ---------------|
* |------ child ------|
* or
* |--------------- me ---------------|
* |------ child ------|
*
* n < 0 is this case:
* |------ me ------|
* |-------- child --------|
* |-- mScrollX --|
*/
return 0;
}
if ((my + n) > child) {
/* this case:
* |------ me ------|
* |------ child ------|
* |-- mScrollX --|
*/
return child - my;
}
return n;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mSavedState = ss;
requestLayout();
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.scrollPosition = getScrollY();
return ss;
}
static class SavedState extends BaseSavedState {
public int scrollPosition;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(Parcel source) {
super(source);
scrollPosition = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(scrollPosition);
}
@NonNull
@Override
public String toString() {
return "HorizontalScrollView.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " scrollPosition=" + scrollPosition + "}";
}
public static final Creator