EdgeEffect 提供了一種方式去畫可滑動View組件的過分滑動效果。EdgeEffect的接口很少,只有6個接口。下面咱們用ScrollView源碼來分析一下如何實現過分滑動的暈影效果. java
ScrollView實現暈影效果,其實是經過下面兩個EdgeEffect spring
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。 canvas
@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); } } }
但僅僅於此仍是不行的,怎麼判斷滑動到邊上呢? app
@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. less
計算滑動到底部,其實是計算以前的mScrollY + 移動距離是否大於ScrollView的滑動到底時Child的偏移量 ide
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中作了一些處理。 post
@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; } } }
到此就已經結束了。 this
總結一下: spa
1.對於每個須要畫OverScroll暈影效果的邊,都須要定義本身的EdgeEffect rest
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中調用的。但具體還不是特別清楚,有知道的告訴我一下。