diff --git a/doric-android/doric/src/main/java/pub/doric/widget/HVScrollView.java b/doric-android/doric/src/main/java/pub/doric/widget/HVScrollView.java index 4bc55026..8c3bdcdb 100644 --- a/doric-android/doric/src/main/java/pub/doric/widget/HVScrollView.java +++ b/doric-android/doric/src/main/java/pub/doric/widget/HVScrollView.java @@ -1,14 +1,15 @@ -/* - * Integration of ScrollView and HorizontalScrollView - * - * For some unknown reason, the H/V scroll bar are missing. - */ - package pub.doric.widget; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; @@ -17,44 +18,92 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; -import android.widget.FrameLayout; -import android.widget.Scroller; +import android.widget.ScrollView; + +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.InputDeviceCompat; +import androidx.core.view.MotionEventCompat; +import androidx.core.view.NestedScrollingChild; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.NestedScrollingParent; +import androidx.core.view.NestedScrollingParentHelper; +import androidx.core.view.ScrollingView; +import androidx.core.view.VelocityTrackerCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityEventCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityRecordCompat; +import androidx.core.widget.EdgeEffectCompat; +import androidx.core.widget.ScrollerCompat; import java.util.List; -/** - * Reference to ScrollView and HorizontalScrollView - */ -public class HVScrollView extends FrameLayout { +public class HVScrollView extends ViewGroup implements NestedScrollingParent, + NestedScrollingChild, ScrollingView { + private boolean DEBUG = false; static final int ANIMATED_SCROLL_GAP = 250; static final float MAX_SCROLL_FACTOR = 0.5f; + private static final String TAG = "NestedScrollView"; + private boolean mChildLayoutCenter; + private int mInitialTouchY; + private int mInitialTouchX; +// private boolean mIsBeingDragged; + + public void setChildLayoutCenter(boolean childLayoutCenter) { + if (mChildLayoutCenter != childLayoutCenter) { + this.mChildLayoutCenter = childLayoutCenter; + requestLayout(); + } + } + + /** + * Interface definition for a callback to be invoked when the scroll + * X or Y positions of a view change. + *
+ * This version of the interface works on all versions of Android, back to API v4. + * + * @see #setOnScrollChangeListener(OnScrollChangeListener) + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(HVScrollView v, int scrollX, int scrollY, + int oldScrollX, int oldScrollY); + } private long mLastScroll; private final Rect mTempRect = new Rect(); - private Scroller mScroller; - - /** - * Flag to indicate that we are moving focus ourselves. This is so the - * code that watches for focus changes initiated outside this ScrollView - * knows that it does not have to do anything. - */ - private boolean mScrollViewMovedFocus; + private ScrollerCompat mScroller; + private EdgeEffectCompat mEdgeGlowTop; + private EdgeEffectCompat mEdgeGlowBottom; + private EdgeEffectCompat mEdgeGlowLeft; + private EdgeEffectCompat mEdgeGlowRight; /** * Position of the last motion event. */ - private float mLastMotionY; - private float mLastMotionX; + private int mLastMotionY; + + private int mLastMotionX; /** * 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 @@ -63,24 +112,38 @@ public class HVScrollView extends FrameLayout { */ 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; + /** - * 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). + * The RecyclerView is not currently scrolling. */ - private boolean mIsBeingDragged = false; + public static final int SCROLL_STATE_IDLE = 0; + + /** + * The RecyclerView is currently being dragged by outside input such as user touch input. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * The RecyclerView is currently animating to a final position while not under + * outside control. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + // Touch/scrolling handling + + private int mScrollState = SCROLL_STATE_IDLE; /** * 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 = true; - /** * Whether arrow scrolling is animated. */ @@ -96,70 +159,228 @@ public class HVScrollView extends FrameLayout { */ 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 mNestedYOffset; + private int mNestedXOffset; + /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; - private boolean mFlingEnabled = true; + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private boolean mFillViewportH; + private boolean mFillViewportV; + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; public HVScrollView(Context context) { this(context, null); } public HVScrollView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public static final int SCROLL_ORIENTATION_NONE = 0; + public static final int SCROLL_ORIENTATION_HORIZONTAL = 1; + public static final int SCROLL_ORIENTATION_VERTICAL = 2; + public static final int SCROLL_ORIENTATION_BOTH = 3; + private int mScrollOrientation; + + public void setScrollOrientation(int scrollOrientation) { + this.mScrollOrientation = scrollOrientation; + requestLayout(); + } + + public int getScrollOrientation() { + return mScrollOrientation; + } + + public HVScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); initScrollView(); + + mChildLayoutCenter = false; + mFillViewportH = true; + mFillViewportV = false; + mScrollOrientation = SCROLL_ORIENTATION_BOTH; + + 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 - protected float getTopFadingEdgeStrength() { - if (getChildCount() == 0) { - return 0.0f; - } + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } - final int length = getVerticalFadingEdgeLength(); - if (getScrollY() < length) { - return getScrollY() / (float) length; - } + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } - return 1.0f; + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @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 dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @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 && canScrollVertically()) || ((nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0 && canScrollHorizontally()); + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + int axes = ViewCompat.SCROLL_AXIS_NONE; + axes = canScrollHorizontally() ? axes | ViewCompat.SCROLL_AXIS_HORIZONTAL : axes; + axes = canScrollVertically() ? axes | ViewCompat.SCROLL_AXIS_VERTICAL : axes; + startNestedScroll(axes); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + public boolean canScrollHorizontally() { + return (mScrollOrientation & SCROLL_ORIENTATION_HORIZONTAL) == SCROLL_ORIENTATION_HORIZONTAL; + } + + public boolean canScrollVertically() { + return (mScrollOrientation & SCROLL_ORIENTATION_VERTICAL) == SCROLL_ORIENTATION_VERTICAL; + } + + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollY = getScrollY(); + final int oldScrollX = getScrollX(); + scrollBy(canScrollHorizontally() ? dxUnconsumed : 0, canScrollVertically() ? dyUnconsumed : 0); + final int myConsumedY = getScrollY() - oldScrollY; + final int myConsumedX = getScrollX() - oldScrollX; + final int myUnconsumedY = dyUnconsumed - myConsumedY; + final int myUnconsumedX = dxUnconsumed - myConsumedX; + 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) { + fling((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 + + public boolean shouldDelayChildPressedState() { + return true; } @Override protected float getLeftFadingEdgeStrength() { - if (getChildCount() == 0) { + if (getChildCount() == 0 || !canScrollHorizontally()) { return 0.0f; } final int length = getHorizontalFadingEdgeLength(); - if (getScrollX() < length) { - return getScrollX() / (float) length; + final int scrollX = getScrollX(); + if (scrollX < length) { + return scrollX / (float) length; } - return 1.0f; } @Override - protected float getRightFadingEdgeStrength() { - if (getChildCount() == 0) { + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0 || !canScrollVertically()) { return 0.0f; } - final int length = getHorizontalFadingEdgeLength(); - final int rightEdge = getWidth() - getPaddingRight(); - final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; - if (span < length) { - return span / (float) length; + 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) { + if (getChildCount() == 0 || !canScrollVertically()) { return 0.0f; } @@ -173,21 +394,36 @@ public class HVScrollView extends FrameLayout { return 1.0f; } + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0 || !canScrollHorizontally()) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollX() - 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 getMaxScrollAmountV() { - return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); + public int getMaxScrollAmountY() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); } - public int getMaxScrollAmountH() { - return (int) (MAX_SCROLL_FACTOR * (getRight() - getLeft())); + public int getMaxScrollAmountX() { + return (int) (MAX_SCROLL_FACTOR * getWidth()); } - private void initScrollView() { - mScroller = new Scroller(getContext()); + mScroller = ScrollerCompat.create(getContext(), null); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); @@ -216,7 +452,7 @@ public class HVScrollView extends FrameLayout { } @Override - public void addView(View child, ViewGroup.LayoutParams params) { + public void addView(View child, LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } @@ -225,7 +461,7 @@ public class HVScrollView extends FrameLayout { } @Override - public void addView(View child, int index, ViewGroup.LayoutParams params) { + public void addView(View child, int index, LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } @@ -233,50 +469,36 @@ public class HVScrollView extends FrameLayout { super.addView(child, index, params); } + /** + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + * This version of the method works on all versions of Android, back to API v4. + * + * @param l The listener to notify when the scroll X or Y position changes. + * @see View#getScrollX() + * @see View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + /** * @return Returns true this ScrollView can be scrolled */ - private boolean canScrollV() { + private boolean canScroll() { View child = getChildAt(0); if (child != null) { int childHeight = child.getHeight(); - return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); - } - return false; - } - - private boolean canScrollH() { - View child = getChildAt(0); - if (child != null) { int childWidth = child.getWidth(); - return getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); + boolean canY = getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + canY |= canScrollVertically(); + boolean canX = getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); + canX |= canScrollHorizontally(); + return canX || canY; } return false; } - /** - * Indicates whether this ScrollView's content is stretched to fill the viewport. - * - * @return True if the content fills the viewport, false otherwise. - */ - public boolean isFillViewport() { - return mFillViewport; - } - - /** - * Indicates this ScrollView whether it 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. - */ - public void setFillViewport(boolean fillViewport) { - if (fillViewport != mFillViewport) { - mFillViewport = fillViewport; - requestLayout(); - } - } - /** * @return Whether arrow scrolling will animate its transition. */ @@ -294,48 +516,11 @@ public class HVScrollView extends FrameLayout { } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); - if (!mFillViewport) { - return; - } - - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - if (heightMode == MeasureSpec.UNSPECIFIED && widthMode == MeasureSpec.UNSPECIFIED) { - return; - } - - if (getChildCount() > 0) { - final View child = getChildAt(0); - int height = getMeasuredHeight(); - int width = getMeasuredWidth(); - width -= getPaddingLeft(); - width -= getPaddingRight(); - height -= getPaddingTop(); - height -= getPaddingBottom(); - int childWidthMeasureSpec; - int childHeightMeasureSpec; - final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); - if (lp.width != ViewGroup.LayoutParams.MATCH_PARENT && lp.height != ViewGroup.LayoutParams.MATCH_PARENT) { - return; - } - if (child.getMeasuredWidth() < width && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - } else { - widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, - getPaddingLeft() + getPaddingRight(), lp.width); - } - if (child.getMeasuredHeight() < height && lp.height == ViewGroup.LayoutParams.MATCH_PARENT) { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - } else { - heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, - getPaddingTop() + getPaddingBottom(), lp.height); - } - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + if (mOnScrollChangeListener != null) { + mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); } } @@ -356,64 +541,149 @@ public class HVScrollView extends FrameLayout { public boolean executeKeyEvent(KeyEvent event) { mTempRect.setEmpty(); - boolean handled = false; + 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_LEFT: - if (canScrollH()) { - if (!event.isAltPressed()) { - handled = arrowScrollH(View.FOCUS_LEFT); - } else { - handled = fullScrollH(View.FOCUS_LEFT); - } - } - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (canScrollH()) { - if (!event.isAltPressed()) { - handled = arrowScrollH(View.FOCUS_RIGHT); - } else { - handled = fullScrollH(View.FOCUS_RIGHT); - } - } - break; case KeyEvent.KEYCODE_DPAD_UP: - if (canScrollV()) { + if (canScrollVertically()) { if (!event.isAltPressed()) { - handled = arrowScrollV(View.FOCUS_UP); + handled = arrowScrollVertically(View.FOCUS_UP); } else { - handled = fullScrollV(View.FOCUS_UP); + handled = fullScrollVertically(View.FOCUS_UP); } } break; case KeyEvent.KEYCODE_DPAD_DOWN: - if (canScrollV()) { + if (canScrollVertically()) { if (!event.isAltPressed()) { - handled = arrowScrollV(View.FOCUS_DOWN); + handled = arrowScrollVertically(View.FOCUS_DOWN); } else { - handled = fullScrollV(View.FOCUS_DOWN); + handled = fullScrollVertically(View.FOCUS_DOWN); } } break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (canScrollHorizontally()) { + if (!event.isAltPressed()) { + handled = arrowScrollHorizontally(View.FOCUS_LEFT); + } else { + handled = fullScrollHorizontally(View.FOCUS_LEFT); + } + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (canScrollHorizontally()) { + if (!event.isAltPressed()) { + handled = arrowScrollHorizontally(View.FOCUS_DOWN); + } else { + handled = fullScrollHorizontally(View.FOCUS_DOWN); + } + } + break; + case KeyEvent.KEYCODE_SPACE: + if (canScrollHorizontally()) { + pageScrollHorizontally(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + } else if (canScrollVertically()) { + pageScrollVertically(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + } + break; } } + return handled; } private boolean inChild(int x, int y) { if (getChildCount() > 0) { - final int scrollX = getScrollX(); final int scrollY = getScrollY(); final View child = getChildAt(0); return !(y < child.getTop() - scrollY || y >= child.getBottom() - scrollY - || x < child.getLeft() - scrollX - || x >= child.getRight() - scrollX); + || x < child.getLeft() + || x >= child.getRight()); } return false; } + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + private void setScrollState(int state) { + if (state == mScrollState) { + return; + } + if (DEBUG) { + Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, + new Exception()); + } + mScrollState = state; + if (state != SCROLL_STATE_SETTLING) { +// stopScrollersInternal(); + } +// dispatchOnScrollStateChanged(state); + } + + private void cancelTouch() { + resetTouch(); + setScrollState(SCROLL_STATE_IDLE); + } + + private void resetTouch() { + recycleVelocityTracker(); + stopNestedScroll(); + releaseGlows(); + } + + private void releaseGlows() { + boolean needsInvalidate = false; + if (mEdgeGlowLeft != null) needsInvalidate = mEdgeGlowLeft.onRelease(); + if (mEdgeGlowTop != null) needsInvalidate |= mEdgeGlowTop.onRelease(); + if (mEdgeGlowRight != null) needsInvalidate |= mEdgeGlowRight.onRelease(); + if (mEdgeGlowBottom != null) needsInvalidate |= mEdgeGlowBottom.onRelease(); + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* @@ -427,12 +697,23 @@ public class HVScrollView extends FrameLayout { * state and he is moving his finger. We want to intercept this * motion. */ + + final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { +// if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { +// return true; +// } + + if (action == MotionEvent.ACTION_MOVE && mScrollState == SCROLL_STATE_DRAGGING) { return true; } - switch (action & MotionEvent.ACTION_MASK) { + setScrollState(SCROLL_STATE_IDLE); + + final boolean canScrollHorizontally = canScrollHorizontally(); + final boolean canScrollVertically = canScrollVertically(); + + switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check @@ -449,54 +730,100 @@ public class HVScrollView extends FrameLayout { break; } - final int pointerIndex = ev.findPointerIndex(activePointerId); - final float y = ev.getY(pointerIndex); - final int yDiff = (int) Math.abs(y - mLastMotionY); - if (yDiff > mTouchSlop) { - mIsBeingDragged = true; - mLastMotionY = y; + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; } - final float x = ev.getX(pointerIndex); - final int xDiff = (int) Math.abs(x - mLastMotionX); - if (xDiff > mTouchSlop) { - mIsBeingDragged = true; - mLastMotionX = x; + + final int y = (int) MotionEventCompat.getY(ev, pointerIndex); + final int x = (int) MotionEventCompat.getX(ev, pointerIndex); + if (mScrollState != SCROLL_STATE_DRAGGING) { + final int dx = x - mInitialTouchX; + final int dy = y - mInitialTouchY; + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + mLastMotionX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1); + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + mLastMotionY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1); + startScroll = true; + } + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + if (startScroll) { +// mIsBeingDragged = true; + setScrollState(SCROLL_STATE_DRAGGING); + } } break; } case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - if (!inChild((int) x, (int) y)) { - mIsBeingDragged = false; + final int y = (int) ev.getY(); + final int x = (int) ev.getX(); + if (!inChild(x, y)) { + setScrollState(SCROLL_STATE_IDLE); + recycleVelocityTracker(); break; } + /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mLastMotionX = x; - mActivePointerId = ev.getPointerId(0); + mInitialTouchY = mLastMotionY; + mInitialTouchX = mLastMotionX; + mActivePointerId = MotionEventCompat.getPointerId(ev, 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. + * otherwise don't. mScroller.isFinished should be false when + * being flinged. We need to call computeScrollOffset() first so that + * isFinished() is correct. */ - mIsBeingDragged = !mScroller.isFinished(); + mScroller.computeScrollOffset(); +// mIsBeingDragged = !mScroller.isFinished(); + + if (mScrollState == SCROLL_STATE_SETTLING) { + getParent().requestDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); break; } case MotionEvent.ACTION_CANCEL: +// mIsBeingDragged = false; + cancelTouch(); + break; case MotionEvent.ACTION_UP: /* Release the drag */ - mIsBeingDragged = false; - mActivePointerId = INVALID_POINTER; +// mIsBeingDragged = false; +// mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); +// if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) { +// ViewCompat.postInvalidateOnAnimation(this); +// } + stopNestedScroll(); break; - case MotionEvent.ACTION_POINTER_UP: + case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } @@ -505,32 +832,29 @@ public class HVScrollView extends FrameLayout { * The only time we want to intercept motion events is if we are in the * drag mode. */ - return mIsBeingDragged; + return false; } @Override public boolean onTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { - // Don't handle edge touches immediately -- they may actually belong to one of our - // descendants. - return false; + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final boolean canScrollHorizontally = canScrollHorizontally(); + final boolean canScrollVertically = canScrollVertically(); + + final int actionMasked = MotionEventCompat.getActionMasked(ev); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + mNestedXOffset = 0; } + vtev.offsetLocation(mNestedXOffset, mNestedYOffset); - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(ev); - - final int action = ev.getAction(); - - switch (action & MotionEvent.ACTION_MASK) { + switch (actionMasked) { case MotionEvent.ACTION_DOWN: { - final float x = ev.getX(); - final float y = ev.getY(); - if (!(mIsBeingDragged = inChild((int) x, (int) y))) { - return false; - } /* * If being flinged and user touches, stop the fling. isFinished @@ -541,89 +865,361 @@ public class HVScrollView extends FrameLayout { } // Remember where the motion event started - mLastMotionY = y; - mLastMotionX = x; - mActivePointerId = ev.getPointerId(0); + mInitialTouchX = mLastMotionY = (int) ev.getY(); + mInitialTouchY = mLastMotionX = (int) ev.getX(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); break; } case MotionEvent.ACTION_MOVE: - if (mIsBeingDragged) { - // Scroll to follow the motion event - final int activePointerIndex = ev.findPointerIndex(mActivePointerId); - final float y = ev.getY(activePointerIndex); - final int deltaY = (int) (mLastMotionY - y); - mLastMotionY = y; - - final float x = ev.getX(activePointerIndex); - final int deltaX = (int) (mLastMotionX - x); - mLastMotionX = x; - - scrollBy(deltaX, deltaY); + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, + mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + return false; } - break; - case MotionEvent.ACTION_UP: - if (mIsBeingDragged) { - if (mFlingEnabled) { - final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialVelocitx = (int) velocityTracker.getXVelocity(mActivePointerId); - int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); - if (getChildCount() > 0) { - if (Math.abs(initialVelocitx) > initialVelocitx || Math.abs(initialVelocity) > mMinimumVelocity) { - fling(-initialVelocitx, -initialVelocity); - } + final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); + final int x = (int) MotionEventCompat.getX(ev, activePointerIndex); + int dy = mLastMotionY - y; + int dx = mLastMotionX - x; + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { + dy -= mScrollConsumed[1]; + dx -= mScrollConsumed[0]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + mNestedXOffset += mScrollOffset[0]; + } + if (mScrollState != SCROLL_STATE_DRAGGING) { + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + if (dx > 0) { + dx -= mTouchSlop; + } else { + dx += mTouchSlop; + } + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop; + } else { + dy += mTouchSlop; + } + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + + if (mScrollState == SCROLL_STATE_DRAGGING) { + // Scroll to follow the motion event + mLastMotionY = y - mScrollOffset[1]; + mLastMotionX = x - mScrollOffset[0]; + + final int oldY = getScrollY(); + final int oldX = getScrollX(); + final int rangeY = getScrollRangeY(); + final int rangeX = getScrollRangeX(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + boolean canOverscrollY = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + rangeY > 0); + boolean canOverscrollX = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + rangeX > 0); + boolean canOverscroll = (canOverscrollY && canScrollVertically()) || (canOverscrollX && canScrollHorizontally()); + +// final ViewParent parent = getParent(); +// if (parent != null) { +// parent.requestDisallowInterceptTouchEvent(true); +// } + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(dx, dy, getScrollX(), getScrollY(), rangeX, rangeY, 0, 0, true)) { + // Break our velocity if we hit a scroll barrier. +// mVelocityTracker.clear(); + if (mChildHelper.hasNestedScrollingParent()) { + getParent().requestDisallowInterceptTouchEvent(true); } } - mActivePointerId = INVALID_POINTER; - mIsBeingDragged = false; - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; + final int scrolledDeltaY = getScrollY() - oldY; + final int scrolledDeltaX = getScrollX() - oldX; + final int unconsumedY = dy - scrolledDeltaY; + final int unconsumedX = dx - scrolledDeltaX; + if (dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + mLastMotionX -= mScrollOffset[0]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + mNestedXOffset += mScrollOffset[0]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToY = oldY + dy; + final int pulledToX = oldX + dx; + if (canScrollVertically()) { + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) dy / getHeight(), MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > rangeY) { + mEdgeGlowBottom.onPull((float) dy / getHeight(), 1.f - MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + } + if (canScrollHorizontally()) { + if (pulledToX < 0) { + mEdgeGlowLeft.onPull((float) dx / getWidth(), MotionEventCompat.getX(ev, activePointerIndex) / getHeight()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } else if (pulledToX > rangeX) { + mEdgeGlowRight.onPull((float) dx / getWidth(), 1.f - MotionEventCompat.getX(ev, activePointerIndex) / getHeight()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.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); + + final float xvel = canScrollHorizontally ? + -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId) : 0; + final float yvel = canScrollVertically ? + -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId) : 0; + + if (fling((int) xvel, (int) yvel)) { + setScrollState(SCROLL_STATE_IDLE); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + mActivePointerId = INVALID_POINTER; + resetTouch(); + break; case MotionEvent.ACTION_CANCEL: - if (mIsBeingDragged && getChildCount() > 0) { - mActivePointerId = INVALID_POINTER; - mIsBeingDragged = false; - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; + if (mScrollState == SCROLL_STATE_DRAGGING) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); } } + mActivePointerId = INVALID_POINTER; + cancelTouch(); break; - case MotionEvent.ACTION_POINTER_UP: + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mInitialTouchX = mLastMotionY = (int) MotionEventCompat.getY(ev, index); + mInitialTouchX = mLastMotionX = (int) MotionEventCompat.getX(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); 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); + final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> + MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = MotionEventCompat.getPointerId(ev, 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; - mLastMotionX = ev.getX(newPointerIndex); - mLastMotionY = ev.getY(newPointerIndex); - mActivePointerId = ev.getPointerId(newPointerIndex); + mInitialTouchX = mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); + mInitialTouchY = mLastMotionX = (int) MotionEventCompat.getX(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } + public boolean onGenericMotionEvent(MotionEvent event) { + if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEventCompat.ACTION_SCROLL: { + final float vscroll = MotionEventCompat.getAxisValue(event, MotionEventCompat.AXIS_VSCROLL); + final float hscroll = MotionEventCompat.getAxisValue(event, MotionEventCompat.AXIS_HSCROLL); + if (vscroll != 0) { + int newScrollY = getScrollY(), oldScrollY = newScrollY; + int newScrollX = getScrollX(), oldScrollX = newScrollX; + if (canScrollHorizontally()) { + final int rangeX = getScrollRangeX(); + final int deltaX = (int) (hscroll * getHorizontalScrollFactorCompat()); + newScrollX = oldScrollX - deltaX; + if (newScrollX < 0) { + newScrollX = 0; + } else if (newScrollX > rangeX) { + newScrollX = rangeX; + } + } + if (canScrollVertically()) { + final int deltaY = (int) (vscroll * getVerticalScrollFactorCompat()); + final int rangeY = getScrollRangeY(); + newScrollY = oldScrollY - deltaY; + if (newScrollY < 0) { + newScrollY = 0; + } else if (newScrollY > rangeY) { + newScrollY = rangeY; + } + } + if (newScrollY != oldScrollY || newScrollX != oldScrollX) { + super.scrollTo(newScrollX, newScrollY); + return true; + } + } + } + } + } + return false; + } + + 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; + } + + private float getHorizontalScrollFactorCompat() { + 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; + } + + 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) { + int oldScrollX = getScrollX(); + int oldScrollY = getScrollY(); + + final int overScrollMode = ViewCompat.getOverScrollMode(this); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent() && canScrollHorizontally(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent() && canScrollVertically(); + final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX; + if (canScrollHorizontally()) { + newScrollX += deltaX; + } + + int newScrollY = scrollY; + if (canScrollVertically()) { + newScrollY += deltaY; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + if (clampedY && clampedX) { + mScroller.springBack(newScrollX, newScrollY, 0, getScrollRangeX(), 0, getScrollRangeY()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return getScrollX() - oldScrollX == deltaX || getScrollY() - oldScrollY == deltaY; + } + + private int getScrollRangeY() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + private int getScrollRangeX() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight())); + } + 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 @@ -635,7 +1231,7 @@ public class HVScrollView extends FrameLayout { * @return the next focusable component in the bounds or null if none can * be found */ - private View findFocusableViewInBoundsV(boolean topFocus, int top, int bottom) { + private View findFocusableViewInBoundsVertically(boolean topFocus, int top, int bottom) { ListHandles scrolling in response to a "home/end" shortcut press. This + * Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus. + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go one page up or + * {@link View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScrollHorizontally(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int width = getWidth(); + + if (down) { + mTempRect.left = getScrollX() + width; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.left + width > view.getRight()) { + mTempRect.left = view.getRight() - width; + } + } + } else { + mTempRect.left = getScrollY() - width; + if (mTempRect.left < 0) { + mTempRect.left = 0; + } + } + mTempRect.right = mTempRect.left + width; + return scrollAndFocusHorizontally(direction, mTempRect.left, mTempRect.right); + } + + public boolean pageScrollVertically(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 scrollAndFocusVertically(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.
+ * focus. * - * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * @param direction the scroll direction: {@link View#FOCUS_UP} * to go the top of the view or - * {@link android.view.View#FOCUS_DOWN} to go the bottom + * {@link View#FOCUS_DOWN} to go the bottom * @return true if the key event is consumed by this method, false otherwise */ - public boolean fullScrollV(int direction) { + public boolean fullScrollVertically(int direction) { boolean down = direction == View.FOCUS_DOWN; int height = getHeight(); @@ -793,47 +1447,84 @@ public class HVScrollView extends FrameLayout { int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); - mTempRect.bottom = view.getBottom(); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); mTempRect.top = mTempRect.bottom - height; } } - return scrollAndFocusV(direction, mTempRect.top, mTempRect.bottom); + return scrollAndFocusVertically(direction, mTempRect.top, mTempRect.bottom); } - public boolean fullScrollH(int direction) { - boolean right = direction == View.FOCUS_RIGHT; + /** + * Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus. + * + * @param direction the scroll direction: {@link View#FOCUS_UP} + * to go the top of the view or + * {@link View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScrollHorizontally(int direction) { + boolean down = direction == View.FOCUS_RIGHT; int width = getWidth(); mTempRect.left = 0; mTempRect.right = width; - if (right) { + if (down) { int count = getChildCount(); if (count > 0) { - View view = getChildAt(0); - mTempRect.right = view.getRight(); + View view = getChildAt(count - 1); + mTempRect.right = view.getRight() + getPaddingLeft(); mTempRect.left = mTempRect.right - width; } } - return scrollAndFocusH(direction, mTempRect.left, mTempRect.right); + return scrollAndFocusHorizontally(direction, mTempRect.left, mTempRect.right); + } + + + private boolean scrollAndFocusHorizontally(int direction, int left, int right) { + boolean handled = true; + + int width = getWidth(); + int containerLeft = getScrollX(); + int containerRight = containerLeft + width; + boolean up = direction == View.FOCUS_LEFT; + + View newFocused = findFocusableViewInBoundsHorizontally(up, left, right); + if (newFocused == null) { + newFocused = this; + } + + if (left >= containerLeft && right <= containerRight) { + handled = false; + } else { + int delta = up ? (left - containerLeft) : (right - containerRight); + doScrollX(delta); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; } /** - *Scrolls the view to make the area defined by top
and
+ * 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.
+ * The scroll range of a scroll view is the overall height of all of its + * children. */ @Override - protected int computeVerticalScrollRange() { + public int computeVerticalScrollRange() { + if (!canScrollVertically()) { + return super.computeVerticalScrollRange(); + } final int count = getChildCount(); final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); if (count == 0) { return contentHeight; } - return getChildAt(0).getBottom(); + 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; } + /** + * + */ @Override - protected int computeHorizontalScrollRange() { + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * + */ + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * + */ + @Override + public int computeHorizontalScrollRange() { + if (!canScrollHorizontally()) { + return super.computeHorizontalScrollRange(); + } final int count = getChildCount(); - final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft(); if (count == 0) { return contentWidth; } - return getChildAt(0).getRight(); + 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; } + /** + * + */ @Override - protected int computeVerticalScrollOffset() { - return Math.max(0, super.computeVerticalScrollOffset()); - } - - @Override - protected int computeHorizontalScrollOffset() { + public int computeHorizontalScrollOffset() { return Math.max(0, super.computeHorizontalScrollOffset()); } + /** + * + */ @Override - protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { - int childWidthMeasureSpec; - int childHeightMeasureSpec; - - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); } @Override - protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, - int parentHeightMeasureSpec, int heightUsed) { + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (getChildCount() == 0) { + return; + } + View child = getChildAt(0); 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); + int childLeft = getPaddingLeft() + lp.leftMargin; + int childTop = getPaddingTop() + lp.topMargin; + if (mChildLayoutCenter) { + if (getMeasuredWidth() > child.getMeasuredWidth()) { + childLeft = (getMeasuredWidth() - child.getMeasuredWidth()) / 2; + } + if (getMeasuredHeight() > child.getMeasuredHeight()) { + childTop = (getMeasuredHeight() - child.getMeasuredHeight()) / 2; + } + } + int measureHeight = child.getMeasuredHeight(); + int measuredWidth = child.getMeasuredWidth(); + child.layout(childLeft, childTop, measuredWidth + childLeft, childTop + measureHeight); + + 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.scrollXPosition, mSavedState.scrollYPosition); + mSavedState = null; + } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRangeY = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0; + final int scrollRangeX = Math.max(0, + childWidth - (b - t - getPaddingRight() - getPaddingLeft())); + + int sY = getScrollY(); + int sX = getScrollX(); + + // Don't forget to clamp + if (getScrollY() > scrollRangeY) { + sY = scrollRangeY; + } else if (getScrollY() < 0) { + sY = 0; + } + if (getScrollX() > scrollRangeX) { + sX = scrollRangeX; + } else if (getScrollX() < 0) { + sX = 0; + } + if (sX != getScrollX() || sY != getScrollY()) { + scrollTo(sX, sY); + } + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public MarginLayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof MarginLayoutParams; + } + + @Override + protected MarginLayoutParams generateLayoutParams(LayoutParams p) { + return new MarginLayoutParams(p); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (getChildCount() == 0) { + return; + } + View child = getChildAt(0); + MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + int widthPadding = lp.leftMargin + lp.rightMargin + getPaddingLeft() + getPaddingRight(); + int heightPadding = lp.topMargin + lp.bottomMargin + getPaddingTop() + getPaddingBottom(); + int maxWidth = widthPadding; + int maxHeight = heightPadding; + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + int childState = 0; + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + if (canScrollVertically()) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED); + } else { + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingBottom() + getPaddingTop() + lp.topMargin + lp.bottomMargin, lp.height); + } + if (canScrollHorizontally()) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED); + } else { + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); + } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + int measuredWidth = ViewCompat.resolveSizeAndState(maxWidth, widthMeasureSpec, childState); + int measuredHeight = ViewCompat.resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT); + setMeasuredDimension(measuredWidth, measuredHeight); + + + boolean needMeasure = false; + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode != MeasureSpec.UNSPECIFIED && mFillViewportV) { + if (child.getMeasuredHeight() < measuredHeight - heightPadding) { + int newChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight - heightPadding, MeasureSpec.EXACTLY); + if (newChildHeightMeasureSpec != childHeightMeasureSpec) { + childHeightMeasureSpec = newChildHeightMeasureSpec; + needMeasure = true; + } + } + } + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + if (widthMode != MeasureSpec.UNSPECIFIED && mFillViewportH) { + if (child.getMeasuredWidth() < measuredWidth - widthPadding) { + int newChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth - widthPadding, MeasureSpec.EXACTLY); + if (newChildWidthMeasureSpec != childWidthMeasureSpec) { + childWidthMeasureSpec = newChildWidthMeasureSpec; + needMeasure = true; + } + } + } + if (needMeasure) { + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { - // This is called at drawing time by ViewGroup. We don't want to - // re-show the scrollbars at this point, which scrollTo will do, - // so we replicate most of scrollTo here. - // - // It's a little odd to call onScrollChanged from inside the drawing. - // - // It is, except when you remember that computeScroll() is used to - // animate scrolling. So unless we want to defer the onScrollChanged() - // until the end of the animated scrolling, we don't really have a - // choice here. - // - // I agree. The alternative, which I think would be worse, is to post - // something and tell the subclasses later. This is bad because there - // will be a window where mScrollX/Y is different from what the app - // thinks it is. - // + int oldX = getScrollX(); + int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - if (getChildCount() > 0) { - View child = getChildAt(0); - x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); - y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); - super.scrollTo(x, y); - } - awakenScrollBars(); + if (oldX != x || oldY != y) { + final int rangeY = getScrollRangeY(); + final int rangeX = getScrollRangeX(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + final boolean canOverscrollY = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && rangeY > 0); + final boolean canOverscrollX = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && rangeX > 0); + boolean canOverscroll = canOverscrollY || canOverscrollX; - // Keep on drawing until the animation has finished. - postInvalidate(); + overScrollByCompat(x - oldX, y - oldY, oldX, oldY, rangeX, rangeY, + 0, 0, false); + + if (canOverscroll) { + ensureGlows(); + if (y <= 0 && oldY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= rangeY && oldY < rangeY) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + + if (x <= 0 && oldX > 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x >= rangeX && oldX < rangeX) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + +// final boolean fullyConsumedVertical = canScrollVertically() +// && x == getScrollX(); +// final boolean fullyConsumedHorizontal = canScrollHorizontally() +// && x == getScrollY(); +// final boolean fullyConsumedAny = fullyConsumedHorizontal +// || fullyConsumedVertical; + + if (mScroller.isFinished()) { + setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this. + } else { + ViewCompat.postInvalidateOnAnimation(this); + } } } @@ -1211,11 +2101,10 @@ public class HVScrollView extends FrameLayout { /* Offset from child's local coordinates to ScrollView coordinates */ offsetDescendantRectToMyCoords(child, mTempRect); - int scrollDeltaV = computeScrollDeltaToGetChildRectOnScreenV(mTempRect); - int scrollDeltaH = computeScrollDeltaToGetChildRectOnScreenH(mTempRect); + int scrollDelta[] = computeScrollDeltaToGetChildRectOnScreen(mTempRect); - if (scrollDeltaH != 0 || scrollDeltaV != 0) { - scrollBy(scrollDeltaH, scrollDeltaV); + if (scrollDelta[0] != 0 || scrollDelta[1] != 0) { + scrollBy(scrollDelta[0], scrollDelta[1]); } } @@ -1228,14 +2117,13 @@ public class HVScrollView extends FrameLayout { * @return true if scrolling was performed */ private boolean scrollToChildRect(Rect rect, boolean immediate) { - final int deltaV = computeScrollDeltaToGetChildRectOnScreenV(rect); - final int deltaH = computeScrollDeltaToGetChildRectOnScreenH(rect); - final boolean scroll = deltaH != 0 || deltaV != 0; + final int delta[] = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta[0] != 0 || delta[1] != 0; if (scroll) { if (immediate) { - scrollBy(deltaH, deltaV); + scrollBy(delta[0], delta[1]); } else { - smoothScrollBy(deltaH, deltaV); + smoothScrollBy(delta[0], delta[1]); } } return scroll; @@ -1249,26 +2137,37 @@ public class HVScrollView extends FrameLayout { * @param rect The rect. * @return The scroll delta. */ - protected int computeScrollDeltaToGetChildRectOnScreenV(Rect rect) { - if (getChildCount() == 0) return 0; + protected int[] computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return new int[]{0, 0}; int height = getHeight(); + int width = getWidth(); int screenTop = getScrollY(); int screenBottom = screenTop + height; + int screenLeft = getScrollX(); + int screenRight = screenLeft + width; - int fadingEdge = getVerticalFadingEdgeLength(); + int fadingEdgeY = getVerticalFadingEdgeLength(); + int fadingEdgeX = getHorizontalFadingEdgeLength(); // leave room for top fading edge as long as rect isn't at very top if (rect.top > 0) { - screenTop += fadingEdge; + screenTop += fadingEdgeY; + } + if (rect.left > 0) { + screenLeft += fadingEdgeX; } // leave room for bottom fading edge as long as rect isn't at very bottom if (rect.bottom < getChildAt(0).getHeight()) { - screenBottom -= fadingEdge; + screenBottom -= fadingEdgeY; + } + if (rect.right < getChildAt(0).getWidth()) { + screenRight -= fadingEdgeX; } int scrollYDelta = 0; + int scrollXDelta = 0; if (rect.bottom > screenBottom && rect.top > screenTop) { // need to move down to get it in view: move down just enough so @@ -1304,32 +2203,8 @@ public class HVScrollView extends FrameLayout { // make sure we aren't scrolling any further than the top our content scrollYDelta = Math.max(scrollYDelta, -getScrollY()); } - return scrollYDelta; - } - - protected int computeScrollDeltaToGetChildRectOnScreenH(Rect rect) { - if (getChildCount() == 0) return 0; - - int width = getWidth(); - int screenLeft = getScrollX(); - int screenRight = screenLeft + width; - - int fadingEdge = getHorizontalFadingEdgeLength(); - - // leave room for left fading edge as long as rect isn't at very left - if (rect.left > 0) { - screenLeft += fadingEdge; - } - - // leave room for right fading edge as long as rect isn't at very right - if (rect.right < getChildAt(0).getWidth()) { - screenRight -= fadingEdge; - } - - int scrollXDelta = 0; - if (rect.right > screenRight && rect.left > screenLeft) { - // need to move right to get it in view: move right just enough so + // 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). @@ -1337,7 +2212,7 @@ public class HVScrollView extends FrameLayout { // just enough to get screen size chunk on scrollXDelta += (rect.left - screenLeft); } else { - // get entire rect at right of screen + // get entire rect at bottom of screen scrollXDelta += (rect.right - screenRight); } @@ -1347,7 +2222,7 @@ public class HVScrollView extends FrameLayout { scrollXDelta = Math.min(scrollXDelta, distanceToRight); } else if (rect.left < screenLeft && rect.right < screenRight) { - // need to move right to get it in view: move right just enough so that + // 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). @@ -1355,25 +2230,23 @@ public class HVScrollView extends FrameLayout { // screen size chunk scrollXDelta -= (screenRight - rect.right); } else { - // entire rect at left + // entire rect at top scrollXDelta -= (screenLeft - rect.left); } - // make sure we aren't scrolling any further than the left our content + // make sure we aren't scrolling any further than the top our content scrollXDelta = Math.max(scrollXDelta, -getScrollX()); } - return scrollXDelta; + return new int[]{scrollXDelta, scrollYDelta}; } @Override public void requestChildFocus(View child, View focused) { - if (!mScrollViewMovedFocus) { - if (!mIsLayoutDirty) { - scrollToChild(focused); - } else { - // The child may not be laid out yet, we can't compute the scroll yet - mChildToScrollTo = 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); } @@ -1383,7 +2256,7 @@ public class HVScrollView extends FrameLayout { * 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} + * This is more expensive than the default {@link ViewGroup} * implementation, otherwise this behavior might have been made the default. */ @Override @@ -1392,12 +2265,11 @@ public class HVScrollView extends FrameLayout { // convert from forward / backward notation to up / down / left / right // (ugh). - // TODO: FUCK -// if (direction == View.FOCUS_FORWARD) { -// direction = View.FOCUS_RIGHT; -// } else if (direction == View.FOCUS_BACKWARD) { -// direction = View.FOCUS_LEFT; -// } + 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) : @@ -1408,9 +2280,15 @@ public class HVScrollView extends FrameLayout { return false; } -// if (isOffScreenH(nextFocus)) { -// return false; -// } + if (canScrollHorizontally()) { + if (isOffScreenX(nextFocus)) { + return false; + } + } else if (canScrollVertically()) { + if (isOffScreenY(nextFocus)) { + return false; + } + } return nextFocus.requestFocus(direction, previouslyFocusedRect); } @@ -1432,17 +2310,10 @@ public class HVScrollView extends FrameLayout { } @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; + public void onAttachedToWindow() { + super.onAttachedToWindow(); - // Calling this with the present values causes it to re-clam them - scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = false; } @Override @@ -1456,26 +2327,18 @@ public class HVScrollView extends FrameLayout { // 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 (isWithinDeltaOfScreenV(currentFocused, 0, oldh)) { + if (isWithinDeltaOfScreenX(currentFocused, 0, oldw) || isWithinDeltaOfScreenY(currentFocused, 0, oldh)) { currentFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(currentFocused, mTempRect); - int scrollDelta = computeScrollDeltaToGetChildRectOnScreenV(mTempRect); - doScrollY(scrollDelta); - } - - final int maxJump = getRight() - getLeft(); - if (isWithinDeltaOfScreenH(currentFocused, maxJump)) { - currentFocused.getDrawingRect(mTempRect); - offsetDescendantRectToMyCoords(currentFocused, mTempRect); - int scrollDelta = computeScrollDeltaToGetChildRectOnScreenH(mTempRect); - doScrollX(scrollDelta); + int scrollDelta[] = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollXY(scrollDelta); } } /** - * Return true if child is an descendant of parent, (or equal to the parent). + * Return true if child is a descendant of parent, (or equal to the parent). */ - private boolean isViewDescendantOf(View child, View parent) { + private static boolean isViewDescendantOf(View child, View parent) { if (child == parent) { return true; } @@ -1484,47 +2347,68 @@ public class HVScrollView extends FrameLayout { return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); } - /** - * Fling the scroll view - * - * @param velocityY The initial velocity in the Y direction. Positive - * numbers mean that the finger/cursor is moving down the screen, - * which means we want to scroll towards the top. - */ - public void fling(int velocityX, int velocityY) { - if (getChildCount() > 0) { - int width = getWidth() - getPaddingRight() - getPaddingLeft(); - int right = getChildAt(0).getWidth(); + private boolean fling(int velocityX, int velocityY) { + final boolean canScrollHorizontal = canScrollHorizontally(); + final boolean canScrollVertical = canScrollVertically(); - int height = getHeight() - getPaddingBottom() - getPaddingTop(); - int bottom = getChildAt(0).getHeight(); - - mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, - 0, Math.max(0, right - width), - 0, Math.max(0, bottom - height)); - -// final boolean movingDown = velocityX > 0 || velocityY > 0; -// -// View newFocused = -// findFocusableViewInMyBoundsV(movingDown, mScroller.getFinalY(), findFocus()); -// if (newFocused == null) { -// newFocused = this; -// } -// -// if (newFocused != findFocus() -// && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) { -// mScrollViewMovedFocus = true; -// mScrollViewMovedFocus = false; -// } - - invalidate(); + if (!canScrollHorizontal || Math.abs(velocityX) < mMinimumVelocity) { + velocityX = 0; } + if (!canScrollVertical || Math.abs(velocityY) < mMinimumVelocity) { + velocityY = 0; + } + if (velocityX == 0 && velocityY == 0) { + // If we don't have any velocity, return false + return false; + } + + if (!dispatchNestedPreFling(velocityX, velocityY)) { + final int scrollY = getScrollY(); + final int scrollX = getScrollX(); + final boolean canFlingY = (scrollY > 0 || velocityY > 0) && + (scrollY < getScrollRangeY() || velocityY < 0); + final boolean canFlingX = (scrollX > 0 || velocityX > 0) && + (scrollX < getScrollRangeX() || velocityX < 0); + boolean canFling = canFlingY || canFlingX; + dispatchNestedFling(velocityX, velocityY, canFling); + if (canFling) { + setScrollState(SCROLL_STATE_SETTLING); + + velocityX = Math.max(-mMaximumVelocity, Math.min(velocityX, mMaximumVelocity)); + velocityY = Math.max(-mMaximumVelocity, Math.min(velocityY, mMaximumVelocity)); + + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int width = getWidth() - getPaddingRight() - getPaddingLeft(); + int bottom = getChildAt(0).getHeight(); + int right = getChildAt(0).getWidth(); + mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, Math.max(0, right - width), 0, + Math.max(0, bottom - height), width / 2, height / 2); + +// ViewCompat.postInvalidateOnAnimation(this); + return true; + } + } + return false; } +// private void endDrag() { +// mIsBeingDragged = false; +// +// recycleVelocityTracker(); +// stopNestedScroll(); +// +// if (mEdgeGlowTop != null) { +// mEdgeGlowTop.onRelease(); +// mEdgeGlowBottom.onRelease(); +// mEdgeGlowLeft.onRelease(); +// mEdgeGlowRight.onRelease(); +// } +// } + /** * {@inheritDoc} - * - *
This version also clamps the scrolling to the bounds of our child. + *
+ * This version also clamps the scrolling to the bounds of our child.
*/
@Override
public void scrollTo(int x, int y) {
@@ -1539,7 +2423,88 @@ public class HVScrollView extends FrameLayout {
}
}
- private int clamp(int n, int my, int child) {
+ private void ensureGlows() {
+ if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
+ if (mEdgeGlowTop == null) {
+ Context context = getContext();
+ mEdgeGlowTop = new EdgeEffectCompat(context);
+ mEdgeGlowBottom = new EdgeEffectCompat(context);
+ mEdgeGlowLeft = new EdgeEffectCompat(context);
+ mEdgeGlowRight = new EdgeEffectCompat(context);
+ }
+ } else {
+ mEdgeGlowTop = null;
+ mEdgeGlowBottom = null;
+ mEdgeGlowLeft = null;
+ mEdgeGlowRight = null;
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(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() + scrollX, Math.min(0, scrollY));
+ mEdgeGlowTop.setSize(width, getHeight());
+ if (mEdgeGlowTop.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() + scrollX,
+ Math.max(getScrollRangeY(), scrollY) + height);
+ canvas.rotate(180, width, 0);
+ mEdgeGlowBottom.setSize(width, height);
+ if (mEdgeGlowBottom.draw(canvas)) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mEdgeGlowLeft.isFinished()) {
+
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(270);
+
+ canvas.translate(-height + getPaddingTop() - scrollY, Math.min(0, scrollX));
+// canvas.rotate(90, 0, 0);
+ mEdgeGlowLeft.setSize(height, width);
+ 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.rotate(90);
+ canvas.translate(-getPaddingTop() + scrollY,
+ -(Math.max(getScrollRangeX(), scrollX) + width));
+ mEdgeGlowRight.setSize(height, width);
+ if (mEdgeGlowRight.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 ---------------|
@@ -1569,11 +2534,186 @@ public class HVScrollView extends FrameLayout {
return n;
}
- public boolean isFlingEnabled() {
- return mFlingEnabled;
+ @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();
}
- public void setFlingEnabled(boolean flingEnabled) {
- this.mFlingEnabled = flingEnabled;
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState);
+ ss.scrollYPosition = getScrollY();
+ ss.scrollXPosition = getScrollX();
+ return ss;
+ }
+
+ static class SavedState extends BaseSavedState {
+ public int scrollYPosition;
+ public int scrollXPosition;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ scrollYPosition = source.readInt();
+ scrollXPosition = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(scrollYPosition);
+ dest.writeInt(scrollXPosition);
+ }
+
+ @Override
+ public String toString() {
+ return "HorizontalScrollView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " scrollXPosition=" + scrollXPosition + " scrollYPosition=" + scrollYPosition + "}";
+ }
+
+ public static final Creator