Android開源中國客戶端學習 (自定義View)左右滑動控件ScrollLayout <11>

左右滑動的控件咱們使用的也是很是多了,可是基本上都是使用的viewpager 等 android基礎的控件,那麼咱們有麼有考慮過查看他的源碼進行定製呢?固然,若是你自我感受很是好的話能夠本身定製一個,osc的ScrollLayout就是本身定義的View 和Viewpager的區別仍是不小的 java

代碼不是不少不到300行,可是卻實現了左右滑動頁面的效果,仍是值得學習的.效果以下: android

咱們看到ScrollLayout直接繼承了ViewGroup而後自定義了一系列功能,那麼接下來就分析一下: app


咱們知道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的做用是什麼呢? ide

其實咱們能夠把android View 認爲是一個桌布,屏幕的左上角是 0,0 scrollTo 就是把這個view移動到某個位置. 函數

如圖來講明 0,0 表示屏幕的左上角 view調用了view.scrollTo(2,3)後就能夠跳轉到這個位置了~ post

至於咱們的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的時候纔會結束,這樣動畫也就執行完了,那他就滑動到下一屏了~

總之就是 invalidate ->computeScroll  scrollTo->postInvalidate->invalidate 這樣的死循環 終止的條件和座標由scroller來決定

爲了易於控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個流程。在繪製View時,會在draw()過程調用該
  方法。所以, 再配合使用Scroller實例,咱們就能夠得到當前應該的偏移座標,手動使View/ViewGroup偏移至該處。
 computeScroll()方法原型以下,該方法位於ViewGroup.java類中      

咱們像下面這樣調用,postInvalidate執行後,會去調computeScroll 方法,而這個方法裏再去調postInvalidate,這樣就能夠不斷地去調用scrollTo方法了,直到mScroller動畫 (computScroll 爲 false)結束,固然第一次時,咱們須要手動去調用一次postInvalidate纔會去調用 

參考 http://blog.csdn.net/c_weibin/article/details/7438323

@Override
	public void computeScroll() {
		if (mScroller.computeScrollOffset()) {
			scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
			postInvalidate();
		}
	}

而後就是處理這個view的手勢操做事件了 首先是對攔截事件的處理

這個控件有個用來表示當前view 所處運動狀態的 標誌位mTouchState

有兩個值

TOUCH_STATE_REST:這個view中止運動

TOUCH_STATE_SCROLLING:這個view正在滾動

onInterceptTouchEvent就是用兩個標誌位來判斷是否須要吧事件想view的孩子分配.代碼以下:

實際上就是,手指在view的運動超過必定的距離就進行把state置爲TOUCH_STATE_SCROLLING 這樣返回的就是true, 因而事件讓view本身處理 孩子獲取不到到事件了,不然就把事件攔截


@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;
	}

 事件攔截到後就會執行ScrollLayout的onTouch函數了


@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;
	}


在用戶手指放到view上移動的時候會經過


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);


這幾句話來移動到相應的位置

若是用戶手指放開,還會計算當前的速度,若是速度達到必定的值就會跳轉到下一頁 VelocityTracker類專門用來測量當前移動的速度 


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;


至此,自定義ScrollLayout就分析完了. 這個博客花費了很多時間 ,可是本身收穫很大,溫習了不少知識.可能這個博客和枯燥,可是看完相信你會有收穫的~android 自定義View是每個android工程師必備的技能,你們必定好好學習研究~

相關文章
相關標籤/搜索