import java.util.List;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.Scroller;
/**
-
Reference to ScrollView and HorizontalScrollView
*/
public class HVScrollView extends FrameLayout {
static final int ANIMATED_SCROLL_GAP = 250;static final float MAX_SCROLL_FACTOR = 0.5f;
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;
/**
- Position of the last motion event.
*/
private float mLastMotionY;
private float 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;
/**
- 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;
/**
- Sentinel value for no current active pointer.
- Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;
private boolean mFlingEnabled = true;
public HVScrollView(Context context) {
this(context, null);
}public HVScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
initScrollView();
}@Override
protected float getTopFadingEdgeStrength() {
if (getChildCount() == 0) {
return 0.0f;
}final int length = getVerticalFadingEdgeLength(); if (getScrollY() < length) { return getScrollY() / (float) length; } return 1.0f;
}
@Override
protected float getLeftFadingEdgeStrength() {
if (getChildCount() == 0) {
return 0.0f;
}final int length = getHorizontalFadingEdgeLength(); if (getScrollX() < length) { return getScrollX() / (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() - getScrollX() - rightEdge; if (span < length) { return span / (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;
}
/**
- @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 getMaxScrollAmountH() {
return (int) (MAX_SCROLL_FACTOR * (getRight() - getLeft()));
}private void initScrollView() {
mScroller = new Scroller(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);
}
/**
- @return Returns true this ScrollView can be scrolled
*/
private boolean canScrollV() {
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() ;
}
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.
*/
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 onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);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(); if (child.getMeasuredHeight() < height || child.getMeasuredWidth() < width) { width -= getPaddingLeft(); width -= getPaddingRight(); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); height -= getPaddingTop(); height -= getPaddingBottom(); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } }
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}/**
- You can call this function yourself to have the scroll view perform
- scrolling from a key event, just as if the event had been dispatched to
- it by the view hierarchy.
- @param event The key event to execute.
-
@return Return true if the event was handled, else false.
*/
public boolean executeKeyEvent(KeyEvent event) {
mTempRect.setEmpty();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 (!event.isAltPressed()) {
handled = arrowScrollV(View.FOCUS_UP);
} else {
handled = fullScrollV(View.FOCUS_UP);
}
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if(canScrollV()){
if (!event.isAltPressed()) {
handled = arrowScrollV(View.FOCUS_DOWN);
} else {
handled = fullScrollV(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);
}
return false;
}@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*- This method JUST determines whether we want to intercept the motion.
- If we return true, onMotionEvent will be called and we do the actual
-
scrolling there.
*//*
- Shortcut the most recurring case: the user is in the dragging
- state and he is moving his finger. We want to intercept this
-
motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*- mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
-
whether the user has moved far enough from his original down touch.
*//*
- Locally do absolute value. mLastMotionY is set to the y value
-
of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}final int pointerIndex = ev.findPointerIndex(activePointerId);
final float y = ev.getY(pointerIndex);
final int yDiff = (int) Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY = y;
}
final float x = ev.getX(pointerIndex);
final int xDiff = (int) Math.abs(x - mLastMotionX);
if (xDiff > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionX = x;
}
break;
}
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
if (!inChild((int)x, (int) y)) {
mIsBeingDragged = false;
break;
}/* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mLastMotionX = x; mActivePointerId = ev.getPointerId(0); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. */ mIsBeingDragged = !mScroller.isFinished(); break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/ Release the drag /
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
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) {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; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { 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 * will be false if being flinged. */ if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started mLastMotionY = y; mLastMotionX = x; mActivePointerId = ev.getPointerId(0); 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); } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { if(mFlingEnabled){ final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocitx = (int) velocityTracker.getXVelocity(); int initialVelocity = (int) velocityTracker.getYVelocity();
// 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); } } } mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } return true;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >>
MotionEvent.ACTION_POINTER_ID_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = ev.getX(newPointerIndex);
mLastMotionY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
/**- <p>
- Finds the next focusable component that fits in the specified bounds.
- </p>
- @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 findFocusableViewInBoundsV(boolean topFocus, int top, int bottom) {List<View> focusables = getFocusables(View.FOCUS_FORWARD);
View focusCandidate = null;/*
- A fully contained focusable is one where its top is below the bound's
- top, and its bottom is above the bound's bottom. A partially
- contained focusable is one where some part of it is within the
- bounds, but it also has some part that is not within bounds. A fully contained
- focusable is preferred to a partially contained focusable.
*/
boolean foundFullyContainedFocusable = false;
int count = focusables.size();
for (int i = 0; i < count; i++) {
View view = focusables.get(i);
int viewTop = view.getTop();
int viewBottom = view.getBottom();if (top < viewBottom && viewTop < bottom) { /* * the focusable is in the target area, it is a candidate for * focusing */ final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); if (focusCandidate == null) { /* No candidate, take this one */ focusCandidate = view; foundFullyContainedFocusable = viewIsFullyContained; } else { final boolean viewIsCloserToBoundary = (topFocus && viewTop < focusCandidate.getTop()) || (!topFocus && viewBottom > focusCandidate .getBottom()); if (foundFullyContainedFocusable) { if (viewIsFullyContained && viewIsCloserToBoundary) { /* * We're dealing with only fully contained views, so * it has to be closer to the boundary to beat our * candidate */ focusCandidate = view; } } else { if (viewIsFullyContained) { /* Any fully contained view beats a partially contained view */ focusCandidate = view; foundFullyContainedFocusable = true; } else if (viewIsCloserToBoundary) { /* * Partially contained view beats another partially * contained view if it's closer */ focusCandidate = view; } } } }
}
return focusCandidate;
}
private View findFocusableViewInBoundsH(boolean leftFocus, int left, int right) {
List<View> focusables = getFocusables(View.FOCUS_FORWARD); View focusCandidate = null; /* * A fully contained focusable is one where its left is below the bound's * left, and its right is above the bound's right. A partially * contained focusable is one where some part of it is within the * bounds, but it also has some part that is not within bounds. A fully contained * focusable is preferred to a partially contained focusable. */ boolean foundFullyContainedFocusable = false; int count = focusables.size(); for (int i = 0; i < count; i++) { View view = focusables.get(i); int viewLeft = view.getLeft(); int viewRight = view.getRight(); if (left < viewRight && viewLeft < right) { /* * the focusable is in the target area, it is a candidate for * focusing */ final boolean viewIsFullyContained = (left < viewLeft) && (viewRight < right); if (focusCandidate == null) { /* No candidate, take this one */ focusCandidate = view; foundFullyContainedFocusable = viewIsFullyContained; } else { final boolean viewIsCloserToBoundary = (leftFocus && viewLeft < focusCandidate.getLeft()) || (!leftFocus && viewRight > focusCandidate.getRight()); if (foundFullyContainedFocusable) { if (viewIsFullyContained && viewIsCloserToBoundary) { /* * We're dealing with only fully contained views, so * it has to be closer to the boundary to beat our * candidate */ focusCandidate = view; } } else { if (viewIsFullyContained) { /* Any fully contained view beats a partially contained view */ focusCandidate = view; foundFullyContainedFocusable = true; } else if (viewIsCloserToBoundary) { /* * Partially contained view beats another partially * contained view if it's closer */ focusCandidate = view; } } } } } return focusCandidate;
}
/**
- <p>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.</p>
- @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 fullScrollV(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();
mTempRect.top = mTempRect.bottom - height;
}
}return scrollAndFocusV(direction, mTempRect.top, mTempRect.bottom);
}
public boolean fullScrollH(int direction) {
boolean right = direction == View.FOCUS_RIGHT;
int width = getWidth();mTempRect.left = 0; mTempRect.right = width; if (right) { int count = getChildCount(); if (count > 0) { View view = getChildAt(0); mTempRect.right = view.getRight(); mTempRect.left = mTempRect.right - width; } } return scrollAndFocusH(direction, mTempRect.left, mTempRect.right);
}
/**
- <p>Scrolls the view to make the area defined by <code>top</code> and
- <code>bottom</code> 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.</p>
- @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
- to go upward
- {@link android.view.View#FOCUS_DOWN} to downward
- @param top the top offset of the new area to be made visible
- @param bottom the bottom offset of the new area to be made visible
-
@return true if the key event is consumed by this method, false otherwise
*/
private boolean scrollAndFocusV(int direction, int top, int bottom) {
boolean handled = true;int height = getHeight();
int containerTop = getScrollY();
int containerBottom = containerTop + height;
boolean up = direction == View.FOCUS_UP;View newFocused = findFocusableViewInBoundsV(up, top, bottom);
if (newFocused == null) {
newFocused = this;
}if (top >= containerTop && bottom <= containerBottom) {
handled = false;
} else {
int delta = up ? (top - containerTop) : (bottom - containerBottom);
doScrollY(delta);
}if (newFocused != findFocus() && newFocused.requestFocus(direction)) {
mScrollViewMovedFocus = true;
mScrollViewMovedFocus = false;
}return handled;
}
private boolean scrollAndFocusH(int direction, int left, int right) {
boolean handled = true;int width = getWidth(); int containerLeft = getScrollX(); int containerRight = containerLeft + width; boolean goLeft = direction == View.FOCUS_LEFT; View newFocused = findFocusableViewInBoundsH(goLeft, left, right); if (newFocused == null) { newFocused = this; } if (left >= containerLeft && right <= containerRight) { handled = false; } else { int delta = goLeft ? (left - containerLeft) : (right - containerRight); doScrollX(delta); } if (newFocused != findFocus() && newFocused.requestFocus(direction)) { mScrollViewMovedFocus = true; mScrollViewMovedFocus = false; } return handled;
}
/**
- Handle scrolling in response to an up or down arrow click.
- @param direction The direction corresponding to the arrow key that was
- pressed
-
@return True if we consumed the event, false otherwise
*/
public boolean arrowScrollV(int direction) {View currentFocused = findFocus();
if (currentFocused == this) currentFocused = null;View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
final int maxJump = getMaxScrollAmountV();
if (nextFocused != null && isWithinDeltaOfScreenV(nextFocused, maxJump, getHeight())) {
nextFocused.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(nextFocused, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreenV(mTempRect);
doScrollY(scrollDelta);
nextFocused.requestFocus(direction);
} else {
// no new focus
int scrollDelta = maxJump;if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { scrollDelta = getScrollY(); } else if (direction == View.FOCUS_DOWN) { if (getChildCount() > 0) { int daBottom = getChildAt(0).getBottom(); int screenBottom = getScrollY() + getHeight(); if (daBottom - screenBottom < maxJump) { scrollDelta = daBottom - screenBottom; } } } if (scrollDelta == 0) { return false; } doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
}
if (currentFocused != null && currentFocused.isFocused()
&& isOffScreenV(currentFocused)) {
// previously focused item still has focus and is off screen, give
// it up (take it back to ourselves)
// (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
// sure to
// get it)
final int descendantFocusability = getDescendantFocusability(); // save
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
requestFocus();
setDescendantFocusability(descendantFocusability); // restore
}
return true;
}
public boolean arrowScrollH(int direction) {
View currentFocused = findFocus(); if (currentFocused == this) currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); final int maxJump = getMaxScrollAmountH(); if (nextFocused != null && isWithinDeltaOfScreenH(nextFocused, maxJump)) { nextFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(nextFocused, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreenH(mTempRect); doScrollX(scrollDelta); nextFocused.requestFocus(direction); } else { // no new focus int scrollDelta = maxJump; if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) { scrollDelta = getScrollX(); } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) { int daRight = getChildAt(0).getRight(); int screenRight = getScrollX() + getWidth(); if (daRight - screenRight < maxJump) { scrollDelta = daRight - screenRight; } } if (scrollDelta == 0) { return false; } doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta); } if (currentFocused != null && currentFocused.isFocused() && isOffScreenH(currentFocused)) { // previously focused item still has focus and is off screen, give // it up (take it back to ourselves) // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are // sure to // get it) final int descendantFocusability = getDescendantFocusability(); // save setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); requestFocus(); setDescendantFocusability(descendantFocusability); // restore } return true;
}
/**- @return whether the descendant of this scroll view is scrolled off
- screen.
*/
private boolean isOffScreenV(View descendant) {
return !isWithinDeltaOfScreenV(descendant, 0, getHeight());
}
private boolean isOffScreenH(View descendant) {
return !isWithinDeltaOfScreenH(descendant, 0);
}/**
- @return whether the descendant of this scroll view is within delta
-
pixels of being on the screen.
*/
private boolean isWithinDeltaOfScreenV(View descendant, int delta, int height) {
descendant.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(descendant, mTempRect);return (mTempRect.bottom + delta) >= getScrollY()
&& (mTempRect.top - delta) <= (getScrollY() + height);
}
private boolean isWithinDeltaOfScreenH(View descendant, int delta) {
descendant.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(descendant, mTempRect);return (mTempRect.right + delta) >= getScrollX() && (mTempRect.left - delta) <= (getScrollX() + getWidth());
}
/**
- Smooth scroll by a Y delta
- @param delta the number of pixels to scroll by on the Y axis
*/
private void doScrollY(int delta) {
if (delta != 0) {
if (mSmoothScrollingEnabled) {
smoothScrollBy(0, delta);
} else {
scrollBy(0, delta);
}
}
}
private void doScrollX(int delta) {
if (delta != 0) {
if (mSmoothScrollingEnabled) {
smoothScrollBy(delta, 0);
} else {
scrollBy(delta, 0);
}
}
}/**
- Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
- @param dx the number of pixels to scroll by on the X axis
-
@param dy the number of pixels to scroll by on the Y axis
*/
public void smoothScrollBy(int dx, int dy) {
if (getChildCount() == 0) {
// Nothing to do.
return;
}
long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
if (duration > ANIMATED_SCROLL_GAP) {
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
final int bottom = getChildAt(0).getHeight();
final int maxY = Math.max(0, bottom - height);
final int scrollY = getScrollY();
dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;final int width = getWidth() - getPaddingRight() - getPaddingLeft(); final int right = getChildAt(0).getWidth(); final int maxX = Math.max(0, right - width); final int scrollX = getScrollX(); dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX; mScroller.startScroll(scrollX, scrollY, dx, dy); invalidate();
} else {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
scrollBy(dx, dy);
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
}
/**
- Like {@link #scrollTo}, but scroll smoothly instead of immediately.
- @param x the position where to scroll on the X axis
- @param y the position where to scroll on the Y axis
*/
public final void smoothScrollTo(int x, int y) {
smoothScrollBy(x - getScrollX(), y - getScrollY());
}
/**
- <p>The scroll range of a scroll view is the overall height of all of its
-
children.</p>
*/
@Override
protected int computeVerticalScrollRange() {
final int count = getChildCount();
final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
if (count == 0) {
return contentHeight;
}return getChildAt(0).getBottom();
}
@Override
protected int computeHorizontalScrollRange() {
final int count = getChildCount();
final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
if (count == 0) {
return contentWidth;
}return getChildAt(0).getRight();
}
@Override
protected int computeVerticalScrollOffset() {
return Math.max(0, super.computeVerticalScrollOffset());
}@Override
protected 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);
}
@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()) {
// 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 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(); // Keep on drawing until the animation has finished. postInvalidate(); }
}
/**
- 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 scrollDeltaV = computeScrollDeltaToGetChildRectOnScreenV(mTempRect);
int scrollDeltaH = computeScrollDeltaToGetChildRectOnScreenH(mTempRect);if (scrollDeltaH != 0 || scrollDeltaV != 0) {
scrollBy(scrollDeltaH, scrollDeltaV);
}
}
/**
- 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 deltaV = computeScrollDeltaToGetChildRectOnScreenV(rect);
final int deltaH = computeScrollDeltaToGetChildRectOnScreenH(rect);
final boolean scroll = deltaH != 0 || deltaV != 0;
if (scroll) {
if (immediate) {
scrollBy(deltaH, deltaV);
} else {
smoothScrollBy(deltaH, deltaV);
}
}
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 computeScrollDeltaToGetChildRectOnScreenV(Rect rect) {
if (getChildCount() == 0) return 0;int height = getHeight();
int screenTop = getScrollY();
int screenBottom = screenTop + height;int fadingEdge = getVerticalFadingEdgeLength();
// leave room for top fading edge as long as rect isn't at very top
if (rect.top > 0) {
screenTop += fadingEdge;
}// leave room for bottom fading edge as long as rect isn't at very bottom
if (rect.bottom < getChildAt(0).getHeight()) {
screenBottom -= fadingEdge;
}int scrollYDelta = 0;
if (rect.bottom > screenBottom && rect.top > screenTop) {
// need to move down to get it in view: move down just enough so
// that the entire rectangle is in view (or at least the first
// screen size chunk).if (rect.height() > height) { // just enough to get screen size chunk on scrollYDelta += (rect.top - screenTop); } else { // get entire rect at bottom of screen scrollYDelta += (rect.bottom - screenBottom); } // make sure we aren't scrolling beyond the end of our content int bottom = 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 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 // 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 right 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 right to get it in view: move right 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 left scrollXDelta -= (screenLeft - rect.left); } // make sure we aren't scrolling any further than the left our content scrollXDelta = Math.max(scrollXDelta, -getScrollX()); } return scrollXDelta;
}
@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;
}
}
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).
// TODO: FUCK
// if (direction == View.FOCUS_FORWARD) {
// direction = View.FOCUS_RIGHT;
// } else if (direction == View.FOCUS_BACKWARD) {
// direction = View.FOCUS_LEFT;
// }final View nextFocus = previouslyFocusedRect == null ?
FocusFinder.getInstance().findNextFocus(this, null, direction) :
FocusFinder.getInstance().findNextFocusFromRect(this,
previouslyFocusedRect, direction);if (nextFocus == null) { return false; } // if (isOffScreenH(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;// Calling this with the present values causes it to re-clam them scrollTo(getScrollX(), getScrollY());
}
@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 (isWithinDeltaOfScreenV(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); }
}
/**
-
Return true if child is an descendant of parent, (or equal to the parent).
*/
private boolean isViewDescendantOf(View child, View parent) {
if (child == parent) {
return true;
}final ViewParent theParent = child.getParent();
return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
}
/**
- Fling the scroll view
- @param velocityY The initial velocity in the Y direction. Positive
- numbers mean that the finger/cursor is moving down the screen,
-
which means we want to scroll towards the top.
*/
public void fling(int velocityX, int velocityY) {
if (getChildCount() > 0) {
int width = getWidth() - getPaddingRight() - getPaddingLeft();
int right = getChildAt(0).getWidth();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();
}
}
/**
- {@inheritDoc}
- <p>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 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;
}
public boolean isFlingEnabled() {
return mFlingEnabled;
}public void setFlingEnabled(boolean flingEnabled) {
this.mFlingEnabled = flingEnabled;
}
}