左右滑動的控件我們使用的也是非常多了,但是基本上都是使用的viewpager 等 android基礎的控件,那么我們有么有考慮過查看他的源碼進行定制呢?當然,如果你自我感覺非常好的話可以自己定制一個,osc的ScrollLayout就是自己定義的View 和Viewpager的區別還是不小的
代碼不是很多不到300行,但是卻實現了左右滑動頁面的效果,還是值得學習的.效果如下:
我們看到ScrollLayout直接繼承了ViewGroup然后自定義了一系列功能,那么接下來就分析一下:
我們知道ViewGroup的繪制流程基本分為onMeasure ,onLayout ,onDraw三部分
那么就首先看onMeasure
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //Log.e(TAG, "onMeasure"); super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException( "ScrollLayout only canmCurScreen run at EXACTLY mode!"); } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { throw new IllegalStateException( "ScrollLayout only can run at EXACTLY mode!"); } // The children are given the same width and height as the scrollLayout final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); } // Log.e(TAG, "moving to screen "+mCurScreen); scrollTo(mCurScreen * width, 0); }
widthMode != MeasureSpec.EXACTLY
那么scrollTo的作用是什么呢?
其實我們可以把android View 認為是一個桌布,屏幕的左上角是 0,0 scrollTo 就是把這個view移動到某個位置.
如圖來說明 0,0 表示屏幕的左上角 view調用了view.scrollTo(2,3)后就可以跳轉到這個位置了~
至於我們的viewpager是如何工作的我們在看完onLayout后再說~
這一句話其實是檢查是否width是"絕對大小" 其實也就是檢查是否是確定的像素 如100dp或者 match_parent
如果是wrap_content就拋異常了.
然后就是把這個layout的孩子的寬高都和他自己一樣:
final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); }
最后是scrollTo(mCurScreen * width, 0); 滾動的當前的屏幕page中去.
然后重寫了onLayout來layout 子View
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } }
代碼其實也很簡單 就是把他的孩子橫向排開, 寬度是前面measure獲取的 然后調用父類的dispatchDraw 和 onDraw把他們畫出來這些都不表了
接上面,那么這個pager是怎么像我們看到的那樣可以左右滑動呢?
其實在layout的時候 這個控件會把他的孩子一字排開,如下圖的紅色方框所示.
我們知道,這個控件只有一個屏幕大小,那么他就會使用scrollTo 左右移動,如下圖藍色的部分,那么我們可以看到左右滑動的效果了.
當然 這樣其實只是實現"計算機"意義的滾動,因為這個滾動只用手機才能知道用戶看上去只不過是其中一個屏幕而已,從一個屏幕跳轉到另一個屏幕也沒有什么過渡動畫,這就想osc客戶端關閉的左右滑動一樣.雖然這個app確實是在左右滑動把各個孩子屏幕顯示給用戶,但是用戶只能看到當前的屏幕而已
那么怎么讓用戶有看到左右滑動時候一個屏幕進入另一個屏幕退出的效果呢?
public void snapToScreen(int whichScreen) { //是否可滑動 if(!isScroll) { this.setToScreen(whichScreen); return; } scrollToScreen(whichScreen); } public void scrollToScreen(int whichScreen) { // get the valid layout page whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); if (getScrollX() != (whichScreen * getWidth())) { final int delta = whichScreen * getWidth() - getScrollX(); mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 1);//持續滾動時間 以毫秒為單位 mCurScreen = whichScreen; invalidate(); // Redraw the layout if (mOnViewChangeListener != null) { mOnViewChangeListener.OnViewChange(mCurScreen); } } }
不可滑動的我們就不看了,其實就是個scrollTo 着重看可以滑動界面的實現,也就是scrollToScreen
我們知道,如果想讓一個空間滑動,本質上其實是改變這個控件的坐標,然后不斷的刷新屏幕,這樣很多幀和在一起連續播放用戶就可以感覺這個屏幕是在滾動了:
為了實現滾動這里用到了Scroller. Scroller可以認為是一個存儲屏幕參數的容器,View需要做動畫的時候就從Scroller中取出已經計算好坐標, 使用這個坐標不斷的刷新屏幕,view的位置就不斷變化了.
代碼實現如下:
public void scrollToScreen(int whichScreen) { // get the valid layout page whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); if (getScrollX() != (whichScreen * getWidth())) { final int delta = whichScreen * getWidth() - getScrollX(); mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 1);//持續滾動時間 以毫秒為單位 mCurScreen = whichScreen; invalidate(); // Redraw the layout if (mOnViewChangeListener != null) { mOnViewChangeListener.OnViewChange(mCurScreen); } } }
核心代碼是startScroll()函數 這個函數是android源碼中的函數,具體作用其實是改變一些數值,他有五個參數
從(startx,starty) 到 (dx ,dy) 最后一個參數是在多少時間內完成這個操作 這個函數只是在這一段時間中計算移動到的坐標,並不會改變view的位置,view的位置一定是由draw來做的.
public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
除了移動位置 ,還需要知道是否移動結束了,如果結束了就不要再刷新屏幕了 這個是通過Scroller的computeScrollOffset 函數實現的,如果移動沒有結束就返回true否則返回false
這樣完事具備就剩下刷新屏幕了~ 在scrollToScreen函數中一定調用了 invalidate()函數告訴View重新進行繪制.在繪制的過程中,其父View會調用Scrolllayout實現的computeScroll函數來真正的移動view的坐標這個是通過scrollTo函數實現的,而坐標就是從scroller中取到的.ok 上面圖中的藍色方框終於開始移動了,移動了一段距離后就執行postInvalidate()函數,我們知道,postInvallidate函數是 異步進行刷新 ,最后還是執行invalidate()函數,invalidate()又開始調用computeScroll ...這個死循環在mScroller.computeScrollOffset()為false的時候才會結束,這樣動畫也就執行完了,那他就滑動到下一屏了~
@Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { //Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop); final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { return true; } final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: final int xDiff = (int) Math.abs(mLastMotionX - x); if (xDiff > mTouchSlop) { mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_DOWN: mLastMotionX = x; mLastMotionY = y; mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_REST; break; } return mTouchState != TOUCH_STATE_REST; }
@Override public boolean onTouchEvent(MotionEvent event) { //是否可滑動 if(!isScroll) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); final int action = event.getAction(); final float x = event.getX(); final float y = event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: //Log.e(TAG, "event down!"); if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mLastMotionX = x; //---------------New Code---------------------- mLastMotionY = y; //--------------------------------------------- break; case MotionEvent.ACTION_MOVE: int deltaX = (int) (mLastMotionX - x); //---------------New Code---------------------- int deltaY = (int) (mLastMotionY - y); if(Math.abs(deltaX) < 200 && Math.abs(deltaY) > 10) break; mLastMotionY = y; //------------------------------------- mLastMotionX = x; scrollBy(deltaX, 0); break; case MotionEvent.ACTION_UP: //Log.e(TAG, "event : up"); // if (mTouchState == TOUCH_STATE_SCROLLING) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); int velocityX = (int) velocityTracker.getXVelocity(); //Log.e(TAG, "velocityX:" + velocityX); if (velocityX > SNAP_VELOCITY && mCurScreen > 0) { // Fling enough to move left //Log.e(TAG, "snap left"); snapToScreen(mCurScreen - 1); } else if (velocityX < -SNAP_VELOCITY && mCurScreen < getChildCount() - 1) { // Fling enough to move right //Log.e(TAG, "snap right"); snapToScreen(mCurScreen + 1); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } // } mTouchState = TOUCH_STATE_REST; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; break; } return true; }
int deltaX = (int) (mLastMotionX - x); //---------------New Code---------------------- int deltaY = (int) (mLastMotionY - y); if(Math.abs(deltaX) < 200 && Math.abs(deltaY) > 10) break; mLastMotionY = y; //------------------------------------- mLastMotionX = x; scrollBy(deltaX, 0);
final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000); int velocityX = (int) velocityTracker.getXVelocity(); //Log.e(TAG, "velocityX:" + velocityX); if (velocityX > SNAP_VELOCITY && mCurScreen > 0) { // Fling enough to move left //Log.e(TAG, "snap left"); snapToScreen(mCurScreen - 1); } else if (velocityX < -SNAP_VELOCITY && mCurScreen < getChildCount() - 1) { // Fling enough to move right //Log.e(TAG, "snap right"); snapToScreen(mCurScreen + 1); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } // } mTouchState = TOUCH_STATE_REST;
http://my.oschina.net/sfshine/blog/151673