+ *
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(HVScrollView v, int scrollX, int scrollY, + int oldScrollX, int oldScrollY); + } + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private OverScroller mScroller; + private EdgeEffect mEdgeGlowTop; + private EdgeEffect mEdgeGlowBottom; + private EdgeEffect mEdgeGlowLeft; + private EdgeEffect mEdgeGlowRight; + private int scrollModeFlag = 0; + public static final int DISABLE_VERTICAL_SCROLL = 1; + public static final int DISABLE_HORIZONTAL_SCROLL = 2; + + /** + * Position of the last motion event. + */ + private int mLastMotionX; + 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 his 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. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedXOffset; + private int mNestedYOffset; + + private int mLastScrollerX; + 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 float mHorizontalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; + + public HVScrollView(Context context) { + this(context, null); + } + + public HVScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HVScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + 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); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public boolean startNestedScroll(int axes, int type) { + return mChildHelper.startNestedScroll(axes, type); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public void stopNestedScroll(int type) { + mChildHelper.stopNestedScroll(type); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean hasNestedScrollingParent(int type) { + return mChildHelper.hasNestedScrollingParent(type); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow, type); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, + int type) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); + } + + @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); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 + || (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + scrollBy(dxUnconsumed, dyUnconsumed); + final int myConsumedX = getScrollX() - oldScrollX; + final int myUnconsumedX = dxUnconsumed - myConsumedX; + final int myConsumedY = getScrollY() - oldScrollY; + final int myUnconsumedY = dyUnconsumed - myConsumedY; + dispatchNestedScroll(myConsumedX, myConsumedY, myUnconsumedX, myUnconsumedY, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + dispatchNestedPreScroll(dx, dy, consumed, null); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityX, (int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(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; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int scrollX = getScrollX(); + if (scrollX < length) { + return scrollX / (float) length; + } + + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollY() - rightEdge; + 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(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 android.view.View#getScrollX() + * @see android.view.View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + return canScrollHorizontally() || canScrollVertically(); + } + + private boolean canScrollVertically() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + private boolean canScrollHorizontally() { + View child = getChildAt(0); + if (child != null) { + int childWidth = child.getWidth(); + return getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); + } + 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 widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED && widthMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + width -= getPaddingLeft(); + width -= getPaddingRight(); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (child.getMeasuredWidth() < width) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + } else { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + } + if (child.getMeasuredHeight() < height) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + } else { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom(), lp.height); + } + 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(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 = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + 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 = getScrollY(); + final int scrollX = getScrollX(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() - scrollX + || x >= child.getRight() - scrollX); + } + 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(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); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int x = (int) ev.getX(pointerIndex); + final int y = (int) ev.getY(pointerIndex); + final int xDiff = Math.abs(x - mLastMotionX); + final int yDiff = Math.abs(y - mLastMotionY); + if ((xDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_HORIZONTAL) == 0) + || (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)) { + mIsBeingDragged = true; + mLastMotionX = x; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedXOffset = 0; + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + if (!inChild(x, y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = x; + 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 need to call computeScrollOffset() first so that + * isFinished() is correct. + */ + mScroller.computeScrollOffset(); + mIsBeingDragged = !mScroller.isFinished(); + int axes = getAxes(); + startNestedScroll(axes, 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, getScrollRangeX(), 0, getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + 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(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = ev.getActionMasked(); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedXOffset = 0; + mNestedYOffset = 0; + } + vtev.offsetLocation(mNestedXOffset, mNestedYOffset); + + switch (actionMasked) { + 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(); + } + + // Remember where the motion event started + mLastMotionX = (int) ev.getX(); + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_TOUCH); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int x = (int) ev.getX(activePointerIndex); + final int y = (int) ev.getY(activePointerIndex); + int deltaX = mLastMotionX - x; + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(deltaX, deltaY, mScrollConsumed, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + deltaX -= mScrollConsumed[0]; + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && (Math.abs(deltaX) > mTouchSlop || Math.abs(deltaY) > mTouchSlop)) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaX > 0) { + deltaX -= mTouchSlop; + } else { + deltaX += mTouchSlop; + } + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionX = x - mScrollOffset[0]; + mLastMotionY = y - mScrollOffset[1]; + + final int oldX = getScrollX(); + final int oldY = getScrollY(); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int overscrollMode = getOverScrollMode(); + boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(deltaX, deltaY, getScrollX(), getScrollY(), rangeX, rangeY, 0, + 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaX = getScrollX() - oldX; + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedX = deltaX - scrolledDeltaX; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + mLastMotionX -= mScrollOffset[0]; + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToX = oldX + deltaX; + final int pulledToY = oldY + deltaY; + if (pulledToX < 0) { + EdgeEffectCompat.onPull(mEdgeGlowLeft, (float) deltaX / getWidth(), + ev.getY(activePointerIndex) / getHeight()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > rangeX) { + EdgeEffectCompat.onPull(mEdgeGlowRight, (float) deltaX / getWidth(), + 1.f - ev.getY(activePointerIndex) + / getHeight()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + if (pulledToY < 0) { + EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > rangeY) { + EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() + || !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId); + int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId); + if ((Math.abs(initialVelocityX) > mMinimumVelocity) || (Math.abs(initialVelocityY) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocityX, -initialVelocityY); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mLastMotionX = (int) ev.getX(index); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private int getAxes() { + int axes = ViewCompat.SCROLL_AXIS_NONE; + if ((scrollModeFlag & DISABLE_VERTICAL_SCROLL) != DISABLE_VERTICAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + if ((scrollModeFlag & DISABLE_HORIZONTAL_SCROLL) != DISABLE_HORIZONTAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + return axes; + } + + 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); + mLastMotionX = (int) ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vscroll != 0 || hscroll != 0) { + final int deltaX = (int) (hscroll * getHorizontalScrollFactorCompat()); + final int deltaY = (int) (vscroll * getVerticalScrollFactorCompat()); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + int oldScrollX = getScrollX(); + int oldScrollY = getScrollY(); + int newScrollX = Math.max(0, Math.min(rangeX, oldScrollX - deltaX)); + int newScrollY = Math.max(0, Math.min(rangeY, oldScrollY - deltaY)); + if (newScrollY != oldScrollY || newScrollX != oldScrollX) { + super.scrollTo(newScrollX, newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getHorizontalScrollFactorCompat() { + if (mHorizontalScrollFactor == 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."); + } + mHorizontalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mHorizontalScrollFactor; + } + + private 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); + } + + 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 ((clampedX || clampedY) && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + mScroller.springBack(newScrollX, newScrollY, 0, getScrollRangeX(), 0, getScrollRangeY()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + int getScrollRangeX() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight())); + } + return scrollRange; + } + + int getScrollRangeY() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + 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 android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.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); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - 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 android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.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); + mTempRect.bottom = view.getBottom() + 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.
+ * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollRange() { + final int count = getChildCount(); + final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (count == 0) { + return contentWidth; + } + + int scrollRange = getChildAt(0).getRight(); + final int scrollX = getScrollX(); + final int overscrollRight = Math.max(0, scrollRange - contentWidth); + if (scrollX < 0) { + scrollRange -= scrollX; + } else if (scrollX > overscrollRight) { + scrollRange += scrollX - overscrollRight; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollOffset() { + return super.computeHorizontalScrollOffset(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + 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 = MeasureSpec.makeMeasureSpec( + lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + + int dx = x - mLastScrollerX; + int dy = y - mLastScrollerY; + + // Dispatch up to parent + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + } + + if (dy != 0 || dx != 0) { + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + + overScrollByCompat(dx, dy, oldScrollX, oldScrollY, rangeX, rangeY, 0, 0, false); + + final int scrolledDeltaX = getScrollX() - oldScrollX; + final int scrolledDeltaY = getScrollY() - oldScrollY; + final int unconsumedX = dx - scrolledDeltaX; + final int unconsumedY = dy - scrolledDeltaY; + + if (!dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, null, + ViewCompat.TYPE_NON_TOUCH)) { + final int mode = getOverScrollMode(); + final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS + || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + if (canOverscroll) { + ensureGlows(); + if (x <= 0 && oldScrollX > 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x >= rangeX && oldScrollX < rangeX) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } + if (y <= 0 && oldScrollY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= rangeY && oldScrollY < rangeY) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + + // Finally update the scroll positions and post an invalidation + mLastScrollerX = x; + mLastScrollerY = y; + ViewCompat.postInvalidateOnAnimation(this); + } else { + // We can't scroll any more, so stop any indirect scrolling + if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); + } + // and reset the scroller y + mLastScrollerX = 0; + mLastScrollerY = 0; + } + } + + /** + * 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] != 0 || scrollDelta[1] != 0) { + scrollBy(scrollDelta[0], scrollDelta[1]); + } + } + + /** + * 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] != 0 || delta[1] != 0; + if (scroll) { + if (immediate) { + scrollBy(delta[0], delta[1]); + } else { + smoothScrollBy(delta[0], delta[1]); + } + } + 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 new int[]{0, 0}; + + int width = getWidth(); + int height = getHeight(); + int screenTop = getScrollY(); + int screenLeft = getScrollX(); + int screenRight = screenLeft + width; + int screenBottom = screenTop + height; + + int fadingEdgeX = getHorizontalFadingEdgeLength(); + int fadingEdgeY = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdgeY; + } + if (rect.left > 0) { + screenLeft += fadingEdgeX; + } + if (rect.right < getChildAt(0).getHeight()) { + screenRight -= fadingEdgeX; + } + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdgeY; + } + + int scrollXDelta = 0; + int scrollYDelta = 0; + if (rect.right > screenRight && rect.left > screenLeft) { + // 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.width() > width) { + // just enough to get screen size chunk on + scrollXDelta += (rect.left - screenLeft); + } else { + // get entire rect at bottom of screen + scrollXDelta += (rect.right - screenRight); + } + + // make sure we aren't scrolling beyond the end of our content + int right = getChildAt(0).getRight(); + int distanceToRight = right - screenRight; + scrollXDelta = Math.min(scrollXDelta, distanceToRight); + } else if (rect.left < screenLeft && rect.right < screenRight) { + // 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.width() > width) { + // screen size chunk + scrollXDelta -= (screenRight - rect.right); + } else { + // entire rect at top + scrollXDelta -= (screenLeft - rect.left); + } + + // make sure we aren't scrolling any further than the top our content + scrollXDelta = Math.max(scrollXDelta, -getScrollX()); + } + 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 = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + 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 new int[]{scrollXDelta, scrollYDelta}; + } + + @Override + public void requestChildFocus(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; + } + super.requestChildFocus(child, 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 android.view.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(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 (mSavedState != null) { + scrollTo(mSavedState.scrollPositionX, mSavedState.scrollPositionY); + mSavedState = null; + } // mScrollY default value is "0" + + final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0; + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRangeX = Math.max(0, + childWidth - (b - t - getPaddingLeft() - getPaddingRight())); + final int scrollRangeY = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + int scrollX = Math.max(0, Math.min(scrollRangeX, getScrollX())); + int scrollY = Math.max(0, Math.min(scrollRangeY, getScrollY())); + scrollTo(scrollX, scrollY); + } + + // 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, oldw, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(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 velocityX The initial velocity in the X direction. Positive + * numbers mean that the finger/cursor is moving right to the screen, + * which means we want to scroll towards the left. + * @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 velocityX, int velocityY) { + if (getChildCount() > 0) { + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_NON_TOUCH); + mScroller.fling(getScrollX(), getScrollY(), // start + velocityX, velocityY, // velocities + Integer.MIN_VALUE, Integer.MAX_VALUE, // x + Integer.MIN_VALUE, Integer.MAX_VALUE, // y + 0, 0); // overscroll + mLastScrollerX = getScrollX(); + mLastScrollerY = getScrollY(); + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityX, int velocityY) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + final boolean canFling = ((scrollY > 0 || velocityY > 0) + && (scrollY < getScrollRangeY() || velocityY < 0)) + || ((scrollX > 0 || velocityX > 0) + && (scrollX < getScrollRangeX() || velocityX < 0)); + if (!dispatchNestedPreFling(velocityX, velocityY)) { + dispatchNestedFling(velocityX, velocityY, canFling); + fling(velocityX, velocityY); + } + + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + stopNestedScroll(ViewCompat.TYPE_TOUCH); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } + } + + /** + * {@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);
+ x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
+ y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
+ if (x != getScrollX() || y != getScrollY()) {
+ super.scrollTo(x, y);
+ }
+ }
+ }
+
+ private void ensureGlows() {
+ if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+ if (mEdgeGlowTop == null) {
+ Context context = getContext();
+ mEdgeGlowTop = new EdgeEffect(context);
+ mEdgeGlowBottom = new EdgeEffect(context);
+ mEdgeGlowLeft = new EdgeEffect(context);
+ mEdgeGlowRight = new EdgeEffect(context);
+ }
+ } else {
+ mEdgeGlowTop = null;
+ mEdgeGlowBottom = null;
+ mEdgeGlowLeft = null;
+ mEdgeGlowRight = null;
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mEdgeGlowTop != null) {
+ final int scrollX = getScrollX();
+ final int scrollY = getScrollY();
+ if (!mEdgeGlowTop.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+
+ canvas.translate(getPaddingLeft(), Math.min(0, scrollY));
+ mEdgeGlowTop.setSize(width, getHeight());
+ if (mEdgeGlowTop.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowLeft.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.translate(getPaddingTop(), Math.min(0, scrollX));
+ mEdgeGlowLeft.setSize(getWidth(), height);
+ if (mEdgeGlowLeft.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowRight.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.translate(Math.max(getScrollRangeX(), scrollX) + width, -height + getPaddingTop());
+ canvas.rotate(180, 0, height);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowBottom.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight();
+
+ canvas.translate(-width + getPaddingLeft(),
+ Math.max(getScrollRangeY(), scrollY) + height);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ 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();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.scrollPositionX = getScrollX();
+ ss.scrollPositionY = getScrollY();
+ return ss;
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int scrollPositionX;
+ public int scrollPositionY;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ SavedState(Parcel source) {
+ super(source);
+ scrollPositionX = source.readInt();
+ scrollPositionY = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(scrollPositionX);
+ dest.writeInt(scrollPositionY);
+ }
+
+ @Override
+ public String toString() {
+ return "HorizontalScrollView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " scrollPositionX=" + scrollPositionX
+ + ", scrollPositionY=" + scrollPositionY
+ + "}";
+ }
+
+ public static final Parcelable.Creator