《Android開發藝術探索》之學習筆記(三)View的基礎知識

View的基礎知識

  • 什么是View

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

  • View的位置參數

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

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

x = left + translationX

y = top + translationY

View在平移的過程中,top和left的值不會發生改變(表示原始左上角的位置信息),發生改變的是x、y、translationX和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();

2、GestureDetector

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

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

3、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);
    }
}

我們可以大致了解點擊事件的傳遞規則:對于一個根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
外部滑動方向與內部滑動方向一致
上面兩種情況的嵌套
2、滑動沖突的處理規則

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

3、滑動沖突的解決方式

  • 外部攔截法

點擊事件都經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,該方法需要重寫父容器的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);
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容