/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.FocusFinder;
import android.view.KeyEvent;
import android.view.LayoutInflater;
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.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import android.widget.Scroller;
import android.view.View.OnTouchListener; import java.lang.reflect.Field;
import java.util.List; /**
* Layout container for a view hierarchy that can be scrolled by the user,
* allowing it to be larger than the physical display. A ScrollView is a
* {@link FrameLayout}, meaning you should place one child in it containing the
* entire contents to scroll; this child may itself be a layout manager with a
* complex hierarchy of objects. A child that is often used is a
* {@link LinearLayout} in a vertical orientation, presenting a vertical array
* of top-level items that the user can scroll through.
* <p>
* The {@link TextView} class also takes care of its own scrolling, so does not
* require a ScrollView, but using the two together is possible to achieve the
* effect of a text view within a larger container.
* <p>
* ScrollView only supports vertical scrolling.
*/
public class OverScrollView extends FrameLayout implements OnTouchListener
{ static final int ANIMATED_SCROLL_GAP = 250; static final float MAX_SCROLL_FACTOR = 0.5f;
static final float OVERSHOOT_TENSION = 0.75f; private long mLastScroll; private final Rect mTempRect = new Rect();
private Scroller mScroller; protected Context mContext; Field mScrollYField;
Field mScrollXField; boolean hasFailedObtainingScrollFields;
int prevScrollY;
boolean isInFlingMode = false; DisplayMetrics metrics;
LayoutInflater inflater;
protected View child; private Runnable overScrollerSpringbackTask; /**
* 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; /**
* 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 OnScrollChangedListener onScrollChangedListener; public OverScrollView(Context context)
{
this(context, null);
} public OverScrollView(Context context, AttributeSet attrs)
{ this(context, attrs, 0);
} public OverScrollView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
mContext = context; initScrollView();
setFillViewport(true);
initBounce();
} private void initBounce()
{
metrics = this.mContext.getResources().getDisplayMetrics(); // init the bouncy scroller, and make sure the layout is being drawn
// after the top padding
mScroller = new Scroller(getContext(), new OvershootInterpolator(OVERSHOOT_TENSION));
overScrollerSpringbackTask = new Runnable()
{
@Override
public void run()
{
// scroll till after the padding
mScroller.computeScrollOffset();
scrollTo(0, mScroller.getCurrY()); if (!mScroller.isFinished())
{
post(this);
}
}
};
prevScrollY = getPaddingTop(); try
{
mScrollXField = View.class.getDeclaredField("mScrollX");
mScrollYField = View.class.getDeclaredField("mScrollY"); }
catch (Exception e)
{
hasFailedObtainingScrollFields = true;
}
} private void SetScrollY(int value)
{
if (mScrollYField != null)
{
try
{
mScrollYField.setInt(this, value);
}
catch (Exception e)
{
}
}
} private void SetScrollX(int value)
{
if (mScrollXField != null)
{
try
{
mScrollXField.setInt(this, value);
}
catch (Exception e)
{
}
}
} public void initChildPointer()
{
child = getChildAt(0);
child.setPadding(0, 1500, 0, 1500); } @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 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 getMaxScrollAmount()
{
return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop()));
} private void initScrollView()
{
mScroller = new Scroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); setOnTouchListener(this); post(new Runnable()
{
public void run()
{
scrollTo(0, child.getPaddingTop());
}
});
} @Override
public void addView(View child)
{
if (getChildCount() > 0)
{
throw new IllegalStateException("ScrollView can host only one direct child");
} super.addView(child);
initChildPointer();
} @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);
initChildPointer();
} @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);
initChildPointer();
} @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 canScroll()
{
View child = getChildAt(0);
if (child != null)
{
int childHeight = child.getHeight();
return getHeight() < childHeight + getPaddingTop() + getPaddingBottom();
}
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);
if (heightMode == MeasureSpec.UNSPECIFIED)
{
return;
} if (getChildCount() > 0)
{
final View child = getChildAt(0);
int height = getMeasuredHeight();
if (child.getMeasuredHeight() < height)
{
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(),
lp.width);
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(); if (!canScroll())
{
if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK)
{
View currentFocused = findFocus();
if (currentFocused == this)
currentFocused = null;
View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN);
}
return false;
} boolean handled = false;
if (event.getAction() == KeyEvent.ACTION_DOWN)
{
switch (event.getKeyCode())
{
case KeyEvent.KEYCODE_DPAD_UP:
if (!event.isAltPressed())
{
handled = arrowScroll(View.FOCUS_UP);
}
else
{
handled = fullScroll(View.FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (!event.isAltPressed())
{
handled = arrowScroll(View.FOCUS_DOWN);
}
else
{
handled = fullScroll(View.FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_SPACE:
pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
break;
}
} return handled;
} public boolean inChild(int x, int y)
{
if (getChildCount() > 0)
{
final int scrollY = getScrollY();
final View child = getChildAt(0);
return !(y < child.getTop() - scrollY || y >= child.getBottom() - scrollY || x < child.getLeft() || x >= child
.getRight());
}
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;
}
break;
} case MotionEvent.ACTION_DOWN:
{
final float y = ev.getY();
if (!inChild((int) ev.getX(), (int) y))
{
mIsBeingDragged = false;
break;
} /*
* Remember location of down touch. ACTION_DOWN always refers to
* pointer index 0.
*/
mLastMotionY = y;
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 y = ev.getY();
if (!(mIsBeingDragged = inChild((int) ev.getX(), (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;
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; if (isOverScrolled())
{
// when overscrolling, move the scroller just half of the
// finger movement, to make it feel like a spring...
scrollBy(0, deltaY * 2 / 3);
}
else
{
scrollBy(0, deltaY);
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged)
{
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity)
{
fling(-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;
} public boolean isOverScrolled()
{
return (getScrollY() < child.getPaddingTop() || getScrollY() > child.getBottom() - child.getPaddingBottom()
- getHeight());
} private void onSecondaryPointerUp(MotionEvent ev)
{
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId)
{
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
// TODO: Make this decision more intelligent.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
if (mVelocityTracker != null)
{
mVelocityTracker.clear();
}
}
} /**
* <p>
* Finds the next focusable component that fits in this View's bounds
* (excluding fading edges) pretending that this View's top is located at
* the parameter top.
* </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 (the fading edge is assumed to start at this position)
* @param preferredFocusable the View that has highest priority and will be
* returned if it is within my bounds (null is valid)
* @return the next focusable component in the bounds or null if none can be
* found
*/
private View findFocusableViewInMyBounds(final boolean topFocus, final int top, View preferredFocusable)
{
/*
* The fading edge's transparent side should be considered for focus
* since it's mostly visible, so we divide the actual fading edge length
* by 2.
*/
final int fadingEdgeLength = getVerticalFadingEdgeLength() / 2;
final int topWithoutFadingEdge = top + fadingEdgeLength;
final int bottomWithoutFadingEdge = top + getHeight() - fadingEdgeLength; if ((preferredFocusable != null) && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
&& (preferredFocusable.getBottom() > topWithoutFadingEdge))
{
return preferredFocusable;
} return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge);
} /**
* <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 findFocusableViewInBounds(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;
} /**
* <p>
* 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.
* </p>
* @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
* to go one page up or {@link android.view.View#FOCUS_DOWN} to go one page
* down
* @return true if the key event is consumed by this method, false otherwise
*/
public boolean pageScroll(int direction)
{
boolean down = direction == View.FOCUS_DOWN;
int height = getHeight(); if (down)
{
mTempRect.top = getScrollY() + height;
int count = getChildCount();
if (count > 0)
{
View view = getChildAt(count - 1);
if (mTempRect.top + height > view.getBottom())
{
mTempRect.top = view.getBottom() - height;
}
}
}
else
{
mTempRect.top = getScrollY() - height;
if (mTempRect.top < 0)
{
mTempRect.top = 0;
}
}
mTempRect.bottom = mTempRect.top + height; return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
} /**
* <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 fullScroll(int direction)
{
boolean down = direction == View.FOCUS_DOWN;
int height = getHeight(); mTempRect.top = 0;
mTempRect.bottom = height; if (down)
{
int count = getChildCount();
if (count > 0)
{
View view = getChildAt(count - 1);
mTempRect.bottom = view.getBottom();
mTempRect.top = mTempRect.bottom - height;
}
} return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
} /**
* <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 scrollAndFocus(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 = findFocusableViewInBounds(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;
} /**
* 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 arrowScroll(int direction)
{ View currentFocused = findFocus();
if (currentFocused == this)
currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); final int maxJump = getMaxScrollAmount(); if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight()))
{
nextFocused.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(nextFocused, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(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() && isOffScreen(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 isOffScreen(View descendant)
{
return !isWithinDeltaOfScreen(descendant, 0, getHeight());
} /**
* @return whether the descendant of this scroll view is within delta pixels
* of being on the screen.
*/
private boolean isWithinDeltaOfScreen(View descendant, int delta, int height)
{
descendant.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(descendant, mTempRect); return (mTempRect.bottom + delta) >= getScrollY() && (mTempRect.top - delta) <= (getScrollY() + height);
} /**
* 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);
}
}
} /**
* 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 final 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; mScroller.startScroll(getScrollX(), scrollY, 0, dy);
invalidate();
}
else
{
if (!mScroller.isFinished())
{
mScroller.abortAnimation();
}
scrollBy(dx, dy);
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis();
} public final void smoothScrollToTop()
{
smoothScrollTo(0, child.getPaddingTop());
} public final void smoothScrollToBottom()
{
smoothScrollTo(0, child.getHeight() - child.getPaddingTop() - getHeight());
} /**
* 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 computeVerticalScrollOffset()
{
return Math.max(0, super.computeVerticalScrollOffset());
} @Override
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
{
ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthMeasureSpec;
int childHeightMeasureSpec; childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(),
lp.width); 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 = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
+ getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin,
MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} @Override
public void computeScroll()
{
// If android implementation has changed and we cannot obtain mScrollY -
// The default behavior will be applied by the parent.
if (hasFailedObtainingScrollFields)
{
super.computeScroll();
return;
} 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 getScrollX()/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());
if (x != oldX || y != oldY)
{
SetScrollX(x);
// mScrollX = x;
SetScrollY(y);
// mScrollY = y;
onScrollChanged(x, y, oldX, oldY);
}
}
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 scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); if (scrollDelta != 0)
{
scrollBy(0, scrollDelta);
}
} /**
* If rect is off screen, scroll just enough to get it (or at least the
* first screen size chunk of it) on screen.
* @param rect The rectangle.
* @param immediate True to scroll immediately without animation
* @return true if scrolling was performed
*/
private boolean scrollToChildRect(Rect rect, boolean immediate)
{
final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
final boolean scroll = delta != 0;
if (scroll)
{
if (immediate)
{
scrollBy(0, delta);
}
else
{
smoothScrollBy(0, delta);
}
}
return scroll;
} /**
* Compute the amount to scroll in the Y direction in order to get a
* rectangle completely on the screen (or, if taller than the screen, at
* least the first screen size chunk of it).
* @param rect The rect.
* @return The scroll delta.
*/
protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect)
{
if (getChildCount() == 0)
return 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;
} @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).
if (direction == View.FOCUS_FORWARD)
{
direction = View.FOCUS_DOWN;
}
else if (direction == View.FOCUS_BACKWARD)
{
direction = View.FOCUS_UP;
} final View nextFocus = previouslyFocusedRect == null ? FocusFinder.getInstance().findNextFocus(this, null,
direction) : FocusFinder.getInstance().findNextFocusFromRect(this, previouslyFocusedRect, direction); if (nextFocus == null)
{
return false;
} if (isOffScreen(nextFocus))
{
return false;
} return nextFocus.requestFocus(direction, previouslyFocusedRect);
} @Override
public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)
{
// offset into coordinate space of this scroll view
rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); return scrollToChildRect(rectangle, immediate);
} @Override
public void requestLayout()
{
mIsLayoutDirty = true;
super.requestLayout();
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this))
{
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null; // 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 (isWithinDeltaOfScreen(currentFocused, 0, oldh))
{
currentFocused.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(currentFocused, mTempRect);
int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
doScrollY(scrollDelta);
}
} @Override
protected void onScrollChanged(int leftOfVisibleView, int topOfVisibleView, int oldLeftOfVisibleView,
int oldTopOfVisibleView)
{
int displayHeight = getHeight();
int paddingTop = child.getPaddingTop();
int contentBottom = child.getHeight() - child.getPaddingBottom();
if (onScrollChangedListener != null)
{
onScrollChangedListener.onScrollChanged(leftOfVisibleView, topOfVisibleView, oldLeftOfVisibleView,
oldTopOfVisibleView);
}
if (isInFlingMode)
{ if (topOfVisibleView < paddingTop || topOfVisibleView > contentBottom - displayHeight)
{
if (topOfVisibleView < paddingTop)
{
mScroller.startScroll(0, topOfVisibleView, 0, paddingTop - topOfVisibleView, 1000);
}
else if (topOfVisibleView > contentBottom - displayHeight)
{
mScroller.startScroll(0, topOfVisibleView, 0, contentBottom - displayHeight - topOfVisibleView,
1000);
} // Start animation.
post(overScrollerSpringbackTask);
isInFlingMode = false;
return; }
} super.onScrollChanged(leftOfVisibleView, topOfVisibleView, oldLeftOfVisibleView, oldTopOfVisibleView);
} /**
* 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 velocityY)
{
if (getChildCount() > 0)
{
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int bottom = getChildAt(0).getHeight(); mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Math.max(0, bottom - height)); final boolean movingDown = velocityY > 0; View newFocused = findFocusableViewInMyBounds(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)
{
super.scrollTo(x, 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 --------| |-- getScrollX() --|
*/
return 0;
}
if ((my + n) > child)
{
/*
* this case: |------ me ------| |------ child ------| |--
* getScrollX() --|
*/
return child - my;
}
return n;
} @Override
public boolean onTouch(View v, MotionEvent event)
{
// Stop scrolling calculation.
mScroller.forceFinished(true);
// Stop scrolling animation.
removeCallbacks(overScrollerSpringbackTask); if (event.getAction() == MotionEvent.ACTION_UP)
{
return overScrollView();
} else if (event.getAction() == MotionEvent.ACTION_CANCEL)
{
return overScrollView();
} return false;
} private boolean overScrollView()
{ // The height of scroll view, in pixels
int displayHeight = getHeight();
// The top of content view, in pixels.
int contentTop = child.getPaddingTop();
// The top of content view, in pixels.
int contentBottom = child.getHeight() - child.getPaddingBottom();
// The scrolled top position of scroll view, in pixels.
int currScrollY = getScrollY(); int scrollBy; // Scroll to content top
if (currScrollY < contentTop)
{ onOverScroll(currScrollY);
scrollBy = contentTop - currScrollY;
}
else if (currScrollY + displayHeight > contentBottom)
{
// Scroll to content top
if (child.getHeight() - child.getPaddingTop() - child.getPaddingBottom() < displayHeight)
{ scrollBy = contentTop - currScrollY;
}
// Scroll to content bottom
else
{ scrollBy = contentBottom - displayHeight - currScrollY;
// Log.d(Definitions.LOG_TAG, "scrollBy=" + scrollBy);
} // fire onOverScroll event, and update scrollBy if a loadingView has
// been added to the scroller.
scrollBy += onOverScroll(currScrollY);
}
// scrolling between the contentTop and contentBottom
else
{
isInFlingMode = true;
return false;
}
mScroller.startScroll(0, currScrollY, 0, scrollBy, 500); // Start animation.
post(overScrollerSpringbackTask); prevScrollY = currScrollY; // consume(to stop fling)
return true; } protected int onOverScroll(int scrollY)
{
return 0;
} public void reset()
{
if (child != null)
{
scrollTo(0, child.getPaddingTop());
}
setBackgroundColor(Color.TRANSPARENT);
} public int getChildPaddingBotton()
{
if (child != null)
{
return child.getPaddingBottom();
}
return 0;
} public boolean isTop()
{
return getScrollY() < child.getPaddingTop();
}
public boolean isJustTop()
{
return getScrollY() <= child.getPaddingTop();
} public boolean isBottom()
{
return getScrollY() > child.getBottom() - child.getPaddingBottom() - getHeight();
} public void setOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener)
{
this.onScrollChangedListener = onScrollChangedListener;
} public interface OnScrollChangedListener
{
public void onScrollChanged(int x, int y, int oldx, int oldy); public void scrollBottom();
} }

Android--ScrollView边界回弹效果的更多相关文章

  1. Android仿IOS回弹效果 ScrollView回弹 总结

    Android仿IOS回弹效果  ScrollView回弹 总结 应项目中的需求  须要仿IOS 下拉回弹的效果 , 我在网上搜了非常多 大多数都是拿scrollview 改吧改吧 试了一些  发现总 ...

  2. 删除android ScrollView边界阴影方法

    XML文件中添加以下方法:   android:fadingEdge=”none”   或者,代码中设置为false即可   ScrollView.setHorizontalFadingEdgeEna ...

  3. Android--实现ViewPager边界回弹效果(转)

    该View转自   http://blog.csdn.net/Kalwang/article/details/4708721  ,感谢这位大神. public class BounceBackView ...

  4. ScrollView的顶部下拉和底部上拉回弹效果

    要实现ScrollView的回弹效果,需要对其进行触摸事件处理.先来看一下简单的效果: 根据Android的View事件分发处理机制,下面对dispatchTouchEvent进行详细分析: 在加载布 ...

  5. ScrollVIew 边界阴影效果

    一.删除android ScrollView边界阴影方法方法 1) 在xml中添加:android:fadingEdge=”none” 2) 代码中添加:ScrollView.setHorizonta ...

  6. ScrollView的阻尼回弹效果实现(仿qq空间)

    玩过新浪微博,qq空间等手机客户端的童鞋,都应该清楚,在主界面向下滑动时,会有一个阻尼回弹效果,看起来挺不错,接下来我们就来实现一下这种效果,下拉后回弹刷新界面,先看效果图: 这个是编辑器里面的界面效 ...

  7. Scrollview回弹效果自定义控件

    滚动回弹效果分析: 首先,创建一个类,继承scrollview,重写ontouch事件,实现伸缩回弹效果. [scroollview节点下只能有一个子节点,这个子节点就是我们要移动的view布局]   ...

  8. 阻尼回弹效果的ScrollView嵌套GridView

    以前写过一篇带阻尼回弹效果的ScrollView,但是有些小问题,于是又重新整理了一下,这篇文章一是一个带阻尼的Scrollview,再个就是Scrollview嵌套GridView实现,而GridV ...

  9. ‎Cocos2d-x 学习笔记(21.1) ScrollView “甩出”效果与 deaccelerateScrolling 方法

    1. 简介 “甩出”效果是当我们快速拖动container并松开后,container继续朝原方向运动,但是渐渐减速直到停止的效果. ScrollView的onTouchEnded方法会设置Timer ...

随机推荐

  1. pgloader 学习(八) pg 2 pg 简单demo

    pg 数据到pg 数据的迁移,同时支持名称的变更 环境准备 docker-compose文件 内容偏多可以忽略部分 version: "3" services: pgloader- ...

  2. SpringBoot:关于默认连接池Hikari的源码剖析

    1.起因 因为这两天在给公司的一个项目升级SpringBoot版本,遇到了一些坑,升级项目版本:SpringBoot1.5.x到SpringBoot2.0.x 今天早上双库操作遇到一个问题:jdbcU ...

  3. linux性能监控常用命令

    概述 我们在linux下,如果想要监控服务器性能.我们必须掌握以下常用的指标查看命令. ps pstree top free vmstat sar ps ps命令能给出当前系统中进程的快照.下面我们列 ...

  4. 微信小程序全局设置分享内容

    微信小程序每个页面都可以在onShareAppMessage中设置分享内容,如果想要全局设置成一样的分享内容如何设置呢? 在app.js中新增以下方法: //重写分享方法 overShare: fun ...

  5. Attention U-Net: Learning Where to Look for the Pancreas

    Attention U-Net: Learning Where to Look for the Pancreas 2019-09-10 09:50:43 Paper: https://arxiv.or ...

  6. 搭建高可用rabbitmq集群及spring boot实现集群配置

    java spring boot配置: //具体参看了配置的源码 org.springframework.boot.autoconfigure.amqp.RabbitProperties //Rabb ...

  7. git远程版本回退

    本文为博主原创,未经允许不得转载: 之前在git提交版本时,发现将新开发的代码提交到了另一个分支上,为了不影响提交分支代码的 功能,需要回退到之前的版本. 在使用命令回退的时候,一直没有回退成功,有个 ...

  8. tf.image.crop_and_resize

    https://blog.csdn.net/m0_38024332/article/details/81779544 将图片剪切下来,池化为固定大小.可以快速的对proposal进行池化

  9. Jmeter里http接口的执行顺序是顺序执行

    1,如果在一个线程组里则是顺序执行 2,如果不在一个线程组里,就勾选独立运行各个线程组,在一个运行结束后启动下一个线程组

  10. PAT 甲级 1080 Graduate Admission (30 分) (简单,结构体排序模拟)

    1080 Graduate Admission (30 分)   It is said that in 2011, there are about 100 graduate schools ready ...