學習筆記|《Android開發藝術探索》第三章

《Android開發藝術探索》第三章筆記


View的基礎知識

  • 什麼是View

View是Android中所有控件的基類,View是一種界面層的控件的一種抽象,它代表了一個控件,在Android設計中,ViewGroup也繼承了View,這就意味着View本身就可以是單個控件也可以是多個控件組成的一組控件,通過這種關係就形成了View樹的結構。

  • View的位置參數

view的位置主要由它的四個頂點來決定,分別對應於View的四個屬性:top、left、right、bottom,其中top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角縱座標
View的寬高和座標的關係:

width = right - left;

height = bottom - top;

如何得到這四個參數:

Left = getLeft();

Right = getRight();

Top = getTop();

Bottom = getBottom();

從Android 3.0開始,view增加了x、y、translationX、translationY四個參數,這幾個參數也是相對於父容器的座標。x和y是左上角的座標,而translationX和translationY是view左上角相對於父容器的偏移量,默認值都是0。

x = left + translationX

y = top + translationY

  • MotionEvent和TouchSlop

MotionEvent:

在手指觸摸屏幕後所產生的一系列事件中,典型的時間類型有:

1、ACTION_DOWN-手指剛接觸屏幕

2、ACTION_MOVE-手指在屏幕上移動

3、ACTION_UP-手機從屏幕上鬆開的一瞬間

正常情況下,一次手指觸摸屏幕的行爲會觸發一系列點擊事件,考慮如下幾種情況:

1、點擊屏幕後離開鬆開,事件序列爲 DOWN -> UP

2、點擊屏幕滑動一會再鬆開,事件序列爲DOWN->MOVE->…->UP

通過MotionEvent對象我們可以得到點擊事件發生的x和y座標,getX/getY返回的是相對於當前View左上角的x和y座標,getRawX和getRawY是相對於手機屏幕左上角的x和y座標。

TouchSlop:

TouchSlope是系統所能識別出的可以被認爲是滑動的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()。

  • VelocityTracker、GestureDetector和Scroller
  1. VelocityTracker
    用於追蹤手指在滑動過程中的速度,包括水平和垂直方向上的速度。

VelocityTracker的使用方式:

	//初始化
	VelocityTracker mVelocityTracker = VelocityTracker.obtain();

	//在onTouchEvent方法中
	mVelocityTracker.addMovement(event);

	//獲取速度
	mVelocityTracker.computeCurrentVelocity(1000);

	float xVelocity = mVelocityTracker.getXVelocity();
	//重置和回收

	mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時候調用

	mVelocityTracker.recycle(); //一般在onDetachedFromWindow中調用

速度的計算公式:

速度 = (終點位置 - 起點位置) / 時間段

速度可能爲負值,例如當手指從屏幕右邊往左邊滑動的時候。此外,速度是單位時間內移動的像素數,單位時間不一定是1秒鐘,可以使用方法computeCurrentVelocity(xxx)指定單位時間是多少,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度,手指在1s中滑動了100個像素,那麼速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來獲取速度,在100ms內手指只是滑動了10個像素,那麼速度是10,即10(像素/100ms)。

當不需要的時候,需要調用clear方法來重置並回收內存

	velocityTracker.clear();
	velocityTracker.recycler();
  1. GestureDetector

手勢檢測,用於輔助檢測用戶的點擊、滑動、長按、雙擊等行爲。

在日常開發中,比較常用的有:onSingleTapUp(單擊)、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)、onDoubleTap(雙擊),建議:如果只是監聽滑動相關的事件在onTouchEvent中實現;如果要監聽雙擊這種行爲的話,那麼就使用GestureDetector。

  1. Scroller

彈性滑動對象,用於實現View的彈性滑動。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。

View的滑動

通過三種方式可以實現View的滑動

  • 第一種是通過View本身提供的scrollTo/scrollBy方法來實現滑動
  • 第二種是通過動畫給View施加平移效果來實現滑動
  • 通過改變View的LayoutParams使得View重新佈局從而實現滑動
  1. 使用scrollTo/scrollBy
    scrollTo和scrollBy方法只能改變view內容的位置而不能改變view在佈局中的位置。 scrollBy是基於當前位置的相對滑動,而scrollTo是基於所傳參數的絕對滑動。通過View的getScrollX和getScrollY方法可以得到滑動的距離。

  2. 使用動畫
    使用動畫來移動view主要是操作view的translationX和translationY屬性,既可以使用傳統的view動畫,也可以使用屬性動畫,使用後者需要考慮兼容性問題,如果要兼容Android3.0一下版本系統的話推薦使用nineoldandroids。使用動畫還存在一個交互問題:在android3.0以前的系統上,view動畫和屬性動畫,新位置均無法觸發點擊事件,同時,老位置仍然可以觸發單擊事件。從3.0開始,屬性動畫的單擊事件觸發位置爲移動後的位置,view動畫仍然在原位置。

  3. 改變佈局參數
    通過改變LayoutParams的方式去實現View的滑動是一種靈活的方法。

  4. 各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單,適合對View內容的滑動
  • 動畫:操作簡單,主要適用於沒有交互的View和實現複雜的動畫效果
  • 改變佈局參數:操作稍微複雜,適用於有交互的View

動畫兼容庫nineoldandroids中的ViewHelper類提供了很多的get/set方法來爲屬性動畫服務,例如setTranslationX和setTranslationY方法,這些方法是沒有版本要求的。

彈性滑動

  1. 使用Scroller
    Scroller的工作原理:Scroller本身並不能實現view的滑動,它需要配合view的computeScroll方法才能完成彈性滑動的效果,它不斷地讓view重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出view的當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成view的滑動。就這樣,view的每一次重繪都會導致view進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作原理。

  2. 通過動畫
    採用這種方法除了能完成彈性滑動以外,還可以實現其他動畫效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。

  3. 使用延時策略
    使用延時策略來實現彈性滑動,它的核心思想是通過發送一系列延時消息從而達到一種漸進式的效果,具體來說可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法。

View的事件分發機制

  1. 事件分發機制的三個重要方法
  • public boolean dispatchTouchEvent(MotionEvent ev)

用來進行事件的分發。如果事件能夠傳遞給當前的View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

  • public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

  • public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前的事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。

這三個方法的關係可以用如下僞代碼表示:

	public boolean dispatchTouchEvent(MotionEvent event)
	{
		boolean consume = false;
		if(onInterceptTouchEvent(ev))
		{
			consume = onTouchEvent(ev);
		}
		else
		{
			consume = child.dispatchTouchEvent(ev);
		}
		return consume;
	}

我們可以大致瞭解點擊事件的傳遞規則:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此反覆直到事件被最終處理。

OnTouchListener的優先級比onTouchEvent要高

如果給一個view設置了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,那麼當前view的onTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。
在onTouchEvent方法中,如果當前view設置了OnClickListener,那麼它的onClick方法會被調用,所以OnClickListener的優先級最低。

當點擊一個事件產生後,它的傳遞過程遵循如順序,Activity->Window->View

如果一個View的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent方法將會被調用,依次類推,如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理(調用Activity的onTouchEvent方法)

關於事件傳遞的機制,給出一些結論:

  • 同一個事件序列是以down事件開始,中間含有數量不定的move事件,最終以up事件結束
  • 正常情況下,一個事件序列只能被一個View攔截且消耗。一旦一個元素攔截了某次事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊手段可以做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理
  • 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件,那麼同一事件序列的其他事情都不會再交給它來處理,並且事件將重新交給它的父容器去處理(調用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那麼這個點擊事件會消失,父容器的onTouchEvent方法不會被調用,當前view依然可以收到後續的事件,但是這些事件最後都會傳遞給Activity處理。
  • ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false,View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會調用。
  • View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時爲false)。View的longClickable屬性默認都爲false,clickable要分情況,比如Button的clickable屬性默認爲true,而TextView的clickable屬性默認爲false。
  • View的enable屬性不影響onTouchEvent的默認返回值,哪怕一個View是disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true
  • 事件傳遞過程總是先傳遞給父元素,然後再由父元素分發給子view,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外,即當面對ACTION_DOWN事件時,ViewGroup總是會調用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件。

View的滑動衝突

  1. 常見的滑動衝突場景
  • 外部滑動方向與內部滑動方向不一致,比如ViewPager中包含ListView
  • 外部滑動方向與內部滑動方向一致
  • 上面兩種情況的嵌套
  1. 滑動衝突的處理規則

可以根據滑動距離和水平方向形成的夾角;或者根據水平和豎直方向滑動的距離差;或者兩個方向上的速度差等。

  1. 滑動衝突的解決方式
  • 外部攔截法

點擊事件都經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,該方法需要重寫父容器的onInterceptTouchEvent方法,在內部做相應的攔截即可,僞代碼如下:

	public boolean onInterceptTouchEvent(MotionEvent event) {
    	boolean intercepted = false;
    	int x = (int) event.getX();
   		int y = (int) event.getY();

    	switch (event.getAction()) {
    	case MotionEvent.ACTION_DOWN: {
    	    intercepted = false;
    	    break;
    	}
    	case MotionEvent.ACTION_MOVE: {
     	   int deltaX = x - mLastXIntercept;
     	   int deltaY = y - mLastYIntercept;
     	   if (父容器需要攔截當前點擊事件的條件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
       	     intercepted = true;
       	 } else {
        	    intercepted = false;
      	  }
       	 break;
    	}
    	case MotionEvent.ACTION_UP: {
     	   intercepted = false;
        	break;
    	}	
    	default:
        	break;
    	}

    	mLastXIntercept = x;
    	mLastYIntercept = y;

    	return intercepted;
	}
  • 內部攔截法

父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就由父容器進行處理,這種方法和Android中的事件分發機制不一樣,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

	public boolean dispatchTouchEvent(MotionEvent event) {
    	int x = (int) event.getX();
    	int y = (int) event.getY();

    	switch (event.getAction()) {
    	case MotionEvent.ACTION_DOWN: {]
    	    getParent().requestDisallowInterceptTouchEvent(true);
        	break;
    	}
    	case MotionEvent.ACTION_MOVE: {
        	int deltaX = x - mLastX;
        	int deltaY = y - mLastY;
        	if (當前view需要攔截當前點擊事件的條件,例如:	Math.abs(deltaX) > Math.abs(deltaY)) {
            		getParent().requestDisallowInterceptTouchEvent(false);
        	}
        	break;
    	}
    	case MotionEvent.ACTION_UP: {
        	break;
    	}
    	default:
        	break;
    	}

    	mLastX = x;
    	mLastY = y;
    	return super.dispatchTouchEvent(event);
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章