ScrollView-vs-NestedScrollView
360 removals
605 lines
372 additions
611 lines
Google Git
Google Git
Sign in
Sign in
android / platform / frameworks / base / jb-release / . / core / java / android / widget / ScrollView.java
android / platform / frameworks / support / refs/heads/androidx-main / . / core / core / src / main / java / androidx / core / widget / NestedScrollView.java
blob: ebc54f4527c8165cac4cc387d16b7f6d90bbf0e5 [file] [log] [blame]
blob: 8eebc918c45dd39c7538fa4529b184fb01dfa92e [file] [log] [blame]
/*
/*
* Copyright (C) 2006 The Android Open Source Project
* Copyright (C) 2015 The Android Open Source Project
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* See the License for the specific language governing permissions and
* limitations under the License.
* limitations under the License.
*/
*/
package android.widget;
package androidx.core.widget;
import com.android.internal.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.content.Context;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Rect;
import android.hardware.SensorManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Bundle;
import android.os.StrictMode;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.FocusFinder;
import android.view.FocusFinder;
import android.view.InputDevice;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewConfiguration;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AnimationUtils;
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 java.util.List;
import java.util.List;
/**
/**
* Layout container for a view hierarchy that can be scrolled by the user,
* NestedScrollView is just like {@link ScrollView}, but it supports acting
* allowing it to be larger than the physical display. A ScrollView
* as both a nested scrolling parent and child on both new and old versions of Android.
* is a {@link FrameLayout}, meaning you should place one child in it
* Nested scrolling is enabled by default.
* containing the entire contents to scroll; this child may itself be a layout
* manager with a complex hierarchy of objects. A child that is often used
* is a {@link LinearLayout} in a vertical orientation, presenting a vertical
* array of top-level items that the user can scroll through.
* <p>You should never use a ScrollView with a {@link ListView}, because
* ListView takes care of its own vertical scrolling. Most importantly, doing this
* defeats all of the important optimizations in ListView for dealing with
* large lists, since it effectively forces the ListView to display its entire
* list of items to fill up the infinite container supplied by ScrollView.
* <p>The {@link TextView} class also
* takes care of its own scrolling, so does not require a ScrollView, but
* using the two together is possible to achieve the effect of a text view
* within a larger container.
*
* <p>ScrollView only supports vertical scrolling. For horizontal scrolling,
* use {@link HorizontalScrollView}.
*
* @attr ref android.R.styleable#ScrollView_fillViewport
*/
*/
public class ScrollView extends FrameLayout {
public class NestedScrollView extends FrameLayout implements NestedScrollingParent3,
NestedScrollingChild3, ScrollingView {
static final int ANIMATED_SCROLL_GAP = 250;
static final int ANIMATED_SCROLL_GAP = 250;
static final float MAX_SCROLL_FACTOR = 0.5f;
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.
*
* <p>This version of the interface works on all versions of Android, back to API v4.</p>
*
* @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 long mLastScroll;
private final Rect mTempRect = new Rect();
private final Rect mTempRect = new Rect();
private OverScroller mScroller;
private OverScroller mScroller;
private EdgeEffect mEdgeGlowTop;
@RestrictTo(LIBRARY)
private EdgeEffect mEdgeGlowBottom;
@VisibleForTesting
@NonNull
public EdgeEffect mEdgeGlowTop;
@RestrictTo(LIBRARY)
@VisibleForTesting
@NonNull
public EdgeEffect mEdgeGlowBottom;
/**
/**
* Position of the last motion event.
* 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;
private int mLastMotionY;
/**
/**
* True when the layout has changed but the traversal has not come through yet.
* 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.
* Ideally the view hierarchy would keep track of this for us.
*/
*/
private boolean mIsLayoutDirty = true;
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
* 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
* layout is dirty. This prevents the scroll from being wrong if the child has not been
* laid out before requesting focus.
* laid out before requesting focus.
*/
*/
private View mChildToScrollTo = null;
private View mChildToScrollTo = null;
/**
/**
* True if the user is currently dragging this ScrollView around. This is
* True if the user is currently dragging this ScrollView around. This is
* not the same as 'is being flinged', which can be checked by
* not the same as 'is being flinged', which can be checked by
* mScroller.isFinished() (flinging begins when the user lifts his finger).
* mScroller.isFinished() (flinging begins when the user lifts their finger).
*/
*/
private boolean mIsBeingDragged = false;
private boolean mIsBeingDragged = false;
/**
/**
* Determines speed during touch scrolling
* Determines speed during touch scrolling
*/
*/
private VelocityTracker mVelocityTracker;
private VelocityTracker mVelocityTracker;
/**
/**
* When set to true, the scroll view measure its child to make it fill the currently
* When set to true, the scroll view measure its child to make it fill the currently
* visible area.
* visible area.
*/
*/
@ViewDebug.ExportedProperty(category = "layout")
private boolean mFillViewport;
private boolean mFillViewport;
/**
/**
* Whether arrow scrolling is animated.
* Whether arrow scrolling is animated.
*/
*/
private boolean mSmoothScrollingEnabled = true;
private boolean mSmoothScrollingEnabled = true;
private int mTouchSlop;
private int mTouchSlop;
private int mMinimumVelocity;
private int mMinimumVelocity;
private int mMaximumVelocity;
private int mMaximumVelocity;
private int mOverscrollDistance;
private int mOverflingDistance;
/**
/**
* ID of the active pointer. This is used to retain consistency during
* ID of the active pointer. This is used to retain consistency during
* drags/flings if multiple pointers are used.
* drags/flings if multiple pointers are used.
*/
*/
private int mActivePointerId = INVALID_POINTER;
private int mActivePointerId = INVALID_POINTER;
/**
/**
* The StrictMode "critical time span" objects to catch animation
* Used during scrolling to retrieve the new offset within the window. Saves memory by saving
* stutters. Non-null when a time-sensitive animation is
* x, y changes to this array (0 position = x, 1 position = y) vs. reallocating an x and y
* in-flight. Must call finish() on them when done animating.
* every time.
* These are no-ops on user builds.
*/
*/
private StrictMode.Span mScrollStrictSpan = null; // aka "drag"
private final int[] mScrollOffset = new int[2];
private StrictMode.Span mFlingStrictSpan = null;
/*
* 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.
* Sentinel value for no current active pointer.
* Used by {@link #mActivePointerId}.
* Used by {@link #mActivePointerId}.
*/
*/
private static final int INVALID_POINTER = -1;
private static final int INVALID_POINTER = -1;
public ScrollView(Context context) {
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);
this(context, null);
}
}
public ScrollView(Context context, AttributeSet attrs) {
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
this(context, attrs, R.attr.nestedScrollViewStyle);
}
}
public ScrollView(Context context, AttributeSet attrs, int defStyle) {
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
super(context, attrs, defStyle);
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();
initScrollView();
TypedArray a =
final TypedArray a = context.obtainStyledAttributes(
context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0);
attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0);
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
setFillViewport(a.getBoolean(0, false));
a.recycle();
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;
Text moved from lines 354-356
}
return false;
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
}
@Override
@Override
public int getNestedScrollAxes() {
return mParentHelper.getNestedScrollAxes();
}
// ScrollView import
@Override
public boolean shouldDelayChildPressedState() {
public boolean shouldDelayChildPressedState() {
return true;
return true;
}
}
@Override
@Override
protected float getTopFadingEdgeStrength() {
protected float getTopFadingEdgeStrength() {
if (getChildCount() == 0) {
if (getChildCount() == 0) {
return 0.0f;
return 0.0f;
}
}
final int length = getVerticalFadingEdgeLength();
final int length = getVerticalFadingEdgeLength();
if (mScrollY < length) {
final int scrollY = getScrollY();
return mScrollY / (float) length;
if (scrollY < length) {
return scrollY / (float) length;
}
}
return 1.0f;
return 1.0f;
}
}
@Override
@Override
protected float getBottomFadingEdgeStrength() {
protected float getBottomFadingEdgeStrength() {
if (getChildCount() == 0) {
if (getChildCount() == 0) {
return 0.0f;
return 0.0f;
}
}
View child = getChildAt(0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int length = getVerticalFadingEdgeLength();
final int length = getVerticalFadingEdgeLength();
final int bottomEdge = getHeight() - mPaddingBottom;
final int bottomEdge = getHeight() - getPaddingBottom();
final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
final int span = child.getBottom() + lp.bottomMargin - getScrollY() - bottomEdge;
if (span < length) {
if (span < length) {
return span / (float) length;
return span / (float) length;
}
}
return 1.0f;
return 1.0f;
}
}
/**
/**
* @return The maximum amount this scroll view will scroll in response to
* @return The maximum amount this scroll view will scroll in response to
* an arrow event.
* an arrow event.
*/
*/
public int getMaxScrollAmount() {
public int getMaxScrollAmount() {
return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
return (int) (MAX_SCROLL_FACTOR * getHeight());
}
}
private void initScrollView() {
private void initScrollView() {
mScroller = new OverScroller(getContext());
mScroller = new OverScroller(getContext());
setFocusable(true);
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mOverscrollDistance = configuration.getScaledOverscrollDistance();
mOverflingDistance = configuration.getScaledOverflingDistance();
}
}
@Override
@Override
public void addView(View child) {
public void addView(@NonNull View child) {
if (getChildCount() > 0) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
throw new IllegalStateException("ScrollView can host only one direct child");
}
}
super.addView(child);
super.addView(child);
}
}
@Override
@Override
public void addView(View child, int index) {
public void addView(View child, int index) {
if (getChildCount() > 0) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
throw new IllegalStateException("ScrollView can host only one direct child");
}
}
super.addView(child, index);
super.addView(child, index);
}
}
@Override
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
public void addView(View child, ViewGroup.LayoutParams params) {
if (getChildCount() > 0) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
throw new IllegalStateException("ScrollView can host only one direct child");
}
}
super.addView(child, params);
super.addView(child, params);
}
}
@Override
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (getChildCount() > 0) {
if (getChildCount() > 0) {
throw new IllegalStateException("ScrollView can host only one direct child");
throw new IllegalStateException("ScrollView can host only one direct child");
}
}
super.addView(child, index, params);
super.addView(child, index, params);
}
}
/**
/**
* Register a callback to be invoked when the scroll X or Y positions of
* this view change.
* <p>This version of the method works on all versions of Android, back to API v4.</p>
*
* @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
* @return Returns true this ScrollView can be scrolled
*/
*/
private boolean canScroll() {
private boolean canScroll() {
View child = getChildAt(0);
if (getChildCount() > 0) {
if (child != null) {
View child = getChildAt(0);
int childHeight = child.getHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
int childSize = child.getHeight() + lp.topMargin + lp.bottomMargin;
int parentSpace = getHeight() - getPaddingTop() - getPaddingBottom();
return childSize > parentSpace;
}
}
return false;
return false;
}
}
/**
/**
* Indicates whether this ScrollView's content is stretched to fill the viewport.
* Indicates whether this ScrollView's content is stretched to fill the viewport.
*
*
* @return True if the content fills the viewport, false otherwise.
* @return True if the content fills the viewport, false otherwise.
*
*
* @attr ref android.R.styleable#ScrollView_fillViewport
* @attr name android:fillViewport
*/
*/
public boolean isFillViewport() {
public boolean isFillViewport() {
return mFillViewport;
return mFillViewport;
}
}
/**
/**
* Indicates this ScrollView whether it should stretch its content height to fill
* Set whether this ScrollView should stretch its content height to fill the viewport or not.
* the viewport or not.
*
*
* @param fillViewport True to stretch the content's height to the viewport's
* @param fillViewport True to stretch the content's height to the viewport's
* boundaries, false otherwise.
* boundaries, false otherwise.
*
*
* @attr ref android.R.styleable#ScrollView_fillViewport
* @attr name android:fillViewport
*/
*/
public void setFillViewport(boolean fillViewport) {
public void setFillViewport(boolean fillViewport) {
if (fillViewport != mFillViewport) {
if (fillViewport != mFillViewport) {
mFillViewport = fillViewport;
mFillViewport = fillViewport;
requestLayout();
requestLayout();
}
}
}
}
/**
/**
* @return Whether arrow scrolling will animate its transition.
* @return Whether arrow scrolling will animate its transition.
*/
*/
public boolean isSmoothScrollingEnabled() {
public boolean isSmoothScrollingEnabled() {
return mSmoothScrollingEnabled;
return mSmoothScrollingEnabled;
}
}
/**
/**
* Set whether arrow scrolling will animate its transition.
* Set whether arrow scrolling will animate its transition.
* @param smoothScrollingEnabled whether arrow scrolling will animate its transition
* @param smoothScrollingEnabled whether arrow scrolling will animate its transition
*/
*/
public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
mSmoothScrollingEnabled = smoothScrollingEnabled;
mSmoothScrollingEnabled = smoothScrollingEnabled;
}
}
@Override
@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);
Text moved from lines 373-375
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
if (!mFillViewport) {
return;
return;
}
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
return;
}
}
if (getChildCount() > 0) {
if (getChildCount() > 0) {
final View child = getChildAt(0);
View child = getChildAt(0);
int height = getMeasuredHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (child.getMeasuredHeight() < height) {
int childSize = child.getMeasuredHeight();
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
int parentSpace = getMeasuredHeight()
- getPaddingTop()
- getPaddingBottom()
- lp.topMargin
- lp.bottomMargin;
if (childSize < parentSpace) {
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin,
height -= mPaddingTop;
lp.width);
height -= mPaddingBottom;
int childHeightMeasureSpec =
int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
}
}
}
@Override
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
}
/**
/**
* You can call this function yourself to have the scroll view perform
* 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
* scrolling from a key event, just as if the event had been dispatched to
* it by the view hierarchy.
* it by the view hierarchy.
*
*
* @param event The key event to execute.
* @param event The key event to execute.
* @return Return true if the event was handled, else false.
* @return Return true if the event was handled, else false.
*/
*/
public boolean executeKeyEvent(KeyEvent event) {
public boolean executeKeyEvent(@NonNull KeyEvent event) {
mTempRect.setEmpty();
mTempRect.setEmpty();
if (!canScroll()) {
if (!canScroll()) {
if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
View currentFocused = findFocus();
View currentFocused = findFocus();
if (currentFocused == this) currentFocused = null;
if (currentFocused == this) currentFocused = null;
View nextFocused = FocusFinder.getInstance().findNextFocus(this,
View nextFocused = FocusFinder.getInstance().findNextFocus(this,
currentFocused, View.FOCUS_DOWN);
currentFocused, View.FOCUS_DOWN);
return nextFocused != null
return nextFocused != null
&& nextFocused != this
&& nextFocused != this
&& nextFocused.requestFocus(View.FOCUS_DOWN);
&& nextFocused.requestFocus(View.FOCUS_DOWN);
}
}
return false;
return false;
}
}
boolean handled = false;
boolean handled = false;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_UP:
if (!event.isAltPressed()) {
if (event.isAltPressed()) {
handled = fullScroll(View.FOCUS_UP);
} else {
handled = arrowScroll(View.FOCUS_UP);
handled = arrowScroll(View.FOCUS_UP);
} else {
handled = fullScroll(View.FOCUS_UP);
}
}
break;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
if (!event.isAltPressed()) {
if (event.isAltPressed()) {
handled = fullScroll(View.FOCUS_DOWN);
} else {
handled = arrowScroll(View.FOCUS_DOWN);
handled = arrowScroll(View.FOCUS_DOWN);
} else {
handled = fullScroll(View.FOCUS_DOWN);
}
}
break;
break;
case KeyEvent.KEYCODE_SPACE:
pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
break;
}
}
return handled;
}
private boolean inChild(int x, int y) {
if (getChildCount() > 0) {
final int scrollY = mScrollY;
final View child = getChildAt(0);
return !(y < child.getTop() - scrollY
|| y >= child.getBottom() - scrollY
|| x < child.getLeft()
|| x >= child.getRight());
Text moved to lines 381-383
}
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;
Text moved to lines 536-538
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept) {
recycleVelocityTracker();
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
@Override
public boolean onInterceptTouchEvent(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 he is moving his 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 his 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);
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
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(), (int) y)) {
mIsBeingDragged = false;
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.
*/
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
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(MotionEvent ev) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
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()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
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) {
// Scroll to follow the motion event
mLastMotionY = y;
final int oldX = mScrollX;
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (overScrollBy(0, deltaY, 0, mScrollY,
0, range, 0, mOverscrollDistance, true)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId ==