Android關(guān)于View的那些事兒

很早就入手了《Android開發(fā)藝術(shù)探索》這本書,但是一直塵封著,沒有看過,最近不是很忙,抽出時間,細細研究一番,感覺是對自己開發(fā)知識的梳理,查缺不漏。打算根據(jù)書中內(nèi)容,另外加入自己的一些梳理,整理這一篇文章,對自己是個總結(jié),對他人希望能提供一點點幫助。
這篇文章整體根據(jù)以下目錄展開敘述:

  • View的位置參數(shù);
  • MotionEvent ,TouchSlop ,VelocityTracker,GestureDetector;
  • View的滑動;
  • View的彈性滑動;
  • 事件分發(fā)機制;
  • 滑動沖突解決
  • View的工作原理
  • 自定義View

一:View的位置參數(shù)

1:getLeft(),getTop(),getRight(),getBottom()

View相對于父布局原始的位置參數(shù);View發(fā)生移動,這幾個參數(shù)不發(fā)生變化,僅僅表示View相對父布局原始的位置參數(shù);可以用于當某View發(fā)生了位置移動,需要獲取相對父布局原始位置參數(shù)信息。

2:getX(),getY()

View左上角相對父布局水平方向和豎直方向的位置參數(shù),如果View發(fā)生位置移動,這兩個參數(shù)發(fā)生變化;

3:getTranslationX(),getTranslationY()

View左上角相對父布局的位置偏移量;如果View發(fā)生位置移動,這兩個參數(shù)發(fā)生變化;

4: 三者之間的關(guān)系如下:
width= right- left
height = bottom - top
x = left + translationX
y = top + translationY
5:獲取屏幕尺寸
        //Activity中獲取屏幕尺寸
        DisplayMetrics mDisPlay = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(mDisPlay);
        int screenHeight = mDisPlay.heightPixels;
        int screenWidth=mDisPlay.widthPixels;
        Log.d(TAG, "onWindowFocusChanged: 屏幕尺寸:寬"+screenWidth+"高:"+screenHeight);
        //非Activity(View)中獲取屏幕尺寸
        width=getResources().getDisplayMetrics().widthPixels;
6:獲取應(yīng)用程序App顯示區(qū)域(屏幕尺寸高度-狀態(tài)欄高度)
       //可編輯區(qū)域?qū)捀?        Rect mRect=new Rect();
        getWindow().getDecorView().getWindowVisibleDisplayFrame(mRect);
       
7:獲取布局部分的尺寸(App顯示區(qū)域高度基礎(chǔ)上-標題欄高度)
//View布局區(qū)域?qū)捀叩瘸叽绔@取
Rect rect = new Rect();  
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
備注:

1:5~7是獲取手機相關(guān)區(qū)域尺寸的方法,這幾個方法在onCreate(),onResume()中都無法正常獲取到數(shù)據(jù),建議在onWindowFocusChanged ()中調(diào)用。原因參考文章淺談View的繪制流程
2:獲取屏幕位置信息參考Android應(yīng)用坐標系統(tǒng)全面詳解


二:MotionEvent ,TouchSlop ,VelocityTracker,GestureDetector;

1:MotionEvent包含了用戶按下,滑動,抬起,等一系列動作信息,即在手指接觸屏幕后所產(chǎn)生的一系列事件中,典型的事件類型有如下幾種:
  • ACTION_DOWN一手指剛接觸屏幕
  • ACTION_MOVE一手指在屏幕上移動
  • ACTION_UP —手機從屏幕上松開的一瞬間
    備注:一個MotionEvent中往往包含一個action_down和一個action_up,兩者中間包含無數(shù)個action_move;
    另外補充一下,獲取用戶觸摸點水平和豎直方向位置坐標的方法。區(qū)別查看代碼注釋:
@Override
    public boolean onTouchEvent(MotionEvent event) {
        //表示動作執(zhí)行點相對手機屏幕的絕對位置
        Float screanX= event.getRawX();
        Float screanY= event.getRawY();
        //表示動作執(zhí)行點相對控件自身的相對位置信息
        Float X= event.getX();
        Float Y= event.getY();
        return super.onTouchEvent(event);
    }
2: TouchSlop 表示系統(tǒng)所能識別的最小移動距離,用于判斷用戶滑動距離是否有效;
touchSlop= ViewConfiguration.get(mContext).getScaledTouchSlop();
3:VelocityTracker用于測算用戶水平或者豎直方向的滑動速度;
@Override
    public boolean onTouchEvent(MotionEvent event) {
        /**
         * 作用:根據(jù)水平和豎直方向的滑動速度對比大小得知滑動方向
         */
        //初始化
        VelocityTracker mVelocityTracker=VelocityTracker.obtain();
        //將事件添加進去(可以理解為告訴mVelocityTracker移動距離)
        mVelocityTracker.addMovement(event);
        //設(shè)定測速的單位時間
        mVelocityTracker.computeCurrentVelocity(1000);
        //獲取水平方向的移動速度
        float velocityX = mVelocityTracker.getXVelocity();
        //獲取豎直方向的移動速度
        float velocityY= mVelocityTracker.getYVelocity();
      》》》》//不用之后務(wù)必進行回收,釋放資源   《《《《
        mVelocityTracker.clear();
        mVelocityTracker.recycle();

        return super.onTouchEvent(event);
    }
4:GestureDetector 手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。具體流程如下:
  • 創(chuàng)建一個GestureDetector對象并實現(xiàn)OnGestureListener接口,根據(jù)需要我們還可以實現(xiàn)OnDoubleTapListener從而能夠監(jiān)聽雙擊行為。
//創(chuàng)建對象并實現(xiàn)onGestureListener監(jiān)聽
        GestureDetector mGestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                //手指觸摸屏幕瞬間,由一個ACTION_down觸發(fā)
                //這個地方默認返回false;
                //手動改成返回true,否則其他監(jiān)聽無法觸發(fā)(我測試中是這樣)
                return true;
            }

            @Override
            public void onShowPress(MotionEvent e) {
            }
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
              //單擊事件監(jiān)聽
                return false;
            }
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //滑動監(jiān)聽
                return false;
            }
            @Override
            public void onLongPress(MotionEvent e) {
            //長按監(jiān)聽
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                //飛快滑動監(jiān)聽
                return false;
            }
        });
        //解決長按屏幕后無法拖動的現(xiàn)象
        mGestureDetector.setIsLongpressEnabled(false);
  • 接管onTouchEvent事件:
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int mAction = event.getAction();
        if(mAction==MotionEvent.ACTION_UP){
            startScroll(0,-mScroller.getFinalY());
            return super.onTouchEvent(event);
        }else{
            //接管事件
            return mGestureDetecter.onTouchEvent(event);
        }

    }
  • 根據(jù)具體業(yè)務(wù)需要實現(xiàn)相應(yīng)監(jiān)聽


    a.jpg

三 View的滑動方法

1:View.scrollTo()/View.scrollBy()
  • View.scrollTo();
    基于所傳遞參數(shù)的絕對滑動,即相對View的原始位置進行移動。假如用戶重復調(diào)用該方法,只會重復移動,不會在已經(jīng)移動的距離基礎(chǔ)上,再移動相應(yīng)的距離;
  • View.scrollBy();
    它實現(xiàn)了基于當前位置的相對滑動,也就是,用戶重復調(diào)用該方法,會再已經(jīng)移動的距離基礎(chǔ)上,繼續(xù)移動相應(yīng)距離。
    備注:scrollBy和scrollTo都是只能移動View的內(nèi)容(對于textView 文字就是其內(nèi)容,對于ViewGroup子View就是其內(nèi)容),不能移動View的位置信息。
    另外要重點理解View內(nèi)部的兩個屬性mScrollX和mScrollY的改變規(guī)則,這兩個屬性可以通過getScrollX和getScrollY方法分別得到??梢赃@樣理解:mSrollX表示滑動過程中View左側(cè)邊沿距離View內(nèi)容左邊沿的水平距離,mSrollY表示滑動過程中View上邊沿距離View內(nèi)容上邊沿的豎直距離,兩種隨著左右上下滑動,縮小變大。并且當View左邊緣在Veiw內(nèi)容左邊緣的右邊時,mScrolX為正值,反之為負值;當View上邊緣在View內(nèi)容上邊緣的下邊時,mScrollY為正值,反之為負值。換句話說,如果從左向右滑動,那么mScrollX負值,反之為正值:如果從上往下滑動,那么mScrollY為負值,反之為正值。
    b.jpg
2: 使用動畫實現(xiàn)View的滑動

這個不是這篇文章重點考慮的,所以略過,想看的同學請參考:
補間動畫寫的比較全面
知乎上關(guān)于android動畫的問題,里面有很多源碼解讀;
關(guān)于android屬性動畫的介紹,寫的非常基礎(chǔ)全面;
android關(guān)于屬性動畫的優(yōu)化處理部分

3:改變View的位置參數(shù)實現(xiàn)位置移動

這個比較好理解了,比如我們想把一個Button向右平移100px,我們只需要將這個Bution的LayoutParams里的marginLeft參數(shù)的值增加100px即可,是不是很簡單呢?還有一種情形,view的默認寬度為0,當我們需要向右移動Button時,只需要重新設(shè)置空View的寬度即可,就自動被擠向右邊,即實現(xiàn)了向右平移的效果。如何重新設(shè)置一個View 的LayoutParams呢?很簡單,如下所示:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) testButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
testButton.requestLayout();
//或者testButton.setLayoutParams(layoutParams);

總結(jié)

  • 移動之后需要接受類似點擊事件以及其他位置信息的場景:建議使用屬性動畫或者改變View的位置參數(shù)兩種方法實現(xiàn)位置移動。
  • 移動之后不需要獲取View的位置信息變化或者點擊事件的場景:三種方法都可以使用

四: View的彈性滑動

上面提到的View的滑動三種方法除了動畫外,另外兩種方法滑動都是瞬間完成的,沒有過渡效果,用戶體驗相當不友好。所以這里介紹幾種能實現(xiàn)彈性滑動的方法:
1:借助動畫(忽略,不詳述)
2:使用延時策略(這個方法,我大致看了看,比較麻煩,不想展開詳述,想詳細查看的請自行百度<使用延時策略實現(xiàn)View的彈性滑動>或者查看原著)
3: 借助工具類Scroller實現(xiàn)彈性滑動
這個方法是我想重點展開詳述的,借助Scroller實現(xiàn)彈性滑動的代碼如下:

public MyHeadScroll(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        /**
         * 1 初始化mRcroller
         */
        
        mScroller=new Scroller(context);
    }
    private void startScroll(int x,int y){
        Log.d(TAG, "startScroll: "+y);
        /**
         * 2 設(shè)置滑動距離數(shù)據(jù)
         */
        mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
        /**
         * 3 重繪,會調(diào)用觸發(fā)computeScroll()
         */
        invalidate();

    }
    @Override
    //這個方法是個空實現(xiàn),需要重寫,自己寫滑動邏輯
    public void computeScroll() {
        //判斷滑動是否完整,沒成就繼續(xù)滑動,繼續(xù)重繪,繼續(xù)調(diào)用computeScroll,類似遞歸,知道移動完整
        if(mScroller.computeScrollOffset()){
            /**
             *4  真正滑動的地方,nnd原來也是借助scrollTo或者scrollBy
             */
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
        super.computeScroll();
    }

上面代碼可以說是模板,借助scroller實現(xiàn)彈性滑動基本上都這樣寫(不懂的地方看注釋),大致流程如下:
1:初始化mScroller,并設(shè)置滑動數(shù)據(jù),重繪;
2:手動實現(xiàn)computScroll()方法,自己實現(xiàn)滑動邏輯;
3:切記,兩個重繪的地方,缺一不可;
補充

1:startScroll(int startX, int startY, int dx, int dy, int duration):

指定起點(startX,startY),從起點平滑變化(dx,dy),耗時duration,通常用于:知道起點與需要改變的距離的平滑滾動等。

2:fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY):

慣性滑動。 給定一個初始速度(velocityX,velocityY),該方法內(nèi)部會根據(jù)這個速度去計算需要滑動的距離以及需要耗費的時間。通常用于:界面的慣性滑動等。


關(guān)于View的一些基本知識,包括滑動的實現(xiàn),基本上講完了。另外關(guān)于scrollTo/scrollBy和Scroller的源碼解析和實現(xiàn)原理,我就不展開詳述了,推薦參考:
站在源碼的肩膀上全解Scroller工作機制
看到這里,相信大家對View基礎(chǔ)知識基本掌握了,下面要講的幾個模塊,是View的重點部分,也是進階自定義View所必須要掌握的,喝口茶,咱們繼續(xù)聊。


五 事件分發(fā)機制

1:概述

所謂的事件分發(fā)其實就是對MotionEvent事件的分發(fā)過程。即當一個MotionEvent事件產(chǎn)生之后,系統(tǒng)需要將這個事件傳遞給一個具體的View進行處理相應(yīng)事件,這個傳遞的過程就是事件分發(fā)的過程。

2:涉及到的主要方法

事件分發(fā)的過程是由三個很重要的方法來完成的,這三個方法分別是:

  • public boolean dispatchTouchEvent(MotionEvent ev)
是事件分發(fā)的開始,返回值代表是否將事件繼續(xù)分發(fā)下去;
用來進行事件的分發(fā)。如果事件能夠傳遞給當前View,那么此方法一定會被調(diào)用,
返回結(jié)果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
  • public boolean onInterceptTouchEvent(MotionEvent ev)
這個方法是ViewGroup所獨有的方法,表示父控件是否攔截事件,返回值也代表當前父控件是否對事件進行攔截。
如果攔截,就交由該父控件的onTouchEvent()方法消耗處理事件;
如果不攔截,就交由子布局的dispatchTouchEvent(),繼續(xù)傳遞事件;
在上述dispatchTouchEvent(MotionEvent ev)方法內(nèi)部調(diào)用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,
那么在同一個事件序列當中,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當前事件
  • public boolean onTouchEvent(MotionEvent event)
表示是否消耗處理事件。
如果不處理,事件就交由上一級父控件的onTouchEvent進行處理;
如果處理,那么MotionEvent中一系列的其他事件都交由當前控件進行處理;
在dispatchTouchEvent方法中調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗當
前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。

三個方法之間的關(guān)系可以用一段偽代碼來表示

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

3:事件傳遞流程(見圖)

事件分發(fā)機制.png

c.png

整個一個事件分發(fā)的流程,我直接扔上兩張圖,感覺這樣不光是對讀者,更是對自己的不負責,下面我就根據(jù)最近看的書,理解到的東西,純白話試著寫寫這個流程:

1:研究對象 dispatchTouchEvent()

用戶點擊屏幕,MotionEvent.Action_Down事件產(chǎn)生,開始所謂的事件傳遞。最開始觸發(fā)的是Activity的dispatchTouchEvent()方法。如果手動重寫dispatchTouchEvent():

1.1 修改 返回false/true,無法調(diào)用下面方法, 事件就在這里消耗,不往下傳遞
1.2 返回super ,正常調(diào)用下面 方法,事件開始傳遞;
查看源碼得知,返回super,調(diào)用源碼中以下方法:

   public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          //做屏保功能的時候可能用到
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

繼續(xù): 該方法中會調(diào)用getWindow().superDispatchTouchEvent(ev)。根據(jù)發(fā)現(xiàn):getWindow()獲取的window對象,查閱源碼得知,PhoneWindow是window唯一實現(xiàn)類,所以直接查看PhoneWindow類中的superDispatchTouchEvent(ev)方法:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

繼續(xù) mDecor是DecorView的對象,代表可編輯布局區(qū)域的最外層布局,實際是FrameLayout,也就是一個ViewGroup,代碼如下:

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

繼續(xù) 跟進去查看,就直接進入ViewGroup的dispatchTouchEvent()方法了,這里源碼不貼了(本來想白話說這個過程的,結(jié)果為了說清楚過程還是貼出部分源碼,請原諒我的無能)。到這里,事件已經(jīng)由activity傳遞到了ViewGroup.dispatchTouchEvent。

2:研究對象ViewGroup.dispatchTouchEvent

2.1:開發(fā)者不重寫該方法,類似下面2.2.3:
2.2:開發(fā)者重寫該方法,手動改動返回結(jié)果,會出現(xiàn)以下幾種可能:
2.2.1: 如果修改這個方法返回false,等同于貼出的第一段源碼中的,getWindow().superDispatchTouchEvent(ev)返回false,那這樣直接調(diào)用activity.onTouchEvent(ev),方法消耗事件;
2.2.2: 如果修改這個方法返回true,就不用調(diào)用activity.onTouchEvent(ev)方法,事件直接在這里消耗了;
2.2.3: 如果不修改直接返回super,其實是走進源碼中,下面的偽代碼可以說明源碼這一塊的邏輯:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //會詢問是否攔截當前事件,如果攔截
        if(onInterceptTouchEvent(ev)){
            //r如果攔截,就調(diào)用自己的onTouchEvent();
            return onTouchEvent(ev);
        }else{
            //如果不攔截
            /**
             * 1:對子View進行遍歷,查找能夠處理事件的View
             * 2;調(diào)用該子View的dispatchTouchEvent方法;
             * 由此,事件傳遞到了View的dispatchTouchEvent
             * 如果View.dispatchTouchEvent()返回true,事件直接消耗
             * 如果返回false,直接調(diào)用父控件的onTouchEvent()方法消耗處理事件
             */
            if(View.dispatchTouchEvent()){
                return true;
            }else{
                return onTouchEvent(ev);
            }

        }
        
    }

由此,事件的傳遞已經(jīng)從ViewGroup傳遞到了View.dispatchTouchEvent();

3:研究對象 View.dispatchTouchEvent方法;

3.1:如果開發(fā)者不重寫該方法,類似下面3.2.3;
3.2:如果開發(fā)者手動重寫該方法:
3.2.1:修改返回值為true,事件到此傳遞結(jié)束,直接消耗;
3.2.2:修改返回值為false;查看上面?zhèn)未a得知,事件直接交付給父控件的onTouchEvent()進行消耗;
3.2.3:不修改,直接返回super;還是得進源碼看看去:

    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

源碼很多,我刪除了一些無關(guān)緊要,留下關(guān)于這塊邏輯的部分:
如果子View有設(shè)置過onTouchListener監(jiān)聽,并且監(jiān)聽中onTouch(event)返回true,就不會調(diào)用onTouchEvent(ev)方法;由此可以得出一條結(jié)論:回調(diào)順序是先是:onTouch,再是onTouchEvent();也就是setOnTouchListener()方法優(yōu)先級高于onTouchEvent()方法,這樣設(shè)計的目的是有利于外界監(jiān)聽用戶點擊滑動事件;
延伸一點對于優(yōu)先級來說:
onTouchListener()<<onTouch()<<onTouchEvent()<<performClick()<<onClickListener

4:研究對象onTouchEvent();

關(guān)于onTouchEvent()這個方法就不貼代碼了
4.1 每個View的onTouchEvent()方法都是默認返回true的,即默認消耗事件的,除非View的clickble()longClickble()同時為false(enable屬性不影響View是否消耗事件)。對于不同的View的longClickble()默認都是false的,對于clickble()屬性要區(qū)別不同的View來說,對于可以點擊的View類似Bottom的clickble()默認為true;不可點擊的View類似TextView的clickble()屬性默認為false。(通常我們設(shè)置onClickListener和onLongClickListener都是默認開啟對應(yīng)屬性的);
4.2 onTouchEvent()方法中在Action_Up中會調(diào)用performClick(),performClick()中會調(diào)用日常開發(fā)中用的最多的setOnClickListener()。簡單來說:自己重寫onTouchEvent方法之后,注意對performClick()的處理,否則點擊事件監(jiān)聽無效;
事件分發(fā)大致流程基本上分析完了,建議讀者自己對比流程圖,對比上述過程,最后再加上源碼,自己好好理解。感覺上述過程寫的不是很清楚,雖然整個事件分發(fā)機制,自己大致流程弄清楚了,但是想寫出來。寫清楚,還是不容易的。
小結(jié)
1:某個控件一旦處理了MotionEvent中的Action_down事件之后,那么后續(xù)的事件(Action_move/Action_up)正常來說,都是交由該控件處理,如果該控件是ViewGroup的話,onInterceptTouchEvent()只會調(diào)用一次,不會重復調(diào)用(一旦處理事件之后,后續(xù)不會再調(diào)用)因此對于ViewGroup,不建議在onInterceptTouchEvent()方法中處理過多邏輯(特別是處理Action_move/Action_up事件),因為不是每次都調(diào)用
2:事件傳遞過程是由外到內(nèi)的。簡單理解就是:事件總是先傳遞給父元素,然后再由父元素分發(fā)給子View,通過requestDisallowInterptTouchEvent方法可以再子元素中干預(yù)元素的事件分發(fā)過程,但是ACTION_DOWN除外;


六 滑動沖突解決

1:常見滑動沖突

  • 滑動方向不一致造成的沖突(ViewPager中嵌套ListView)
  • 滑動方向一致造成的沖突(豎直方向的scrollView嵌套多個豎直方向的ListView)
  • 上述兩種的綜合

2:處理原則

  • 對于滑動方向不一致造成的沖突,通常解決辦法是通過代碼判斷用戶滑動方向(通過水平和豎直方向的滑動距離大小比較判斷滑動方向),如果是左右滑動,就交給ViewPager,如果是上下滑動就交給ListView;
  • 對于滑動方向一致造成的沖突,這個場景只能根據(jù)業(yè)務(wù)邏輯進行活動沖突的解決。

3:解決辦法

3.1:外部攔截

結(jié)合上一節(jié)的事件分發(fā)機制可知,通過重寫父控件的onInterceptTouchEvent()方法,可以攔截事件繼續(xù)傳遞。也就是:需要外層控件滑動的時候,進行攔截,需要內(nèi)層控件滑動的時候,不進行攔截。標準的偽代碼(看注釋)如下:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
              //這里一定要返回false,如果返回true,事件就不會繼續(xù)
              //傳遞,onInterceptTouchEvent也不會重復調(diào)用   
              //父容器必須返回false,即不攔截ACTION_DOWN事件,
               //這是因為一旦父容器攔截了ACTION_DOWN,
                 //那么后續(xù)的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,                                                    
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if("父容器的點擊/滑動事件"){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
//最后是ACTION_UP事件,這里必須要返回false,
//因為ACTION_UP事件本身沒有太多意義考慮一種情況,假設(shè)事件交由子元素處理,如果父容器在ACTION_UP時返回了true,
//會導致子元素無法接收到ACTION_UP事件,這個時候子元素中的onClick事件就無法觸發(fā),
                intercepted = false;
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = x;
        return intercepted;
    }
3.2: 內(nèi)部攔截

父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素要消耗此事件就直接消耗掉,否則就交由父容器進行處理,這種方法和Android中的事件分發(fā)機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來較外部攔截法稍顯復雜。偽代碼如下,我們需要重寫子元素的dispatchTouchEvent方法:

@Override
    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 =  x - mLastY;
                if("父容器的點擊/滑動事件"){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

除了子元素需要處理之外,父元素默認也要攔截除ACTION_DOWN之外的其他事件,這樣當子元素調(diào)用getParent().requestDisallowInterceptTouchEvent(true)方法時,父元素才能繼續(xù)攔截所需要的事件

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            return false;
        }else {
            return true;
        }
    }

小結(jié)
兩種方法對比,都能解決問題,但是第一種方法簡單明了,所以。。。你懂的。。。


七 View的工作原理

1:初識ViewRoot和DecorView

  • ViewRoot對應(yīng)于ViewRootImpl類,他是連接WindowManager和DecorView的紐帶,View的三大流程都是通過ViewRoot來完成的。
  • 頂級View DecorView,一般情況下他內(nèi)部會包含一個豎直方向的LinearLayout,這里面有上下兩部分,上面是標題欄,下面是內(nèi)容,在Activity中,我們可用通過setContentView設(shè)置布局文件就是放在內(nèi)容里,而內(nèi)容欄的id為content,因此我們可以理解為實際上是在setView,那如何得到content呢?你可以ViewGroup content = findviewbyid(android.R.id.content),如何得到我們設(shè)置的View呢:content.getChildAt(0),同時,通過源碼我們可用知道,DeaorView其實就是一個FrameLayout,View層事件都先經(jīng)過DecorView,然后傳遞給View。
  • 在ActivityThread中,當Activity被創(chuàng)建完畢后,會將DecorView添加到Window中,同時會創(chuàng)建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立聯(lián)系。從而完成DecorView的三大繪制流程。
  • View的繪制流程從ViewRoot的perfromTraversals方法開始,他經(jīng)過measurelayoutdraw三個過程才能將View畫出來,,其中measure測量,layout確定view在容器的位置,draw開始繪制在屏幕上,perfromTraversals的大致流程,可以看圖
    1539225875(1).png

2:理解MeasureSpec

  • MeasureSpec代表一個32位int值,高兩位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某個測量模式下的規(guī)格大小。

  • SpecMode有三種分別是:
    EXACTLY:父容器已經(jīng)檢測出View所需要的精度大小,這個時候View的最終大小就是SpecSize所指定的值,它對應(yīng)于LayoutParams中的match_parent,和具體的數(shù)值這兩種模式;
    AT_MOST:父容器指定了一個可用大小,即SpecSize,view的大小不能大于這個值,具體是什么值要看不同view的具體實現(xiàn),它對應(yīng)于LayoutParams中wrap_content;
    UNSPECIFIED:父容器不對View有任何的限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)

  • 子View的尺寸寬高大小是由自己的MeasureSpec直接決定的,但是子View的MeasureSpec形成過程是子View的LayoutParams和父View的限制共同決定的.也就是說:在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的MeasureSpec。

3:View的工作流程

View的工作流程主要是指measure、layout、draw這三大流程,即測量、布局和繪制,其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draww則將View繪制到屏幕上

3.1:Measure過程

3.1.1:View的Measure
  • View 的 measure過程由其measure方法來完成,measure方法是一個final類型的方法,這就意味著子類不能重寫此方法,在View的measure方法中去調(diào)用View的onMesure方法,因此只需要看onMeasure的實現(xiàn)即可,View的onMesure方法如下所示:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
                getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  • setMeasuredDimension會設(shè)置View寬/高的測量值,因此我們只需要看getDefaultSize方法(看注釋)即可。
* 
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
//這里兩種模式并成一種模式進行處理,可以聯(lián)想到,如果自定View,
//使用wrap_content屬性,即case MeasureSpec.AT_MOST,
//會按照case MeasureSpec.EXACTLY,進行處理,這往往不是我們想要的
//所以自定義View直接繼承View,針對wrap_content,我們需要自己進行處理,這個文章最后補充中會提到;
//突然想問一句:既然如此源碼為啥要這樣寫呢???????
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

  • 看AT_MOST和EXACTLY這兩種情況,簡單的理解,其實getDefaultSize返回的大小就是mesourSpec中的specSize,而這個specSize就是view的大小,這里多次提到測量后的大小,是因為View最終的大小,是在layout階段的,所以這里必須要加以區(qū)分,但是幾乎所有情況下的View的測量大小和最終大小是相等的。 至于UNSPECIFIED這種情況,一般用于系統(tǒng)內(nèi)部的測量過程,在這種情況下,View的大小為getDefaultSize的第一個參數(shù)是size,即寬高分別為getSuggestedMinimumWidth和getSuggestedMinimumHeight()這兩個方法的返回值:
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }
  • 簡單來說就是:看控件是否設(shè)置背景屬性,如果設(shè)置了背景屬性,比較控件寬高最小值mMinWidth(沒有設(shè)置的話,默認為0)和背景屬性(Drawable中圖片有寬高,shapeDrawable無寬高,這里不詳細展開,請自行百度)寬高誰大就返回誰;如果沒有設(shè)置背景屬性,就直接返回寬高的最小值mMinWidth;
總結(jié)
  • onMeasure中View測量自己的高度;
  • 方法調(diào)用流程:measure()>>>onMeasure()>>>setMeasureDimension()>>>getDefaultSizi()>>>getSuggestedMinimumWidth();
3.1.2:ViewGroup的Measure

對于ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去調(diào)用所有子元素的measure方法,各個子元素再重復去執(zhí)行3.1.1過程。和View不同的是,ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法,但是它提供了一個叫measureChildren:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
//遍歷對每個子View進行測量
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //這個方法在自定義ViewGroup中使用到
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
      //在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的
      //MeasureSpec。
//這兩個過程就是獲取子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
    //拿到子View的MeasureSpec,調(diào)用子View的測量方法。之后就重復3.1.1了
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

ViewGroup并沒有定義其測量的具體過程,這是因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去具體實現(xiàn)。那是因為不同的ViewGroup子類有不同的布局特性,這導致它們的測量細節(jié)各不相同.這里就不舉例子詳細說明了,總結(jié)以下大致流程:

  • ViewGroup遍歷所有的子View,獲取各子View的MeasureSpec,調(diào)用子View的測量Measure()方法進行測量;
  • 根據(jù)自個ViewGroup各自的特性綜合各自子View的測量結(jié)果,測量自身的寬高尺寸;

3.2:layout過程

Layout的作用是ViewGroup用來確定自己的位置,當ViewGroup的位置被確認之后,他的layout就會去遍歷所有子元素并且調(diào)用onLayout方法,在layout方法中onLayou又被調(diào)用,layout的過程和measure過程相比就要簡單很多了,layout方法確定了View本身的位置,而onLayout方法則會確定所有子元素的位置,先看View的layout方法

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
layout的方法的大致流程如下

首先會通過一個setFrame方法來設(shè)定父View的四個頂點的位置,即初始化mLeft,mTop,mRight,mBottom這四個值,View的四個頂點一旦確定,那么父View在其父容器的位置也就確定了,接下來會調(diào)用onLayout方法,這個方法的用途是父View確定其子元素的位置。

3.3:draw過程

3.3.1:draw是比較簡單的,他的作用是將View繪制到屏幕上面,View的繪制過程由如下幾個步驟

  • 繪制背景
  • 繪制自己
  • 繪制children
  • 繪制裝飾
    3.3.2:setwilINotDraw()
    作用:決定onDraw()方法是否會執(zhí)行。
    無論對于View還是ViewGroup而言,對于自定義而言,需要調(diào)用onDraw()方法進行繪制東西的話,就要在構(gòu)造方法中明確設(shè)置setwilINotDraw(false);不需要調(diào)用onDraw()方法的話,也要在構(gòu)造方法中明確設(shè)置setwilINotDraw(true),系統(tǒng)會進行相應(yīng)優(yōu)化。

小結(jié)

到這里View的工作原理三大流程measure,layout,draw.基本上分析完了,下面補充一點東西,日常開發(fā)中,獲取控件寬高的方法。

4:獲取控件寬高的方法

4.1: Activity/View#onWindowFocusChanged

View已經(jīng)初始化完畢了,寬/高已經(jīng)準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調(diào)用多次,當Activity的窗口得到焦點和失去焦點時均會被調(diào)用一次。具體來說,當Activity繼續(xù)執(zhí)行和暫停執(zhí)行時,onWindowFocusChanged均會被調(diào)用,如果頻繁地進行onResume和onPause,那么onWindowFocusChanged也會被頻繁地調(diào)用,典型代碼如下:

@Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        //獲取屏幕尺寸
        DisplayMetrics mDisPlay = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(mDisPlay);
        int screenHeight = mDisPlay.heightPixels;
        int screenWidth=mDisPlay.widthPixels;
        Log.d(TAG, "onWindowFocusChanged: 屏幕尺寸:寬"+screenWidth+"高:"+screenHeight);
    }
4.2: view.post(runnable)

通過post可以將一個runnable投遞到消息隊列,然后等到Lopper調(diào)用runnable的時候,View也就初始化好了,典型代碼如下:

@Override
    protected void onStart() {
        super.onStart();

        mTextView.post(new Runnable() {
            @Override
            public void run() {
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }
4.3: ViewTreeObserver

使用ViewTreeObserver的眾多回調(diào)可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View的可見性發(fā)生改變,onGlobalLayout方法就會回調(diào),因此這是獲取View的寬高一個很好的例子,需要注意的是,伴隨著View樹狀態(tài)的改變,這個方法也會被調(diào)用多次,典型代碼如下

@Override
    protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = mTextView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mTextView.getMeasuredWidth();
                int height = mTextView.getMeasuredHeight();
            }
        });
    }
4.4: view.measure(int widthMeasureSpec , int heightMeasureSpec)

通過手動測量View的寬高,這種方法比較復雜,這里要分情況來處理,根據(jù)View的LayoutParams來處理

  • match_parent
    直接放棄,無法測量出具體的寬高,根據(jù)View的測量過程,構(gòu)造這種measureSpec需要知道parentSize,即父容器的剩下空間,而這個時候我們無法知道parentSize的大小,所以理論上我們不可能測量出View的大小
  • 比如寬高都是100dp,那我們可以這樣:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        mTextView.measure(widthMeasureSpec,heightMeasureSpec);
  • wrap_content
    可以這樣測量:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        mTextView.measure(widthMeasureSpec,heightMeasureSpec);

八:自定義View

1:自定義View須知:

  • 讓View支持wrap_content
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (eightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }
  • 如果有有必要,讓你的View支持padding
    這是因為如果你不處理下的話,那么該屬性是不會生效的,在ViewGroup也是一樣
  • 盡量不要在View中使用Handler
    View本身就有一系列的post方法,建議使用這View.post()代替Handler;
  • View中如果有線程或者動畫,需要及時停止,參考View#onDetachedFromWindow
    不停止這個線程或者動畫,容易導致內(nèi)存溢出的,所以你要在一個合適的機會銷毀這些資源,在Activity有生命周期,而在View中,當View被remove的時候,onDetachedFromWindow會被調(diào)用,,和此方法對應(yīng)的是onAttachedToWindow
  • View帶有滑動嵌套時,需要處理好滑動沖突

2: 自定義View例子

自己手動擼了一遍書中自定義View的例子,將自己的理解加入到注釋中。有助于理解代碼。

public class MyViewPagerX extends ViewGroup {
    private Scroller mScroller;
    private VelocityTracker mTracker;
    private Context context;
    private  int mLastX;
    private int mLastY;
    private int currentIndex;
    public MyViewPagerX(Context context) {
        super(context);
        initView();
        this.context=context;
    }

    public MyViewPagerX(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
        this.context=context;
    }

    public MyViewPagerX(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
        this.context=context;
    }
   private void initView(){
        if(mScroller==null){
            mScroller=new Scroller(context);
            mTracker=VelocityTracker.obtain();
        }


    }

    /**
     * 測量四步部曲:
     * 1:獲取父容器寬高測量值以及測量模式
     * 2:拿著父容器的測量widthMeasureSpec,heightMeasureSpec測量子View:measureChildren(widthMeasureSpec,heightMeasureSpec);
     * 3;支持wrap_content
     * 4:測量自己:setMeasuredDimension(measureWidth,measureHeight);
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int measureWidthMode=MeasureSpec.getMode(widthMeasureSpec);
        int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
        int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
        int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
        int childCount=getChildCount();
    //測量子View
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        //支持wrap_content
        View childView = getChildAt(0);
        if(childCount==0){
            setMeasuredDimension(0,0);
        }else  if(measureWidthMode==MeasureSpec.AT_MOST&&measureHeightMode==MeasureSpec.AT_MOST){
            measureWidth=childView.getMeasuredWidth()*childCount;
            measureHeight=childView.getMeasuredHeight();
        }else if(measureHeightMode==MeasureSpec.AT_MOST){
            measureHeight=childView.getMeasuredHeight();
        }else if(measureWidthMode==MeasureSpec.AT_MOST){
            measureWidth=childView.getMeasuredWidth()*childCount;
        }
        //測量自己
        setMeasuredDimension(measureWidth,measureHeight);
    }

    /**
     * 布局:layout確定自己的位置
     * onLayout確定子View的位置
     * 1:遍歷子View,分別獲取各個子View的left,right,top,bottom等值
     * 2:進行布局:childView.layout();
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
       if(changed){
           int childCount = getChildCount();
           for (int i = 0; i <childCount ; i++) {
               View childView=getChildAt(i);
               //嚴謹一點判斷一下當前View有沒有被Gone掉
               if(childView.getVisibility()!=GONE){
                   int childWidth = childView.getMeasuredWidth();
                   int childHeight=childView.getMeasuredHeight();
                   childView.layout(childWidth*(i-1),0,childWidth+i,childHeight);
               }

           }
       }

    }

    /**
     * 借助scroller彈性滑動方法:
     * 1:初始化scroller對象mScroller;
     * 2:設(shè)置滑動距離:mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
     * 3:重繪
     * 4:重寫computeScroll(),自己實現(xiàn)滑動邏輯
     * 5:重繪
     * @param x
     * @param y
     */
    private void startSmoothTo(int x,int y){
       mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),x,y);
       invalidate();

    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }

    }

    @Override
    protected void onDetachedFromWindow() {
        mTracker.recycle();
        super.onDetachedFromWindow();
    }

    @Override
    protected void onAttachedToWindow() {

        super.onAttachedToWindow();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted=false;
        int x= (int) ev.getX();
        int y= (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                isIntercepted=false;

                if(!mScroller.isFinished()){
                    //如果沒有滑動結(jié)束比方說:處理用戶手指快速滑動并離開屏幕然后點擊屏幕也就是
                    // “快速滑動”這一過程未結(jié)束,重新進來ACTION_DOWN事件對其進行攔截
                    mScroller.abortAnimation();
                    isIntercepted=true;
                }
                break;
            case MotionEvent.ACTION_UP:
                //如果這里返回true的話,內(nèi)部控件無法接收ACTION_UP事件
                //考慮perfomOnClick()在Action_up中執(zhí)行,所以
                //內(nèi)部控件無法相應(yīng)onClick事件
                isIntercepted=false;

                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                if(Math.abs(x-mLastX)>Math.abs(y-mLastY)){
                    isIntercepted=true;
                }else{
                    isIntercepted=false;
                }
                break;
        }
        mLastY=y;
        mLastX=x;
        return isIntercepted;
    }

    /**1:ACTION_DOWN中:不做任何處理,見注釋
     * 思考:主要是action_move和action_up中的邏輯:
     * action_up中等同于滑動動作結(jié)束,需要判斷當前滑動距離應(yīng)該顯示哪個頁面
     * action_move中不需要考慮那么多,滑動沒結(jié)束,繼續(xù)滑動就行
     * 
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x= (int) event.getX();
        int y= (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //如果沒有滑動結(jié)束比方說:處理用戶手指快速滑動并離開屏幕然后點擊屏幕也就是
                // “快速滑動”這一過程未結(jié)束,重新進來ACTION_DOWN事件:不做任何處理
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }

                break;
            case MotionEvent.ACTION_UP:
                int mScrollX = getScrollX();
                mTracker.addMovement(event);
                mTracker.computeCurrentVelocity(1000);
                float mXVelocity = mTracker.getXVelocity();
                //如果滑動速度大于50:
                       //滑動速度>0證明從左向右滑動,currentIndex應(yīng)該-1;
                        //滑動速度<0證明從右向左滑動,currentIndex應(yīng)該+1;
                //如果滑動速度小于50
                        //自己舉例子試試,說不明白;

               // 然后 對currentIndex 進行大于0處理;
                //獲取滑動差距,繼續(xù)代碼滑動
                //特別說明getScrollX()表示:控件左邊距到控件內(nèi)容左邊距的偏移距離
                if(Math.abs(mXVelocity)>50){
                    currentIndex=mXVelocity>0?currentIndex-1:currentIndex+1;
                }else{
                    currentIndex=(mScrollX+getWidth()/2)/getWidth();
                }
                currentIndex=Math.max(0,Math.min(currentIndex,getChildCount()-1));
                startSmoothTo(currentIndex*getWidth()-mScrollX,0);
                mTracker.clear();
                break;
            case MotionEvent.ACTION_MOVE:
                //考慮scrollBy和scrollTo滑動方向和預(yù)期相反,所以是mLastX-x,不是x-mLastX;
                scrollBy(mLastX-x,0);
                break;

        }
        mLastX=x;
        mLastY=y;
        return true;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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