天天看點

Android View事件機制21問21答

1.view的坐标參數 主要有哪些?分别有什麼注意的要點?

答:left,right,top,bottom 注意這4個值其實就是 view 和 他的父控件的 相對坐标值。 并非是距離螢幕左上角的絕對值,這點要注意。

此外,x和y 其實也是相對于父控件的坐标值。 translationx,translationy 這2個值 預設都為0,是相對于父控件的左上角的偏移量。

換算關系:

x=left+tranx,y=top+trany.

很多人不了解,為什麼事這樣,其實就是view 如果有移動的話,比如平移這種,你們就要注意了,top和left 這種值 是不會變化的。

無論你把view怎麼拖動,但是 x,y,tranx,trany 的值是随着拖動平移 而變化的。想明白這點 就行了。

2.ontouchevent和gesturedetector 在什麼時候用哪個比較好?

答:隻有滑動需求的時候 就用前者,如果有輕按兩下等這種行為的時候 就用後者。

3.scroller 用來解決什麼問題?

答:view的scrollto和scrollby 滑動效果太差了,是瞬間完成。而scroller可以配合view的computescroll 來完成 漸變的滑動效果。體驗更好。

4.scrollto和scrollby 有什麼需要注意的?

答:前者是絕對滑動,後者是相對滑動。滑動的是view的内容 而不是view本身。這很重要。比如textview 調用這2個方法 滑動的就是顯示出來的字的内容。

一般而言 我們用scrollby會比較多一些。傳值的話 其實 記住幾個法則就可以了。 右-左 x為正 否則x為負 上-下 y為負,否則y為正。

可以稍微看一下 這2個的源碼:

public void scrollto(int x, int y) { 

        if (mscrollx != x || mscrolly != y) { 

            int oldx = mscrollx; 

            int oldy = mscrolly; 

            mscrollx = x; 

            mscrolly = y; 

            invalidateparentcaches(); 

            onscrollchanged(mscrollx, mscrolly, oldx, oldy); 

            if (!awakenscrollbars()) { 

                postinvalidateonanimation(); 

            } 

        } 

    } 

 public void scrollby(int x, int y) { 

        scrollto(mscrollx + x, mscrolly + y); 

    }  

看到裡面有2個變量 mscrollx 和mscrolly 這2個東西沒,這2個機關的 值是像素,前者代表 view的左邊緣和view内容左邊緣的距離。 後者代表 view上邊緣和view内容上邊緣的距離。

5.使用動畫來實作view的滑動 有什麼後果?

答:實際上view動畫 是對view的表面ui 也就是給使用者呈現出的視覺效果 來做的移動,動畫本身并不能移動view的真正位置。屬性動畫除外。動畫播放結束以後,view最終還是會回到自己的位置的,。當然了你可以設定fillafter 屬性 來讓動畫播放結束以後 view表象停留在 變化以後的位置。是以這會帶來一個很嚴重的後果。比如你的button在螢幕的左邊,你現在用個動畫 并且設定了fillafter屬性讓他去了右邊。你會發現 點選右邊的button 沒有click事件觸發,但是點選左邊的 卻可以觸發,原因就是右邊的button 隻是view的表象,真正的button 還在左邊沒有動過。你一定要這麼做的話 可以提前在右邊button移動後的位置放一個新的button,當你動畫執行結束以後 把右邊的enable 左邊的讓他gone就可以了。

這麼做就可以規避上述問題。

6.讓view滑動總共有幾種方式,分别要注意什麼?都适用于那些場景?

答:總共有三種:

a:scrollto,scrollby。這種是最簡單的,但是隻能滑動view的内容 不可以滑動view本身。

b:動畫。動畫可以滑動view内容,但是注意非屬性動畫 就如我們問題5說的内容 會影響到互動,使用的時候要多注意。不過多數複雜的滑動效果都是屬性動畫來完成的,屬于大殺器級别、

c:改變布局參數。這種最好了解了,無非是動态的通過java代碼來修改 margin等view的參數罷了。不過用的比較少。我本人不怎麼用這種方法。

7.scroller是幹嘛的?原理是什麼?

答:scroller就是用于 讓view有滑動漸變效果的。用法如下:

package com.example.administrator.motioneventtest; 

import android.content.context; 

import android.util.attributeset; 

import android.widget.scroller; 

import android.widget.textview; 

/** 

 * created by administrator on 2016/2/2. 

 */ 

public class customtextview extends textview{ 

    private scroller mscroller; 

    public customtextview(context context) { 

        super(context); 

        mscroller=new scroller(context); 

    public customtextview(context context, attributeset attrs) { 

        super(context, attrs); 

    public customtextview(context context, attributeset attrs, int defstyleattr) { 

        super(context, attrs, defstyleattr); 

    //調用此方法滾動到目标位置 

    public void smoothscrollto(int fx, int fy) { 

        int dx = fx - mscroller.getfinalx(); 

        int dy = fy - mscroller.getfinaly(); 

        smoothscrollby(dx, dy); 

    //調用此方法設定滾動的相對偏移 

    public void smoothscrollby(int dx, int dy) { 

        //設定mscroller的滾動偏移量 

        mscroller.startscroll(mscroller.getfinalx(), mscroller.getfinaly(), dx, dy,4000); 

        invalidate();//這裡必須調用invalidate()才能保證computescroll()會被調用,否則不一定會重新整理界面,看不到滾動效果 

    //使用scroller最重要不要遺漏這個方法 

    @override 

    public void computescroll() { 

        if (mscroller.computescrolloffset()) 

        { 

            scrollto(mscroller.getcurrx(),mscroller.getcurry()); 

            //這個方法不要忘記調用。 

            postinvalidate(); 

        super.computescroll(); 

}  

其實上述代碼 很多人應該都能搜到。我們這裡主要講一下 他的原理。

//參數很好了解 前面滑動起始點 中間滑動距離 最後一個是 漸變時間 

//而且我們看到startscroll 這個方法就是設定了一下參數 并沒有什麼滑動的代碼在 

//回到前面的demo能看到我們通常調用完這個方法以後 都會馬上調用invalidate()方法 

 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; 

//我們都知道invalidate 會觸發view的 draw方法  

//我們跟進去看 會發現draw方法裡 會調用下面的代碼: 

//也就是說會調用    computescroll方法 而view本身這個方法 

//是空的是以會留給我們自己實作 

 int sx = 0; 

        int sy = 0; 

        if (!drawingwithrendernode) { 

            computescroll(); 

            sx = mscrollx; 

            sy = mscrolly; 

//然後回到我們的customtextview 可以看到我們實作的    computescroll方法如下: 

//你看在這個方法裡 我們調用了scrollto方法 來實作滑動,滑動結束以後再次觸發view的重繪 

//然後又會再次觸發computescroll 實作一個循環。 

public void computescroll() { 

//傳回true就代表滑動還沒結束 false就是結束了 

//其實這個方法 就跟屬性動畫裡的插值器一樣 你在使用startscroll方法的時候 會傳一個事件的值, 

//這個方法就是根據這個事件的值來計算你每一次scrollx和scrolly的值 

public boolean computescrolloffset() { 

        if (mfinished) { 

            return false; 

        int timepassed = (int)(animationutils.currentanimationtimemillis() - mstarttime); 

        if (timepassed < mduration) { 

            switch (mmode) { 

            case scroll_mode: 

                final float x = minterpolator.getinterpolation(timepassed * mdurationreciprocal); 

                mcurrx = mstartx + math.round(x * mdeltax); 

                mcurry = mstarty + math.round(x * mdeltay); 

                break; 

            case fling_mode: 

                final float t = (float) timepassed / mduration; 

                final int index = (int) (nb_samples * t); 

                float distancecoef = 1.f; 

                float velocitycoef = 0.f; 

                if (index < nb_samples) { 

                    final float t_inf = (float) index / nb_samples; 

                    final float t_sup = (float) (index + 1) / nb_samples; 

                    final float d_inf = spline_position[index]; 

                    final float d_sup = spline_position[index + 1]; 

                    velocitycoef = (d_sup - d_inf) / (t_sup - t_inf); 

                    distancecoef = d_inf + (t - t_inf) * velocitycoef; 

                } 

                mcurrvelocity = velocitycoef * mdistance / mduration * 1000.0f; 

                mcurrx = mstartx + math.round(distancecoef * (mfinalx - mstartx)); 

                // pin to mminx <= mcurrx <= mmaxx 

                mcurrx = math.min(mcurrx, mmaxx); 

                mcurrx = math.max(mcurrx, mminx); 

                mcurry = mstarty + math.round(distancecoef * (mfinaly - mstarty)); 

                // pin to mminy <= mcurry <= mmaxy 

                mcurry = math.min(mcurry, mmaxy); 

                mcurry = math.max(mcurry, mminy); 

                if (mcurrx == mfinalx && mcurry == mfinaly) { 

                    mfinished = true; 

        else { 

            mcurrx = mfinalx; 

            mcurry = mfinaly; 

            mfinished = true; 

        return true; 

8.view的滑動漸變效果總共有幾種方法?

答:三種,第一種是scroller 也是使用最多的。問題7裡有解釋。還有一種就是動畫,動畫我就不多說了,不屬于本文範疇。最後一種也是我們經常使用的就是用handler ,每隔一個時間間隔 來更新view的狀态。

代碼不寫了很簡單。 自行體會。

9.view的事件傳遞機制 如何用僞代碼來表示?

答:

     * 對于一個root viewgroup來說,如果接受了一個點選事件,那麼首先會調用他的dispatchtouchevent方法。 

     * 如果這個viewgroup的onintercepttouchevent 傳回true,那就代表要攔截這個事件。接下來這個事件就 

     * 給viewgroup自己處理了,進而viewgroup的ontouchevent方法就會被調用。如果如果這個viewgroup的onintercepttouchevent 

     * 傳回false就代表我不攔截這個事件,然後就把這個事件傳遞給自己的子元素,然後子元素的dispatchtouchevent 

     * 就會被調用,就是這樣一個循環直到 事件被處理。 

     * 

     */ 

public boolean dispatchtouchevent(motionevent ev) 

    boolean consume=false; 

    if (onintercepttouchevent(ev)) { 

        consume=ontouchevent(ev); 

    }else 

    { 

        consume=child.dispatchtouchevent(ev); 

    return consume; 

10.view的ontouchevent,onclicklisterner和ontouchlistener的ontouch方法 三者優先級如何?

答:ontouchlistener優先級最高,如果ontouch方法傳回 false ,那ontouchevent就被調用了,傳回true 就不會被調用。至于onclick 優先級最低。

11.點選事件的傳遞順序如何?

答:activity-window-view。從上到下依次傳遞,當然了如果你最低的那個view ontouchevent傳回false 那就說明他不想處理 那就再往上抛,都不處理的話

最終就還是讓activity自己處理了。舉個例子,pm下發一個任務給leader,leader自己不做 給架構師a,小a也不做 給程式員b,b如果做了那就結束了這個任務。

b如果發現自己搞不定,那就找a做,a要是也搞不定 就會不斷向上發起請求,最終可能還是pm做。

//activity的dispatchtouchevent 方法 一開始就是交給window去處理的 

//win的superdispatchtouchevent 傳回true 那就直接結束了 這個函數了。傳回false就意味 

//這事件沒人處理,最終還是給activity的ontouchevent 自己處理 這裡的getwindow 其實就是phonewindow 

 public boolean dispatchtouchevent(motionevent ev) { 

        if (ev.getaction() == motionevent.action_down) { 

            onuserinteraction(); 

        if (getwindow().superdispatchtouchevent(ev)) { 

            return true; 

        return ontouchevent(ev); 

//來看phonewindow的這個函數 直接把事件傳遞給了mdecor 

 @override 

    public boolean superdispatchtouchevent(motionevent event) { 

        return mdecor.superdispatchtouchevent(event); 

//devorview就是 我們的rootview了 就是那個framelayout 我們的setcontentview裡面傳遞的那個layout 

//就是這個decorview的 子view了 

     @override 

    public final view getdecorview() { 

        if (mdecor == null) { 

            installdecor(); 

        return mdecor; 

12.事件分為幾個步驟?

答:down事件開頭,up事件結尾,中間可能會有數目不定的move事件。

13.viewgroup如何對點選事件分發?

viewgroup就是在actionmasked == motionevent.action_down 和 mfirsttouchtarget != null 這兩種情況來判斷是否會進入攔截事件的流程 

看代碼可以知道 如果是action_down事件  那就肯定進入 是否要攔截事件的流程 

如果不是action_down事件 那就要看mfirsttouchtarget != null 這個條件是否成立 

這個地方有點繞但是也好了解,其實就是 對于一個事件序列來說 down是事件的開頭 是以肯定進入了這個事件是否攔截的流程 也就是if 括号内。 

mfirsttouchtarget其實是一個單連結清單結構他指向的是 成功處理事件的子元素。 

也就是說 如果有子元素成功處理了 事件,那這個值就不為null。反過來說 

隻要viewgroup攔截了事件,mfirsttouchtarget就不為null,是以括号内就不會執行,也就側面說明了一個結論: 

某個view 一旦決定攔截事件,那麼這個事件所屬的事件序列 都隻能由他來執行。并且onintercepttouchevent 這個方法不會被調用了 

            final boolean intercepted; 

            if (actionmasked == motionevent.action_down 

                    || mfirsttouchtarget != null) { 

                final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0; 

                if (!disallowintercept) { 

                    intercepted = onintercepttouchevent(ev); 

                    ev.setaction(action); // restore action in case it was changed 

                } else { 

                    intercepted = false; 

            } else { 

                // there are no touch targets and this action is not an initial down 

                // so this view group continues to intercept touches. 

                intercepted = true; 

            }  

14.如果某個view 處理事件的時候 沒有消耗down事件 會有什麼結果?

答:假如一個view,在down事件來的時候 他的ontouchevent傳回false, 那麼這個down事件 所屬的事件序列 就是他後續的move 和up 都不會給他處理了,全部都給他的父view處理。

15.如果view 不消耗move或者up事件 會有什麼結果?

答:那這個事件所屬的事件序列就消失了,父view也不會處理的,最終都給activity 去處理了。

16.viewgroup 預設攔截事件嗎?

答:預設不攔截任何事件,onintercepttouchevent傳回的是false。

17.一旦有事件傳遞給view,view的ontouchevent一定會被調用嗎?

答:是的,因為view 本身沒有onintercepttouchevent方法,是以隻要事件來到view這裡 就一定會走ontouchevent方法。

并且預設都是消耗掉,傳回true的。除非這個view是不可點選的,所謂不可點選就是clickable和longgclikable同時為fale

button的clickable就是true 但是textview是false。

18.enable是否影響view的ontouchevent傳回值?

答:不影響,隻要clickable和longclickable有一個為真,那麼ontouchevent就傳回true。

19.requestdisallowintercepttouchevent 可以在子元素中幹擾父元素的事件分發嗎?如果可以,是全部都可以幹擾嗎?

答:肯定可以,但是down事件幹擾不了。

20.dispatchtouchevent每次都會被調用嗎?

答:是的,onintercepttouchevent則不會。

21.滑動沖突問題如何解決 思路是什麼?

答。要解決滑動沖突 其實最主要的就是有一個核心思想。你到底想在一個事件序列中,讓哪個view 來響應你的滑動?比如 從上到下滑,是哪個view來處理這個事件,從左到右呢?

用業務需求 來想明白以後 剩下的 其實就很好做了。核心的方法 就是2個 外部攔截也就是父親攔截,另外就是内部攔截,也就是子view攔截法。 學會這2種 基本上所有的滑動沖突

都是這2種的變種,而且核心代碼思想都一樣。

外部攔截法:思路就是重寫父容器的onintercepttouchevent即可。子元素一般不需要管。可以很容易了解,因為這和android自身的事件處理機制 邏輯是一模一樣的

@override 

    public boolean onintercepttouchevent(motionevent ev) { 

        boolean intercepted = false; 

        int x = (int) ev.getx(); 

        int y = (int) ev.gety(); 

        switch (ev.getaction()) { 

            //down事件肯定不能攔截 攔截了後面的就收不到了 

            case motionevent.action_down: 

                intercepted = false; 

            case motionevent.action_move: 

                if (你的業務需求) { 

                    //如果确定攔截了 就去自己的ontouchevent裡 處理攔截之後的操作和效果 即可了 

                    intercepted = true; 

            case motionevent.action_up: 

                //up事件 我們一般都是傳回false的 一般父容器都不會攔截他。 因為up是事件的最後一步。這裡傳回true也沒啥意義 

                //唯一的意義就是因為 父元素 up被攔截。導緻子元素 收不到up事件,那子元素 就肯定沒有onclick事件觸發了,這裡的 

                //小細節 要想明白 

            default: 

        return intercepted; 

内部攔截法:内部攔截法稍微複雜一點,就是事件到來的時候,父容器不管,讓子元素自己來決定是否處理。如果消耗了 就最好,沒消耗 自然就轉給父容器處理了。

子元素代碼:

    public boolean dispatchtouchevent(motionevent event) { 

        int x = (int) event.getx(); 

        int y = (int) event.gety(); 

        switch (event.getaction()) { 

                getparent().requestdisallowintercepttouchevent(true); 

                if (如果父容器需要這個點選事件) { 

                    getparent().requestdisallowintercepttouchevent(false); 

                }//否則的話 就交給自己本身view的ontouchevent自動處理了 

        return super.dispatchtouchevent(event); 

父親容器代碼也要修改一下,其實就是保證父親别攔截down:

本文作者:佚名

來源:51cto

繼續閱讀