Comparing sensitive data, confidential files or internal emails?

Most legal and privacy policies prohibit uploading sensitive data online. Diffchecker Desktop ensures your confidential information never leaves your computer. Work offline and compare documents securely.

ScrollView-vs-NestedScrollView

Created Diff never expires
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 ==