Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> view基礎知識介紹(二)

view基礎知識介紹(二)

編輯:關於Android編程

View的滑動

View的滑動可以通過三種方式來實現:

通過view本身提供的scrollTo和scrollBy方法 通過動畫施加平移效果來實現

通過改變view的LayoutParams使得view重新布局來實現

scrollTo/scrollBy

①. 通過查看view的源碼 我們可以發現 scrollBy方法其實也是調用了scrollTo方法來實現的

scrollTo方法是基於所傳遞參數的絕對位置滑動 而scrollBy是根據所傳遞參數基於當前未知的滑動

通過源碼可知 這兩個方法只能改變view內容的位置 而不能改變view在布局中的位置

②.使用動畫

如果使用動畫來進行view的滑動 需要注意的是 動畫是對view的影像所做的操作 並沒有真正改變view的參數 如果希望動畫過後的狀態還可以保留的話 需要將fillAfter屬性設置為true 否則動畫結束之後影像會消失

使用動畫對view影像進行操作的話 會帶來一個嚴重的問題 那就是view的影像無法響應onClick事件

而view的真實位置依然可以響應onClick事件

針對以上問題 解決方案為 我們可以在新位置蔚縣創建一個和目標view一摸一樣的view 連點擊事件也相同

在view完成平移動畫之後 將目標view隱藏 將我們預先創建好的view顯示出來

③.改變布局參數

通過改變目標wiew的margin屬性 可以使得view進行位移操作 或者我們可以在目標view的旁邊設置一個寬高為0的view 在需要位移的時候更改這個view的寬度 自然可以達到 移動目標view的目的

三種方式的對比

scrollTo/scrollBy

操作簡單 適合對view內容的滑動

是view提供的原生的實現滑動效果並且不影響內部點擊事件的方法 但他只能滑動view內容 不能滑動view本身

動畫

操作簡單 主要適用於對外界沒有交互的view和實現復雜的動畫效果

改變布局參數

操作略微復雜 適合於有交互的view

彈性滑動

Scroller 彈性滑動對象

用以實現view的彈性滑動 當我們使用view的scrollTo和scrollBy操作時 view的滑動是瞬間完成的 體驗不好 所以我們需要使用scroller對象來實現view的彈性滑動操作 scroller本身無法實現view的彈性滑動 需要和 view的computeScroll方法配合使用

代碼如下

    /**
     * 自定義滑動View
     * 注意 該方式時機調用的還是view的scrollTo方法 滑動的只是view的內容 並不會改變view的實際位置
     */
    public class ScrollerView extends View{

        private Context mContext;

        //滑動總時間 默認為1000ms 可以設置
        private int mDuration = 1000;

        //構造一個scroller對象
        private Scroller mScroller = new Scroller(mContext);

        public ScrollerView(Context context) {
            super(context);
            mContext = context;
        }

        /**
         * 緩慢的滑動到某個位置
         * @param destX 要滑動到位置的X坐標
         * @param destY 要滑動到位置的Y坐標
         */
        public void smoothScrollTo(int destX,int destY){
            //獲取view當前的位置
            int scrollX = getScrollX();
            //計算view需要滑動的距離
            int deltaX = destX - scrollX;
            int scrollY = getScrollY();
            int deltaY = destY - scrollY;
            //在指定時間內滑向destX
            //通過讀取該方法的源碼 我們可以發現 在該方法中只是將我們設置的數據進行了記錄 並沒有實質性的對view進行滑動
            mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,mDuration);
            //重點在這裡 調用該方法 可以導致view的重繪 當view重繪時會調用draw方法 在draw方法中 又會調用computeScroll方法
            //computeScroll方法在view中是一個空實現 在這裡我們對其進行了重寫
            invalidate();
        }

        /**
         * view 中的方法 在view的Draw方法中調用 但在view中是一個空實現 具體代碼需要我們自己來補充
         *
         * scroller需要配合該方法才能實現彈性滑動 具體原因如下
         * 我們在開始滑動的時候調用了invalidate方法 該方法會導致view重繪 view重繪時draw方法會調用computeScroll方法
         * 由於我們對其進行了重寫 調用我們這裡的代碼 在這裡我們又向mScroller請求view當前的具體位置 通過scrollTo方法對其進行設置
         * 接著又調用postInvalidate方法 再次導致view重繪 以此類推
         *
         */
        @Override
        public void computeScroll() {
            //判斷條件中的方法 通過閱讀源碼可知 其根據總執行時間以及當前時間 來確定滑動是否已經結束 如果還沒有結束
            //則返回true 同時 計算view當前應該滑動到的位置 當滑動沒有結束時 我們去請求view當前應該滑動到的位置
            //利用scrollTo方法對其進行設置 同時調用postInvalidate方法 使view重繪
            if(mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                postInvalidate();
            }
        }

        /**
         * 設置view彈性滑動的總時間
         * @param duration view彈性滑動的時間
         */
        public void setmDuration(int duration){
            mDuration = duration;
        }

    }

動畫

動畫可以很輕松的實現彈性滑動的效果 其原理類似於Scroller對象 同時 在動畫中 我們可以通過設置addUpdateListener

加上一些我們想要進行的其他操作

使用延時策略

使用延時策略的核心點依然是使用view的scrollTo方法 延時的具體實現可以通過handler或者view的postDelay Thread的sleep等

view的事件分發機制

點擊事件的傳遞規則

點擊事件的傳遞其實就是對MotionEvent事件的分發過程 該事件的分發過程主要由以下三個方法共同來完成

public boolean dispatchTouchEvent(MotionEvent ev)

該方法用來進行事件的分發 如果事件能夠傳遞到當前view 那麼此方法 一定會被調用

返回結果受當前view的onTouchEvent方法和下級view的dispatchTouchEvent方法的影響 表示是否能消耗當前事件

public boolean onInterceptTouchEvent(MotionEvent ev)

該方法在dispatchTouchEvent方法內部調用 用來判斷當前view是都攔截某個事件 如果當前view攔截了某個事件

那麼在同一事件序列中 此方法不會被再次調用 返回結果表示是否攔截當前事件

public boolean onTouchEvent(MotionEvent ev)

在dispatchTouchEvent方法中調用 用來處理點擊事件 返回結果表示是否消耗當前事件 如果不消耗 則在同一事件序列中

當前view無法再次接受到事件

上述三個方法之間的關系可以用以下偽代碼來表示


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

對於一個根viewGrounp來說 點擊事件產生之後 首先會傳遞給他 這時他的dispatchTouchEvent就會被調用 如果這個

viewGrounp的onInterceptTouchEvent方法返回true 就表示這個viewGrounp要攔截當前事件 那麼此時當前事件

就會被交給viewGrounp的onTouchEvent方法來處理 如果這個viewGrounp的onInterceptTouchEvent方法返回false

表示沒有攔截這個事件 那麼這個事件將會傳遞給viewGrounp的子元素來處理 接著子元素的dispatchTouchEvent方法就會被調用 如此反復 直到事件最終被處理為止。

當一個view需要處理事件時 如果他設置了onTouchListener 那麼onTouchListener會被優先調用 調用onTouchListener

中的onTouch方法 這時事件的處理結果要看onTouch的返回結果 如果onTouch返回false 那麼此時view的onTouchEvent方法才會被調用 用來處理該事件 當onTouchEvent處理事件時 如果此時我們設置了onClickListener的話 onClickListener會被調用 從這裡我們可以看出來 平時我們經常使用的onClickListener優先級最低 處於事件傳遞機制的末端

這幾個方法的優先級高低如下所示

onTouchListener–>onTouchEvent–>onClickListener

當一個點擊事件產生之後 他的傳遞過程遵循如下規則

Activity–>window–>view

事件總是先傳遞給Activity Activity再傳遞給window 最後window再分發給頂級view 頂級view接收到事件後 就會按照

事件的分發機制去分發事件 此時如果末端的view在onTouchEvent中返回false 那麼它的父容器的onTouchEvent方法將會被調用 以此類推 如果所有的元素都沒有處理該事件 那麼該事件最終會被返回給Activity處理 即Activity的onTouchEvent方法

將會被調用 這個過程就類似於日常生活中任務的委派過程一樣 上級委派下級去處理 下級委派給更下級去處理 如果下級不能處理該事件那麼該事件最終還是要返回給上級去處理的

關於事件傳遞機制 這裡給出一些結論 根據這些結論可以更好地理解事件傳遞機制 如下所示

同一個事件序列 指的是從手指解除屏幕的那一刻開始 到手指離開屏幕結束 中間產生的一系列事件 這個事件序列以down事件開始 以up事件結束 中間包含數量不等的move事件

正常情況下 一個事件序列只能被一個view攔截並消耗 因為一個元素一旦攔截了這一事件 那麼同一事件序列中的所有事件都會直接交給該元素來處理 因此同一事件序列中的事件不能交給兩個view同時處理 但是通過特殊手段可以做到 比如一個view將本該自己處理的事件通過onTouchEvent強行傳遞給其他view處理

某個view一旦攔截某一個事件 那麼這個事件序列都只能由他來處理 並且它的onInterceptTouchEvent不會再次被調用 因為這個事件序列中的所有事件都有他來處理 所以不需要再次詢問他是否攔截了

某個view一旦開始處理某個事件 如果他沒有將ACTION_DOWN消耗掉 返回了false 那麼此時該事件將會重新交給他的父元素去處理 即父元素的onTouchEvent將會被重新調用 意思就是事件一旦交給一個view來處理 那麼他就必須要將其消耗掉 至少要將ACTION_DOWN消耗掉 否則同一事件序列中的其他事件也就不再交給他處理了 這就好比上級交給你一件任務 你一開始就沒有處理好 那麼之後的任務也不回交給你了 同一個道理

如果view不消耗除了ACTION_DOWN以外的事件 那麼這個點擊事件會消失 此時父元素的onTouchEvent方法也不會被調用 並且當前view可以繼續接受該事件序列中後續的事件 最終這些消失的事件會交給Activity來處理

ViewGrounp默認不會攔截任何事件 在Android的源碼當中 ViewGrounp的onInterceptTouchEvent方法默認返回為false

view沒有onInterceptTouchEvent方法 事件一旦傳遞給他 那麼他的onTouchEvent方法就會被調用

view的onTouchEvent方法默認都會消耗事件 返回true 除非他當前狀態是不可點擊的(clickable和longClickable同時為false)view的longClickable默認都為false clickable分情況 例如button為true textview為false

view的enable屬性不影響onTouchEvent的默認返回值 哪怕一個view當前處於不可用狀態 事件傳遞給他之後 他的onTouchEvent方法依舊會返回true來消耗事件

onClick會發生的前提是當前view是可點擊的 並且他收到了down和up事件

事件傳遞過程是由外向內的 即事件總是先傳遞給父元素 然後再由父元素分發給子元素 通過requestDisallowInterceptTouchEvent

方法可以在子元素中干預父元素的事件分發過程 請求父元素不要攔截該事件 該方法對ACTION_DOWN無效 因為通過閱讀源碼可知 該方法會改變

父元素中的一個標記位 當事件為ACION_DOWN時 會reset這個標記位 所以該方法對ACTION_DOWN無效

view的滑動沖突

沖突場景

外部滑動與內部滑動方向不一致 外部滑動方向與內部滑動方向一致 上述兩種情況嵌套

處理規則

對於場景1來說 當用戶左右滑動時 需要讓外界view攔截事件 當用戶上下滑動時 需要讓內部view攔截事件我們需要做的就是判斷用戶滑動的方向 具體來說 我們可以根據用戶水平滑動和豎直滑動的距離差來判斷用戶滑動的方向 對於場景2來說 我們無法通過用戶的滑動方向來確定到底需要哪個view來滑動 所以對於這種類型的滑動沖突 我們就需要在業務上來對兩種滑動作出區分 例如 當處於某種狀態之下 需要外部view來響應用戶的滑動 另一種狀態之下需要內部view來響應用戶的滑動 對於場景3這種比較復雜的滑動沖突來說 單一的解決辦法肯定是不行的 所以我們需要將上邊兩種方案結合起來使用 具體場景具體分析

解決方式

對於場景1來說 具體的解決辦法有兩種 外部攔截法和內部攔截法

外部攔截法

指的是所有的點擊事件都要經過父容器的攔截處理(不包括DOWN事件) 如果父容器需要此事件 就攔截 如果不需要就不進行攔截 外部攔截法需要重寫父容器的onInterceptTouchEvent方法 在該方法中作出相應的判斷處理 大體邏輯可以

根據如下偽代碼得出

    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:
                intercepted = false;
                break;
            case MotionEvetn.ACTION_MOVE:
                if(父容器需要該事件){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

上述代碼是外部攔截法的典型邏輯 針對不同的滑動沖突處理 只需要修改父容器需要當前事件的條件即可 其他都不需要作出修改 在上述代碼中 ACTION_DOWN事件 父容器必須返回false 因為一旦父容器攔截了這個事件 那麼後邊的事件都不能傳遞給子元素了 ACTION_MOVE事件中 我們根據需要來判斷是否攔截該事件 這裡是代碼中的重點 ACTION_UP事件 我們也需要返回false 因為 如果我們在父容器的move事件中攔截了事件 那麼該事件序列中後續的事件就會交給父容器來處理 我們無需關心up事件 如果我們 沒有在父容器匯總攔截move事件的話 事件應該是交給子元素去處理的 但是我們在這裡對up事件做了攔截 up事件就會交個父容器來處理 此時子元素將不能收到up事件 那麼子元素就不能正確的響應點擊事件了 所以在這裡我們需要對ACTION_UP事件返回false

總結起來其實就是我們只需要對ACTION_MOVE事件作出處理而已 ACTION_DOWN和ACTION_UP事件我們都只需要返回false即可

內部攔截法

內部攔截法的意思就是父容器在攔截階段不做任何邏輯上的處理(並不是父元素不攔截 而是默認攔截除了ACTION_DOWN 以外的所有事件 子元素如果需要某個事件的話 就需要通過requestDisallowInterceptTouchEvent()方法來獲取該事件) 所有的攔截邏輯處理都交給子元素去處理 子元素需要該事件就直接消耗掉 如果子元素不需要該事件的話 就交給父容器去處理 與外部攔截法相比 該方法會比較復雜 偽代碼如下所示 我們需要重寫的是 子元素 的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent ev){
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch(MotionEvent.getAction()){
            case MotionEvent.ACTION_DOWN:
                requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if(子元素不需要該事件){
                    requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            defalut:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }

上述代碼是內部攔截法的典型代碼 當面對不同的滑動策略時 只需要修改子元素中該方法中的判斷條件即可 別的地方不需要做出改動 除了子元素需要重寫該方法以外 父元素也需要作出一些處理 需要默認攔截除了ACTION_DOWN以外的所有事件(ACTION_DOWN事件不受requestDisallowTouchEvent()方法的控制 一旦父容器攔截了這個事件 那麼所有的事件都不能傳遞到子元素中去了 內部攔截法也就無從談起了) 這樣當子元素認為不需要事件的時候 調用requestDisallowInterceptTouchEvent(false)時父容器才能繼續攔截之後的事件 父容器的修改如下所示 為固定代碼

    public boolean onInterceptTouchEvent(MotionEvent ev){
        int action = ev.getAction();
        if(action == MotionEvetn.ACTION_DOWN){
            return false;
        }else{
            return true;
        }
    }
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved