EdgeEffect 提供了一种方式去画可滑动View组件的过度滑动效果。EdgeEffect的接口不多,只有6个接口。下面我们用ScrollView源码来分析一下如何实现过度滑动的晕影效果.
ScrollView实现晕影效果,实际上是通过下面两个EdgeEffect
private EdgeEffect mEdgeGlowTop; //滑动到顶时,出现的晕影效果
private EdgeEffect mEdgeGlowBottom; //滑动到底时,出现的晕影效果
从ScrollView的代码中可以看到OverScrollMode会对是否有EdgeEffect有影响,当OverScrollMode为OVER_SCROLL_NEVER的时候,是没有EdgeEffect效果的。
@Override
public void setOverScrollMode(int mode) {
if (mode != OVER_SCROLL_NEVER) {//当mode不为OVER_SCROLL_NEVER的时候,创建EdgeEffect实例。
if (mEdgeGlowTop == null) {
Context context = getContext();
mEdgeGlowTop = new EdgeEffect(context); //创建EdgeEffect实例
mEdgeGlowBottom = new EdgeEffect(context);//创建EdgeEffect实例
}
} else {
mEdgeGlowTop = null;
mEdgeGlowBottom = null;
}
super.setOverScrollMode(mode);
}
每次画的时候,每次画的时候都会调用draw。
@Override
public void draw(Canvas canvas) {
super.draw(canvas); //画ScrollView
if (mEdgeGlowTop != null) {
final int scrollY = mScrollY;
if (!mEdgeGlowTop.isFinished()) {//画滑到顶的晕影效果
final int restoreCount = canvas.save();
final int width = getWidth() - mPaddingLeft - mPaddingRight;
canvas.translate(mPaddingLeft, Math.min(0, scrollY));
mEdgeGlowTop.setSize(width, getHeight());
if (mEdgeGlowTop.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeGlowBottom.isFinished()) {//画滑动到底的晕影效果
final int restoreCount = canvas.save();
final int width = getWidth() - mPaddingLeft - mPaddingRight;
final int height = getHeight();
canvas.translate(-width + mPaddingLeft,
Math.max(getScrollRange(), scrollY) + height);
canvas.rotate(180, width, 0);
mEdgeGlowBottom.setSize(width, height);
if (mEdgeGlowBottom.draw(canvas)) {
postInvalidateOnAnimation();
}
canvas.restoreToCount(restoreCount);
}
}
}
但仅仅于此还是不行的,怎么判断滑动到边上呢?
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
if (getChildCount() == 0) {
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y; //sh if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y;
final int oldX = mScrollX;
final int oldY = mScrollY;
final int range = getScrollRange();//ScrollView的高度
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (overScrollBy(0, deltaY, 0, mScrollY,
0, range, 0, mOverscrollDistance, true)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight()); //ScrollView滑动到顶 if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {//ScrollView滑动到底
mEdgeGlowBottom.onPull((float) deltaY / getHeight());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();//使ViewRoot重新去画
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (getChildCount() > 0) {
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
fling(-initialVelocity);
} else {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
getScrollRange())) {
postInvalidateOnAnimation();
}
}
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;//重置EdgeEffect
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();//重置EdgeEffect
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
break;
}
return true;
}
从上面可以看出,ScrollView计算滑动到顶部,实际上是计算之前的mScrollY+移动距离是否大于0.
计算滑动到底部,实际上是计算之前的mScrollY + 移动距离是否大于ScrollView的滑动到底时Child的偏移量
private int getScrollRange() {
int scrollRange = 0;
if (getChildCount() > 0) {
View child = getChildAt(0);//ScrollView仅有一个Child
//计算滑动到底时候Child的偏移量
scrollRange = Math.max(0,
child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
}
return scrollRange;
}
画了个简单的图示,方便理解。
private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
if (mEdgeGlowTop != null) {
mEdgeGlowTop.onRelease(); //重置滑到顶的EdgeEffect
mEdgeGlowBottom.onRelease();//重置滑到底的EdgeEffect
}
if (mScrollStrictSpan != null) {
mScrollStrictSpan.finish();
mScrollStrictSpan = null;
}
}
上面基本已经实现了EdgeEffect效果,以及缓慢拖动的时候,晕影的渐变效果。为了使晕影效果更加平滑,Android在computeScroll中做了一些处理。
@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 = mScrollX;
int oldY = mScrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());//画的时候的吸收效果
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
}
}
if (!awakenScrollBars()) {
// Keep on drawing until the animation has finished.
postInvalidateOnAnimation();
}
} else {
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();
mFlingStrictSpan = null;
}
}
}
到此就已经结束了。
总结一下:
1.对于每一个需要画OverScroll晕影效果的边,都需要定义自己的EdgeEffect
2.在接收到ACTION_MOVE的event时,判断是否已经滑动到边上,如果是就调用EdgeEffect的onPull方法。
如果调用了onPull,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。
3.在收到ACTION_MOVE或者ACITION_CANCEL的时候,调用EdgeEffect的onRelease方法重置。
在调用onRelease方法后,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。
4.重写draw方法,在super.draw(canvas)之后调用EdgeEffect的draw方法。如果EdgeEffect没有finish.做旋转和平移的变换,然后调用EdgeEffect的setSize和draw方法。如果EdgeEffect的draw方法返回ture,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。
5.在失去Window Focus的时候,调用EdgeEffect的finish方法(AbsListView.java)。对于EdgeEffect的onAbsorb方法一般是在computeScroll中调用的。但具体还不是特别清楚,有知道的告诉我一下。
来源:oschina
链接:https://my.oschina.net/u/75011/blog/202820