《Android開(kāi)發(fā)藝術(shù)探索》之學(xué)習(xí)筆記(三)View的基礎(chǔ)知識(shí)

View的基礎(chǔ)知識(shí)

  • 什么是View

View是Android中所有控件的基類,View是一種界面層的控件的一種抽象,它代表了一個(gè)控件,在Android設(shè)計(jì)中,ViewGroup也繼承了View,這就意味著View本身就可以是單個(gè)控件也可以是多個(gè)控件組成的一組控件,通過(guò)這種關(guān)系就形成了View樹(shù)的結(jié)構(gòu)。

  • View的位置參數(shù)

view的位置主要由它的四個(gè)頂點(diǎn)來(lái)決定,分別對(duì)應(yīng)于View的四個(gè)屬性:top、left、right、bottom,其中top是左上角縱坐標(biāo),left是左上角橫坐標(biāo),right是右下角橫坐標(biāo),bottom是右下角縱坐標(biāo)

從Android 3.0開(kāi)始,view增加了x、y、translationX、translationY四個(gè)參數(shù),這幾個(gè)參數(shù)也是相對(duì)于父容器的坐標(biāo)。x和y是左上角的坐標(biāo),而translationX和translationY是view左上角相對(duì)于父容器的偏移量,默認(rèn)值都是0。

x = left + translationX

y = top + translationY

View在平移的過(guò)程中,top和left的值不會(huì)發(fā)生改變(表示原始左上角的位置信息),發(fā)生改變的是x、y、translationX和translationY。

  • MotionEvent和TouchSlop

MotionEvent:

在手指觸摸屏幕后所產(chǎn)生的一系列事件中,典型的時(shí)間類型有:

1、ACTION_DOWN-手指剛接觸屏幕

2、ACTION_MOVE-手指在屏幕上移動(dòng)

3、ACTION_UP-手機(jī)從屏幕上松開(kāi)的一瞬間

正常情況下,一次手指觸摸屏幕的行為會(huì)觸發(fā)一系列點(diǎn)擊事件,考慮如下幾種情況:

1、點(diǎn)擊屏幕后離開(kāi)松開(kāi),事件序列為 DOWN -> UP

2、點(diǎn)擊屏幕滑動(dòng)一會(huì)再松開(kāi),事件序列為DOWN->MOVE->…->UP

通過(guò)MotionEvent對(duì)象我們可以得到點(diǎn)擊事件發(fā)生的x和y坐標(biāo),getX/getY返回的是相對(duì)于當(dāng)前View左上角的x和y坐標(biāo),getRawX和getRawY是相對(duì)于手機(jī)屏幕左上角的x和y坐標(biāo)。

TouchSlop:

TouchSlope是系統(tǒng)所能識(shí)別出的可以被認(rèn)為是滑動(dòng)的最小距離,獲取方式是ViewConfiguration.get(getContext()).getScaledTouchSlope()。

  • VelocityTracker、GestureDetector和Scroller

1、 VelocityTracker:用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平和垂直方向上的速度。

VelocityTracker的使用方式:

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

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

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

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

mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的時(shí)候調(diào)用

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

速度的計(jì)算公式:速度 = (終點(diǎn)位置 - 起點(diǎn)位置) / 時(shí)間段

速度可能為負(fù)值,例如當(dāng)手指從屏幕右邊往左邊滑動(dòng)的時(shí)候。此外,速度是單位時(shí)間內(nèi)移動(dòng)的像素?cái)?shù),單位時(shí)間不一定是1秒鐘,可以使用方法computeCurrentVelocity(xxx)指定單位時(shí)間是多少,單位是ms。例如通過(guò)computeCurrentVelocity(1000)來(lái)獲取速度,手指在1s中滑動(dòng)了100個(gè)像素,那么速度是100,即100(像素/1000ms)。如果computeCurrentVelocity(100)來(lái)獲取速度,在100ms內(nèi)手指只是滑動(dòng)了10個(gè)像素,那么速度是10,即10(像素/100ms)。

當(dāng)不需要的時(shí)候,需要調(diào)用clear方法來(lái)重置并回收內(nèi)存

velocityTracker.clear();
velocityTracker.recycler();

2、GestureDetector

手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的點(diǎn)擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。

在日常開(kāi)發(fā)中,比較常用的有:onSingleTapUp(單擊)、onFling(快速滑動(dòng))、onScroll(拖動(dòng))、onLongPress(長(zhǎng)按)、onDoubleTap(雙擊),建議:如果只是監(jiān)聽(tīng)滑動(dòng)相關(guān)的事件在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽(tīng)雙擊這種行為的話,那么就使用GestureDetector。

3、Scroller

彈性滑動(dòng)對(duì)象,用于實(shí)現(xiàn)View的彈性滑動(dòng)。Scroller本身無(wú)法讓View彈性滑動(dòng),它需要和View的computeScroll方法配合使用才能共同完成這個(gè)功能。

View的滑動(dòng)

通過(guò)三種方式可以實(shí)現(xiàn)View的滑動(dòng)

  • 通過(guò)View本身提供的scrollTo/scrollBy方法來(lái)實(shí)現(xiàn)滑動(dòng)
  • 動(dòng)畫(huà)給View施加平移效果來(lái)實(shí)現(xiàn)滑動(dòng)
  • 通過(guò)改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動(dòng)

1、使用scrollTo/scrollBy
scrollTo和scrollBy方法只能改變view內(nèi)容的位置而不能改變view在布局中的位置。 scrollBy是基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo是基于所傳參數(shù)的絕對(duì)滑動(dòng)。通過(guò)View的getScrollX和getScrollY方法可以得到滑動(dòng)的距離。

2、使用動(dòng)畫(huà)
使用動(dòng)畫(huà)來(lái)移動(dòng)view主要是操作view的translationX和translationY屬性,既可以使用傳統(tǒng)的view動(dòng)畫(huà),也可以使用屬性動(dòng)畫(huà),使用后者需要考慮兼容性問(wèn)題,如果要兼容Android3.0一下版本系統(tǒng)的話推薦使用nineoldandroids。使用動(dòng)畫(huà)還存在一個(gè)交互問(wèn)題:在android3.0以前的系統(tǒng)上,view動(dòng)畫(huà)和屬性動(dòng)畫(huà),新位置均無(wú)法觸發(fā)點(diǎn)擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。從3.0開(kāi)始,屬性動(dòng)畫(huà)的單擊事件觸發(fā)位置為移動(dòng)后的位置,view動(dòng)畫(huà)仍然在原位置。

3、改變布局參數(shù)
通過(guò)改變LayoutParams的方式去實(shí)現(xiàn)View的滑動(dòng)是一種靈活的方法。

4、各種滑動(dòng)方式的對(duì)比

  • scrollTo/scrollBy:操作簡(jiǎn)單,適合對(duì)View內(nèi)容的滑動(dòng)
  • 動(dòng)畫(huà):操作簡(jiǎn)單,主要適用于沒(méi)有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果
  • 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View

動(dòng)畫(huà)兼容庫(kù)nineoldandroids中的ViewHelper類提供了很多的get/set方法來(lái)為屬性動(dòng)畫(huà)服務(wù),例如setTranslationX和setTranslationY方法,這些方法是沒(méi)有版本要求的。

彈性滑動(dòng)

1、使用Scroller
Scroller的工作原理:Scroller本身并不能實(shí)現(xiàn)view的滑動(dòng),它需要配合view的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷地讓view重繪,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔,通過(guò)這個(gè)時(shí)間間隔Scroller就可以得出view的當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過(guò)scrollTo方法來(lái)完成view的滑動(dòng)。就這樣,view的每一次重繪都會(huì)導(dǎo)致view進(jìn)行小幅度的滑動(dòng),而多次的小幅度滑動(dòng)就組成了彈性滑動(dòng),這就是Scroller的工作原理。

2、通過(guò)動(dòng)畫(huà)
采用這種方法除了能完成彈性滑動(dòng)以外,還可以實(shí)現(xiàn)其他動(dòng)畫(huà)效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。

3、使用延時(shí)策略
使用延時(shí)策略來(lái)實(shí)現(xiàn)彈性滑動(dòng),它的核心思想是通過(guò)發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果,具體來(lái)說(shuō)可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用線程的sleep方法。

View的事件分發(fā)機(jī)制

1、事件分發(fā)機(jī)制的三個(gè)重要方法

public boolean dispatchTouchEvent(MotionEvent ev)
用來(lái)進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前的View,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。

public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法內(nèi)部調(diào)用,用來(lái)判斷是否攔截某個(gè)事件,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。

public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來(lái)處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前的事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無(wú)法再次接受到事件。

這三個(gè)方法的關(guān)系可以用如下偽代碼表示:

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

我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對(duì)于一個(gè)根ViewGroup來(lái)說(shuō),點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的dispatchTouchEvent會(huì)被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè)ViewGroup處理,即它的onTouchEvent方法就會(huì)被調(diào)用;如果這個(gè)ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當(dāng)前事件,這時(shí)當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。

OnTouchListener的優(yōu)先級(jí)比onTouchEvent要高

如果給一個(gè)view設(shè)置了OnTouchListener,那么OnTouchListener中的onTouch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看onTouch的返回值,如果返回false,那么當(dāng)前view的onTouchEvent方法會(huì)被調(diào)用;如果返回true,那么onTouchEvent方法將不會(huì)被調(diào)用。
在onTouchEvent方法中,如果當(dāng)前view設(shè)置了OnClickListener,那么它的onClick方法會(huì)被調(diào)用,所以O(shè)nClickListener的優(yōu)先級(jí)最低。

當(dāng)點(diǎn)擊一個(gè)事件產(chǎn)生后,它的傳遞過(guò)程遵循如順序,Activity->Window->View

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

關(guān)于事件傳遞的機(jī)制,給出一些結(jié)論:

  • 同一個(gè)事件序列是以down事件開(kāi)始,中間含有數(shù)量不定的move事件,最終以u(píng)p事件結(jié)束

  • 正常情況下,一個(gè)事件序列只能被一個(gè)View攔截且消耗。一旦一個(gè)元素?cái)r截了某次事件,那么同一個(gè)事件序列內(nèi)的所有事件都會(huì)直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過(guò)特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過(guò)onTouchEvent強(qiáng)行傳遞給其他View處理

  • 某個(gè)View一旦開(kāi)始處理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列的其他事情都不會(huì)再交給它來(lái)處理,并且事件將重新交給它的父容器去處理(調(diào)用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他類型事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,父容器的onTouchEvent方法不會(huì)被調(diào)用,當(dāng)前view依然可以收到后續(xù)的事件,但是這些事件最后都會(huì)傳遞給Activity處理。

  • ViewGroup默認(rèn)不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false,View沒(méi)有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,那么它的onTouchEvent方法就會(huì)調(diào)用。

  • View的onTouchEvent默認(rèn)都會(huì)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。View的longClickable屬性默認(rèn)都為false,clickable要分情況,比如Button的clickable屬性默認(rèn)為true,而TextView的clickable屬性默認(rèn)為false。

  • View的enable屬性不影響onTouchEvent的默認(rèn)返回值,哪怕一個(gè)View是disable狀態(tài)的,只要它的clickable或者longClickable有一個(gè)為true,那么它的onTouchEvent就返回true

  • 事件傳遞過(guò)程總是先傳遞給父元素,然后再由父元素分發(fā)給子view,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程,但是ACTION_DOWN事件除外,即當(dāng)面對(duì)ACTION_DOWN事件時(shí),ViewGroup總是會(huì)調(diào)用自己的onInterceptTouchEvent方法來(lái)詢問(wèn)自己是否要攔截事件。

View的滑動(dòng)沖突

1、常見(jiàn)的滑動(dòng)沖突場(chǎng)景

外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致,比如ViewPager中包含ListView
外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致
上面兩種情況的嵌套
2、滑動(dòng)沖突的處理規(guī)則

可以根據(jù)滑動(dòng)距離和水平方向形成的夾角;或者根絕水平和豎直方向滑動(dòng)的距離差;或者兩個(gè)方向上的速度差等。

3、滑動(dòng)沖突的解決方式

  • 外部攔截法

點(diǎn)擊事件都經(jīng)過(guò)父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,該方法需要重寫(xiě)父容器的onInterceptTouchEvent方法,再內(nèi)部做相應(yīng)的攔截即可,偽代碼如下:

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 (父容器需要攔截當(dāng)前點(diǎn)擊事件的條件,例如: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;
}
  • 內(nèi)部攔截法

父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就由父容器進(jìn)行處理,這種方法和Android中的事件分發(fā)機(jī)制不一樣,需要配合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 (當(dāng)前view需要攔截當(dāng)前點(diǎn)擊事件的條件,例如:   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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容