《Android開發(fā)藝術(shù)探索》Chap3_View的事件體系

注:此篇筆記只記錄重難點(diǎn)李滴,對于基礎(chǔ)和詳細(xì)內(nèi)容請自行學(xué)習(xí)《Android開發(fā)藝術(shù)探索》


View的基礎(chǔ)知識

  • 什么是View

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

  • View的位置參數(shù)

view的位置主要由它的四個頂點(diǎn)來決定,分別對應(yīng)于View的四個屬性:top剔桨、left屉更、right、bottom洒缀,其中top是左上角縱坐標(biāo)瑰谜,left是左上角橫坐標(biāo),right是右下角橫坐標(biāo)树绩,bottom是右下角縱坐標(biāo)

View的寬高和坐標(biāo)的關(guān)系:

width = right - left;

height = bottom - top;

如何得到這四個參數(shù):

Left = getLeft();

Right = getRight();

Top = getTop();

Bottom = getBottom();

從Android 3.0開始萨脑,view增加了x、y饺饭、translationX渤早、translationY四個參數(shù),這幾個參數(shù)也是相對于父容器的坐標(biāo)瘫俊。x和y是左上角的坐標(biāo)鹊杖,而translationX和translationY是view左上角相對于父容器的偏移量,默認(rèn)值都是0扛芽。

x = left + translationX

y = top + translationY

  • MotionEvent和TouchSlop

MotionEvent:

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

1、ACTION_DOWN-手指剛接觸屏幕

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

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

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

1被芳、點(diǎn)擊屏幕后離開松開缰贝,事件序列為 DOWN -> UP

2、點(diǎn)擊屏幕滑動一會再松開畔濒,事件序列為DOWN->MOVE->...->UP

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

TouchSlop:

TouchSlope是系統(tǒng)所能識別出的可以被認(rèn)為是滑動的最小距離李破,獲取方式是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的時候調(diào)用

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

速度的計(jì)算公式:

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

速度可能為負(fù)值,例如當(dāng)手指從屏幕右邊往左邊滑動的時候承粤。此外暴区,速度是單位時間內(nèi)移動的像素?cái)?shù),單位時間不一定是1秒鐘辛臊,可以使用方法computeCurrentVelocity(xxx)指定單位時間是多少仙粱,單位是ms。例如通過computeCurrentVelocity(1000)來獲取速度彻舰,手指在1s中滑動了100個像素伐割,那么速度是100,即100(像素/1000ms)刃唤。如果computeCurrentVelocity(100)來獲取速度隔心,在100ms內(nèi)手指只是滑動了10個像素,那么速度是10尚胞,即10(像素/100ms)硬霍。

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

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

?

2笼裳、GestureDetector

手勢檢測唯卖,用于輔助檢測用戶的點(diǎn)擊、滑動躬柬、長按拜轨、雙擊等行為。

在日常開發(fā)中楔脯,比較常用的有:onSingleTapUp(單擊)撩轰、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)堪嫂、onDoubleTap(雙擊)偎箫,建議:如果只是監(jiān)聽滑動相關(guān)的事件在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為的話皆串,那么就使用GestureDetector淹办。

3、Scroller

彈性滑動對象恶复,用于實(shí)現(xiàn)View的彈性滑動怜森。Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能谤牡。

View的滑動

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

  • 第一種是通過View本身提供的scrollTo/scrollBy方法來實(shí)現(xiàn)滑動
  • 第二種是通過動畫給View施加平移效果來實(shí)現(xiàn)滑動
  • 通過改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動

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

2套么、使用動畫
使用動畫來移動view主要是操作view的translationX和translationY屬性培己,既可以使用傳統(tǒng)的view動畫,也可以使用屬性動畫胚泌,使用后者需要考慮兼容性問題省咨,如果要兼容Android3.0一下版本系統(tǒng)的話推薦使用nineoldandroids。使用動畫還存在一個交互問題:在android3.0以前的系統(tǒng)上玷室,view動畫和屬性動畫零蓉,新位置均無法觸發(fā)點(diǎn)擊事件,同時阵苇,老位置仍然可以觸發(fā)單擊事件壁公。從3.0開始,屬性動畫的單擊事件觸發(fā)位置為移動后的位置绅项,view動畫仍然在原位置紊册。

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

4囊陡、各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單,適合對View內(nèi)容的滑動
  • 動畫:操作簡單掀亥,主要適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動畫效果
  • 改變布局參數(shù):操作稍微復(fù)雜撞反,適用于有交互的View

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

彈性滑動

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

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

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

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

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

  • public boolean dispatchTouchEvent(MotionEvent ev)

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

  • public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法內(nèi)部調(diào)用离例,用來判斷是否攔截某個事件,如果當(dāng)前View攔截了某個事件悉稠,那么在同一個事件序列當(dāng)中宫蛆,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件的猛。

  • public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中調(diào)用耀盗,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前的事件卦尊,如果不消耗叛拷,則在同一個事件序列中,當(dāng)前View無法再次接受到事件岂却。

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

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

我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對于一個根ViewGroup來說忿薇,點(diǎn)擊事件產(chǎn)生后裙椭,首先會傳遞給它,這時它的dispatchTouchEvent會被調(diào)用署浩,如果這個ViewGroup的onInterceptTouchEvent方法返回true骇陈,就表示它要攔截當(dāng)前事件,接著事件就會交給這個ViewGroup處理瑰抵,即它的onTouchEvent方法就會被調(diào)用你雌;如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當(dāng)前事件二汛,這時當(dāng)前事件就會繼續(xù)傳遞給它的子元素婿崭,接著子元素的dispatchTouchEvent方法就會被調(diào)用,如此反復(fù)直到事件被最終處理肴颊。

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

如果給一個view設(shè)置了OnTouchListener氓栈,那么OnTouchListener中的onTouch方法會被回調(diào)。這時事件如何處理還要看onTouch的返回值婿着,如果onTouch返回false授瘦,那么當(dāng)前view的onTouchEvent方法會被調(diào)用;如果onTouch返回true竟宋,那么onTouchEvent方法將不會被調(diào)用提完。

在onTouchEvent方法中,如果當(dāng)前view設(shè)置了OnClickListener丘侠,那么它的onClick方法會被調(diào)用徒欣,所以O(shè)nClickListener的優(yōu)先級最低。

當(dāng)點(diǎn)擊一個事件產(chǎn)生后蜗字,它的傳遞過程遵循如順序打肝,Activity->Window->View。如果一個View的onTouchEvent方法返回false挪捕,那么它的父容器的onTouchEvent方法將會被調(diào)用粗梭,依次類推,如果所有的元素都不處理這個事件级零,那么這個事件將會最終傳遞給Activity處理(調(diào)用Activity的onTouchEvent方法)

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

  • 同一個事件序列是以down事件開始,中間含有數(shù)量不定的move事件妄讯,最終以up事件結(jié)束孩锡。
  • 正常情況下,一個事件序列只能被一個View攔截且消耗亥贸。一旦一個元素?cái)r截了某次事件躬窜,那么同一個事件序列內(nèi)的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理炕置,但是通過特殊手段可以做到荣挨,比如一個View將本該自己處理的事件通過onTouchEvent強(qiáng)行傳遞給其他View處理男韧。
  • 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件默垄,那么同一事件序列的其他事情都不會再交給它來處理此虑,并且事件將重新交給它的父容器去處理(調(diào)用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件口锭,但是不消耗其他類型事件朦前,那么這個點(diǎn)擊事件會消失,父容器的onTouchEvent方法不會被調(diào)用鹃操,當(dāng)前view依然可以收到后續(xù)的事件韭寸,但是這些事件最后都會傳遞給Activity處理
  • ViewGroup默認(rèn)不攔截任何事件荆隘。Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false恩伺,View沒有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它椰拒,那么它的onTouchEvent方法就會調(diào)用晶渠。
  • View的onTouchEvent默認(rèn)都會消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時為false)燃观。View的longClickable屬性默認(rèn)都為false褒脯,clickable要分情況,比如Button的clickable屬性默認(rèn)為true仪壮,而TextView的clickable屬性默認(rèn)為false憨颠。
  • View的enable屬性不影響onTouchEvent的默認(rèn)返回值,哪怕一個View是disable狀態(tài)的积锅,只要它的clickable或者longClickable有一個為true,那么它的onTouchEvent就返回true养盗。
  • 事件傳遞過程總是先傳遞給父元素缚陷,然后再由父元素分發(fā)給子view,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程往核,但是ACTION_DOWN事件除外箫爷,即當(dāng)面對ACTION_DOWN事件時,ViewGroup總是會調(diào)用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件聂儒。

View的滑動沖突

1虎锚、常見的滑動沖突場景

  • 外部滑動方向與內(nèi)部滑動方向不一致,比如ViewPager中包含ListView
  • 外部滑動方向與內(nèi)部滑動方向一致
  • 上面兩種情況的嵌套

2衩婚、滑動沖突的處理規(guī)則

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

3柱徙、滑動沖突的解決方式

  • 外部攔截法

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

  • 首先羊初,ACTION_DOWN這個事件滨溉,父容器必須返回false,即不攔截ACTION_DOWN事件,因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE/ACTION_UP都會直接交給父容器處理长赞;
  • 其次业踏,ACTION_MOVE,根據(jù)需求來決定是否要攔截;
  • 最后,ACTION_UP事件,這里必須要返回false,在這里沒有多大意義

    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方法才能正常工作乐设。

  • 父元素需要默認(rèn)攔截除ACTION_DOWN以外的事件,這樣子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時讼庇,父元素才能繼續(xù)攔截需要的事件。
  • ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法影響,所以一旦父元素?cái)r截ACTION_DOWN事件,那么所有元素都無法傳遞到子元素去近尚。
    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)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市戈锻,隨后出現(xiàn)的幾起案子歼跟,更是在濱河造成了極大的恐慌,老刑警劉巖格遭,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哈街,死亡現(xiàn)場離奇詭異,居然都是意外死亡拒迅,警方通過查閱死者的電腦和手機(jī)骚秦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來璧微,“玉大人作箍,你說我怎么就攤上這事∏傲颍” “怎么了胞得?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長开瞭。 經(jīng)常有香客問我懒震,道長罩息,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任个扰,我火速辦了婚禮瓷炮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘递宅。我一直安慰自己娘香,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布办龄。 她就那樣靜靜地躺著烘绽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俐填。 梳的紋絲不亂的頭發(fā)上安接,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音英融,去河邊找鬼盏檐。 笑死,一個胖子當(dāng)著我的面吹牛驶悟,可吹牛的內(nèi)容都是我干的胡野。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼痕鳍,長吁一口氣:“原來是場噩夢啊……” “哼硫豆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笼呆,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤熊响,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抄邀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耘眨,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年境肾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胆屿。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡奥喻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出非迹,到底是詐尸還是另有隱情环鲤,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布憎兽,位于F島的核電站冷离,受9級特大地震影響吵冒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜西剥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一痹栖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瞭空,春花似錦揪阿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至旧找,卻和暖如春溺健,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钮蛛。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工鞭缭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人愿卒。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓缚去,卻偏偏與公主長得像,于是被迫代替她去往敵國和親琼开。 傳聞我的和親對象是個殘疾皇子易结,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容