Android之View的內容


View的事件體系

本章介紹View的事件分發和滑動沖突問題的解決方案。

3.4 View的事件分發機制

3.4.1 點擊事件的傳遞規則

點擊事件是MotionEvent。首先我們先看看下面一段偽代碼,通過它我們可以理解到點擊事件的傳遞規則:

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

上面代碼主要涉及到以下三個方法:

  • public boolean dispatchTouchEvent(MotionEvent ev);
    這個方法用來進行事件的分發。如果事件傳遞給當前view,則調用此方法。返回結果表示是否消耗此事件,受onTouchEvent和下級View的dispatchTouchEvent方法影響。
  • public boolean onInterceptTouchEvent(MotionEvent ev);
    這個方法用來判斷是否攔截事件。在dispatchTouchEvent方法中調用。返回結果表示是否攔截。
  • public boolean onTouchEvent(MotionEvent ev);
    這個方法用來處理點擊事件。在dispatchTouchEvent方法中調用,返回結果表示是否消耗事件。如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。
 
 

點擊事件的傳遞規則:對於一個根ViewGroup,點擊事件產生后,首先會傳遞給他,這時候就會調用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要攔截事件,接下來事件就會交給ViewGroup處理,調用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值為false,表示ViewGroup不攔截該事件,這時事件就傳遞給他的子View,接下來子View的dispatchTouchEvent方法,如此反復直到事件被最終處理。

當一個View需要處理事件時,如果它設置了OnTouchListener,那么onTouch方法會被調用,如果onTouch返回false,則當前View的onTouchEvent方法會被調用,返回true則不會被調用,同時,在onTouchEvent方法中如果設置了OnClickListener,那么他的onClick方法會被調用。==由此可見處理事件時的優先級關系: onTouchListener > onTouchEvent >onClickListener==

關於事件傳遞的機制,這里給出一些結論:

  1. 一個事件系列以down事件開始,中間包含數量不定的move事件,最終以up事件結束。
  2. 正常情況下,一個事件序列只能由一個View攔截並消耗。
  3. 某個View攔截了事件后,該事件序列只能由它去處理,並且它的onInterceptTouchEvent
    不會再被調用。
  4. 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false),那么同一事件序列中的其他事件都不會交給他處理,並且事件將重新交由他的父元素去處理,即父元素的onTouchEvent被調用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么這個事件將會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到后續的事件,最終消失的點擊事件會傳遞給Activity去處理。
  6. ViewGroup默認不攔截任何事件。
  7. View沒有onInterceptTouchEvent方法,一旦事件傳遞給它,它的onTouchEvent方法會被調用。
  8. View的onTouchEvent默認消耗事件,除非他是不可點擊的( clickable和longClickable同時為false) 。View的longClickable屬性默認false,clickable默認屬性分情況(如TextView為false,button為true)。
  9. View的enable屬性不影響onTouchEvent的默認返回值。
  10. onClick會發生的前提是當前View是可點擊的,並且收到了down和up事件。
  11. 事件傳遞過程總是由外向內的,即事件總是先傳遞給父元素,然后由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發過程,但是ACTION_DOWN事件除外。

3.4.2 事件分發的源碼解析

3.5 滑動沖突

在界面中,只要內外兩層同時可以滑動,這個時候就會產生滑動沖突。滑動沖突的解決有固定的方法。

3.5.1 常見的滑動沖突場景

 
 
  1. 外部滑動和內部滑動方向不一致;
    比如viewpager和listview嵌套,但這種情況下viewpager自身已經對滑動沖突進行了處理。
  2. 外部滑動方向和內部滑動方向一致;
  3. 上面兩種情況的嵌套。
    只要解決1和2即可。

3.5.2 滑動沖突的處理規則

對於場景一,處理的規則是:當用戶左右( 上下) 滑動時,需要讓外部的View攔截點擊事件,當用戶上下( 左右) 滑動的時候,需要讓內部的View攔截點擊事件。根據滑動的方向判斷誰來攔截事件。

對於場景二,由於滑動方向一致,這時候只能在業務上找到突破點,根據業務需求,規定什么時候讓外部View攔截事件,什么時候由內部View攔截事件。

場景三的情況相對比較復雜,同樣根據需求在業務上找到突破點。

3.5.3 滑動沖突的解決方式

外部攔截法
所謂外部攔截法是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。下面是偽代碼:

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

針對不同沖突,只需修改父容器需要當前事件的條件即可。其他不需修改也不能修改。

  • ACTION_DOWN:必須返回false。因為如果返回true,后續事件都會被攔截,無法傳遞給子View。
  • ACTION_MOVE:根據需要決定是否攔截
  • ACTION_UP:必須返回false。如果攔截,那么子View無法接受up事件,無法完成click操作。而如果是父容器需要該事件,那么在ACTION_MOVE時已經進行了攔截,根據上一節的結論3,ACTION_UP不會經過onInterceptTouchEvent方法,直接交給父容器處理。

內部攔截法
內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗,否則就交由父容器進行處理。這種方法與Android事件分發機制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是偽代碼:

    public boolean dispatchTouchEvent ( MotionEvent event ) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此類點擊事件) { parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default : break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); } 

==除了子元素需要做處理外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。==因此,父元素要做以下修改:

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

優化滑動體驗:

    mScroller.abortAnimation(); 

外部攔截法實例:HorizontalScrollViewEx

4 View的工作原理

主要內容

  • View的工作原理
  • 自定義View的實現方式
  • 自定義View的底層工作原理,比如View的測量流程、布局流程、繪制流程
  • View常見的回調方法,比如構造方法、onAttach.onVisibilityChanged/onDetach等

4.1 初識ViewRoot和DecorView

ViewRoot的實現是 ViewRootImpl 類,是連接WindowManager和DecorView的紐帶,View的三大流程( mearsure、layout、draw) 均是通過ViewRoot來完成。當Activity對象被創建完畢后,會將DecorView添加到Window中,同時創建 ViewRootImpl 對象,並將ViewRootImpl 對象和DecorView建立連接,源碼如下:

    root = new ViewRootImpl(view.getContext(),display); root.setView(view,wparams, panelParentView); 

View的繪制流程是從ViewRoot的performTraversals開始的。

 
image
  1. measure用來測量View的寬高
  2. layout來確定View在父容器中的位置
  3. draw負責將View繪制在屏幕上

performTraversals會依次調用 performMeasure 、 performLayout 和performDraw 三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重復父容器的measure過程,如此反復完成了整個View數的遍歷。另外兩個過程同理。

  • Measure完成后, 可以通過getMeasuredWidth 、getMeasureHeight 方法來獲取View測量后的寬/高。特殊情況下,測量的寬高不等於最終的寬高,詳見后面。
  • Layout過程決定了View的四個頂點的坐標和實際View的寬高,完成后可通過 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四個定點坐標。

DecorView作為頂級View,其實是一個 FrameLayout ,它包含一個豎直方向的 LinearLayout ,這個 LinearLayout 分為標題欄和內容欄兩個部分。

    <div align="center"> <img src="http://images2015.cnblogs.com/blog/500720/201609/500720-20160925174505236-1295369287.png" width = "150" height = "200" alt="圖片" align=center /> </div> 

在Activity通過setContextView所設置的布局文件其實就是被加載到內容欄之中的。這個內容欄的id是 R.android.id.content ,通過 ``` ViewGroup content = findViewById(R.android.id.content);
可以得到這個contentView。View層的事件都是先經過DecorView,然后才傳遞到子View。

4.2 理解MeasureSpec

MeasureSpec決定了一個View的尺寸規格。但是父容器會影響View的MeasureSpec的創建過程。系統將View的 LayoutParams 根據父容器所施加的規則轉換成對應的MeasureSpec,然后根據這個MeasureSpec來測量出View的寬高。

4.2.1 MeasureSpec

MeasureSpec代表一個32位int值,高2位代表SpecMode( 測量模式) ,低30位代表SpecSize( 在某個測量模式下的規格大小) 。
SpecMode有三種:

  • UNSPECIFIED :父容器不對View進行任何限制,要多大給多大,一般用於系統內部
  • EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的 match_parent 和具體數值這兩種模式
  • AT_MOST :對應View的默認大小,不同View實現不同,View的大小不能大於父容器的SpecSize,對應 LayoutParams 中的 wrap_content

4.2.2 MeasureSpec和LayoutParams的對應關系

對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。

View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方法,通過調用子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec,再調用子元素的 measure 方法。

parentSize是指父容器中目前可使用的大小。

  1. 當View采用固定寬/高時( 即設置固定的dp/px) ,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,並且大小遵循我們設置的值。
  2. 當View的寬/高是 match_parent 時,View的MeasureSpec都是EXACTLY模式並且其大小等於父容器的剩余空間。
  3. 當View的寬/高是 wrap_content 時,View的MeasureSpec都是AT_MOST模式並且其大小不能超過父容器的剩余空間。
  4. 父容器的UNSPECIFIED模式,一般用於系統內部多次Measure時,表示一種測量的狀態,一般來說我們不需要關注此模式。

4.3 View的工作流程

4.3.1 measure過程

View的measure過程

直接繼承View的自定義控件需要重寫 onMeasure 方法並設置 wrap_content ( 即specMode是 AT_MOST 模式) 時的自身大小,

否則在布局中使用 wrap_content 相當於使用 match_parent 。對於非 wrap_content 的情形,我們沿用系統的測量值即可。

      @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); // 在 MeasureSpec.AT_MOST 模式下,給定一個默認值mWidth,mHeight。默認寬高靈活指定 //參考TextView、ImageView的處理方式 //其他情況下沿用系統測量規則即可 if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWith, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWith, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } } 

ViewGroup的measure過程

ViewGroup是一個抽象類,沒有重寫View的 onMeasure 方法,但是它提供了一個 measureChildren 方法。這是因為不同的ViewGroup子類有不同的布局特性,導致他們的測量細節各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure方法。

measureChildren方法的流程:

  1. 取出子View的 LayoutParams
  2. 通過 getChildMeasureSpec 方法來創建子元素的 MeasureSpec
  3. 將 MeasureSpec 直接傳遞給View的measure方法來進行測量

通過LinearLayout的onMeasure方法里來分析ViewGroup的measure過程:

  1. LinearLayout在布局中如果使用match_parent或者具體數值,測量過程就和View一致,即高度為specSize
  2. LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度總和,但不超過它的父容器的剩余空間
  3. LinearLayout的的最終高度同時也把豎直方向的padding考慮在內

View的measure過程是三大流程中最復雜的一個,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是准確的。

==如果我們想要在Activity啟動的時候就獲取一個View的寬高,怎么操作呢?==因為View的measure過程和Activity的生命周期並不是同步執行,無法保證在Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來獲取View的寬高:

  1. Activity/View#onWindowFocusChanged
    onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經准備好了,需要注意:它會被調用多次,當Activity的窗口得到焦點和失去焦點均會被調用。
  2. view.post(runnable)
    通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此runnable的時候,View也初始化好了。
  3. ViewTreeObserver
    使用 ViewTreeObserver 的眾多回調可以完成這個功能,比如OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的可見性發生改變時,onGlobalLayout 方法會被回調,這是獲取View寬高的好時機。需要注意的是,伴隨着View樹狀態的改變, onGlobalLayout 會被回調多次。
  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)
    手動對view進行measure。需要根據View的layoutParams分情況處理:
    • match_parent:
      無法measure出具體的寬高,因為不知道父容器的剩余空間,無法測量出View的大小
    • 具體的數值( dp/px):
            int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec); - wrap_content: int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); // View的尺寸使用30位二進制表示,最大值30個1,在AT_MOST模式下,我們用View理論上能支持的最大值去構造MeasureSpec是合理的 int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec); 

4.3.2 layout過程

layout的作用是ViewGroup用來確定子View的位置,當ViewGroup的位置被確定后,它會在onLayout中遍歷所有的子View並調用其layout方法,在 layout 方法中, onLayout 方法又會被調用。

View的 layout 方法確定本身的位置,源碼流程如下:

  1. setFrame 確定View的四個頂點位置,即確定了View在父容器中的位置
  2. 調用 onLayout 方法,確定所有子View的位置,和onMeasure一樣,onLayout的具體實現和布局有關,因此View和ViewGroup均沒有真正實現 onLayout 方法。

以LinearLayout的 onLayout 方法為例:

  1. 遍歷所有子View並調用 setChildFrame 方法來為子元素指定對應的位置
  2. setChildFrame 方法實際上調用了子View的 layout 方法,形成了遞歸

==View的測量寬高和最終寬高的區別:==
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成於measure過程,最終寬高形成於layout過程。但重寫view的layout方法可以使他們不相等。

4.3.3 draw過程

View的繪制過程遵循如下幾步:

  1. 繪制背景 drawBackground(canvas)
  2. 繪制自己 onDraw
  3. 繪制children dispatchDraw 遍歷所有子View的 draw 方法
  4. 繪制裝飾 onDrawScrollBars

ViewGroup會默認啟用 setWillNotDraw 為ture,導致系統不會去執行 onDraw ,所以自定義ViewGroup需要通過onDraw來繪制內容時,必須顯式的關閉 WILL_NOT_DRAW 這個優化標記位,即調用 setWillNotDraw(false);

4.4 自定義View

4.4.1 自定義View的分類

繼承View 重寫onDraw方法
通過 onDraw 方法來實現一些不規則的效果,這種效果不方便通過布局的組合方式來達到。這種方式需要自己支持 wrap_content ,並且padding也要去進行處理。

繼承ViewGroup派生特殊的layout
實現自定義的布局方式,需要合適地處理ViewGroup的測量、布局這兩個過程,並同時處理子View的測量和布局過程。

繼承特定的View子類( 如TextView、Button)
擴展某種已有的控件的功能,比較簡單,不需要自己去管理 wrap_content 和padding。

** 繼承特定的ViewGroup子類( 如LinearLayout)**
比較常見,實現幾種view組合一起的效果。與方法二的差別是方法二更接近底層實現。

4.4.2 自定義View須知

  1. 直接繼承View或ViewGroup的控件, 需要在onmeasure中對wrap_content做特殊處理。指定wrap_content模式下的默認寬/高。
  2. 直接繼承View的控件,如果不在draw方法中處理padding,那么padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。
  3. 盡量不要用在View中使用Handler,因為沒必要。View內部提供了post系列的方法,完全可以替代Handler的作用。
  4. View中有線程和動畫,需要在View的onDetachedFromWindow中停止。當View不可見時,也需要停止線程和動畫,否則可能造成內存泄漏。
  5. View帶有滑動嵌套情形時,需要處理好滑動沖突

4.4.3 自定義View實例

自定義屬性設置方法:

  1. 在values目錄下創建自定義屬性的XML,如attrs.xml。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleView"> <attr name="circle_color" format="color" /> </declare-styleable> </resources> 
  1. 在View的構造方法中解析自定義屬性的值並做相應處理,這里我們解析circle_color。
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); a.recycle(); init(); } 
  1. 在布局文件中使用自定義屬性
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical" > <com.ryg.chapter_4.ui.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" android:layout_margin="20dp" android:background="#000000" android:padding="20dp" app:circle_color="@color/light_green" /> </LinearLayout> 

Android中的命名空間

  • 繼承ViewGroup派生特殊的layout:HorizontalScrollViewEx
    onMeasure方法中,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高做相應處理。然后判斷寬高是不是wrap_content,如果寬是,那么HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和。如果高是wrap_content,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin。
    onLayout方法中,在放置子元素時候也要考慮padding和margin。

4.4.4 自定義View的思想

  • 掌握基本功,比如View的彈性滑動、滑動沖突、繪制原理等
  • 面對新的自定義View時,對其分類並選擇合適的實現思路

 *************************

 

3.1 view的基礎知識

View的位置參數、MotionEvent和TouchSlop對象、VelocityTracker、GestureDetector和Scroller對象。

3.1.1什么是view

View是Android中所有控件的基類,View的本身可以是單個空間,也可以是多個控件組成的一組控件,即ViewGroup,ViewGroup繼承自View,其內部可以有子View,這樣就形成了View樹的結構。

3.1.2 View的位置參數

View的位置主要由它的四個頂點來決定,即它的四個屬性:top、left、right、bottom,分別表示View左上角的坐標點( top,left) 以及右下角的坐標點( right,bottom) 。

同時,我們可以得到View的大小:

   width = right - left height = bottom - top 

而這四個參數可以由以下方式獲取:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();

Android3.0后,View增加了x、y、translationX和translationY這幾個參數。其中x和y是View左上角的坐標,而translationX和translationY是View左上角相對於容器的偏移量。他們之間的換算關系如下:

x = left + translationX; y = top + translationY; 

top,left表示原始左上角坐標,而x,y表示變化后的左上角坐標。在View沒有平移時,x=left,y=top。==View平移的過程中,top和left不會改變,改變的是x、y、translationX和translationY。==

3.1.3 MotionEvent和TouchSlop

MotionEvent
事件類型

  • ACTION_DOWN 手指剛接觸屏幕
  • ACTION_MOVE 手指在屏幕上移動
  • ACTION_UP 手指從屏幕上松開

點擊事件類型

  • 點擊屏幕后離開松開,事件序列為DOWN->UP
  • 點擊屏幕滑動一會再松開,事件序列為DOWN->MOVE->...->MOVE->UP

通過MotionEven對象我們可以得到事件發生的x和y坐標,我們可以通過getX/getY和getRawX/getRawY得到。它們的區別是:getX/getY返回的是相對於當前View左上角的x和y坐標,getRawX/getRawY返回的是相對於手機屏幕左上角的x和y坐標。

TouchSloup
TouchSloup是系統所能識別出的被認為是滑動的最小距離,這是一個常量,與設備有關,可通過以下方法獲得:

ViewConfiguration.get(getContext()).getScaledTouchSloup(). 

當我們處理滑動時,比如滑動距離小於這個值,我們就可以過濾這個事件(系統會默認過濾),從而有更好的用戶體驗。

3.1.4 VelocityTracker、GestureDetector和Scroller

VelocityTracker
速度追蹤,用於追蹤手指在滑動過程中的速度,包括水平放向速度和豎直方向速度。使用方法:

  • 在View的onTouchEvent方法中追蹤當前單擊事件的速度
    VelocityRracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);
  • 計算速度,獲得水平速度和豎直速度
    velocityTracker.computeCurrentVelocity(1000);
    int xVelocity = (int)velocityTracker.getXVelocity();
    int yVelocity = (int)velocityTracker.getYVelocity();
    注意,獲取速度之前必須先計算速度,即調用computeCurrentVelocity方法,這里指的速度是指一段時間內手指滑過的像素數,1000指的是1000毫秒,得到的是1000毫秒內滑過的像素數。速度可正可負:速度 = ( 終點位置 - 起點位置) / 時間段
  • 最后,當不需要使用的時候,需要調用clear()方法重置並回收內存:
    velocityTracker.clear();
    velocityTracker.recycle();

GestureDetector
手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行為。使用方法:

  • 創建一個GestureDetector對象並實現OnGestureListener接口,根據需要,也可實現OnDoubleTapListener接口從而監聽雙擊行為:
    GestureDetector mGestureDetector = new GestureDetector(this);
    //解決長按屏幕后無法拖動的現象
    mGestureDetector.setIsLongpressEnabled(false);
  • 在目標View的OnTouchEvent方法中添加以下實現:
    boolean consume = mGestureDetector.onTouchEvent(event);
    return consume;
  • 實現OnGestureListener和OnDoubleTapListener接口中的方法

     
     

其中常用的方法有:onSingleTapUp(單擊)、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)和onDoubleTap( 雙擊)。建議:如果只是監聽滑動相關的,可以自己在onTouchEvent中實現,如果要監聽雙擊這種行為,那么就使用GestureDetector。

Scroller
彈性滑動對象,用於實現View的彈性滑動。其本身無法讓View彈性滑動,需要和View的computeScroll方法配合使用才能完成這個功能。使用方法:

    Scroller scroller = new Scroller(mContext); //緩慢移動到指定位置 private void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; //1000ms內滑向destX,效果就是慢慢滑動 mScroller.startScroll(scrollX,0,delta,0,1000); invalidata(); } @Override public void computeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX,mScroller.getCurrY()); postInvalidate(); } } 

3.2 View的滑動

三種方式實現View滑動

3.2.1 使用scrollTo/scrollBy

scrollBy實際調用了scrollTo,它實現了基於當前位置的相對滑動,而scrollTo則實現了絕對滑動。

==scrollTo和scrollBy只能改變View的內容位置而不能改變View在布局中的位置。滑動偏移量mScrollX和mScrollY的正負與實際滑動方向相反,即從左向右滑動,mScrollX為負值,從上往下滑動mScrollY為負值。==

3.2.2 使用動畫

使用動畫移動View,主要是操作View的translationX和translationY屬性,既可以采用傳統的View動畫,也可以采用屬性動畫,如果使用屬性動畫,為了能夠兼容3.0以下的版本,需要采用開源動畫庫nineolddandroids。 如使用屬性動畫:(View在100ms內向右移動100像素)

    ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start(); 

3.2.3 改變布局屬性

通過改變布局屬性來移動View,即改變LayoutParams。

3.2.4 各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單,適合對View內容的滑動;
  • 動畫:操作簡單,主要適用於沒有交互的View和實現復雜的動畫效果;
  • 改變布局參數:操作稍微復雜,適用於有交互的View。

3.3 彈性滑動

3.3.1 使用Scroller

使用Scroller實現彈性滑動的典型使用方法如下:

    Scroller scroller = new Scroller(mContext); //緩慢移動到指定位置 private void smoothScrollTo(int destX,int dextY){ int scrollX = getScrollX(); int deltaX = destX - scrollX; //1000ms內滑向destX,效果就是緩慢滑動 mScroller.startSscroll(scrollX,0,deltaX,0,1000); invalidate(); } @override public void computeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } } 

從上面代碼可以知道,我們首先會構造一個Scroller對象,並調用他的startScroll方法,該方法並沒有讓view實現滑動,只是把參數保存下來,我們來看看startScroll方法的實現就知道了:

    public void startScroll(int startX,int startY,int dx,int dy,int duration){ mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAminationTimeMills(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float)mDuration; } 

可以知道,startScroll方法的幾個參數的含義,startX和startY表示滑動的起點,dx和dy表示的是滑動的距離,而duration表示的是滑動時間,注意,這里的滑動指的是View內容的滑動,在startScroll方法被調用后,馬上調用invalidate方法,這是滑動的開始,invalidate方法會導致View的重繪,在View的draw方法中調用computeScroll方法,computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然后通過scrollTo方法實現滑動,接着又調用postInvalidate方法進行第二次重繪,一直循環,直到computeScrollOffset()方法返回值為false才結束整個滑動過程。 我們可以看看computeScrollOffset方法是如何獲得當前的scrollX和scrollY的:

    public boolean computeScrollOffset(){ ... int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime); if(timePassed < mDuration){ switch(mMode){ case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDuratio nReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(y * mDeltaY); break; ... } } return true; } 

到這里我們就基本明白了,computeScroll向Scroller獲取當前的scrollX和scrollY其實是通過計算時間流逝的百分比來獲得的,每一次重繪距滑動起始時間會有一個時間間距,通過這個時間間距Scroller就可以得到View當前的滑動位置,然后就可以通過scrollTo方法來完成View的滑動了。

3.3.2 通過動畫

動畫本身就是一種漸近的過程,因此通過動畫來實現的滑動本身就具有彈性。實現也很簡單:

    ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start() ; //當然,我們也可以利用動畫來模仿Scroller實現View彈性滑動的過程: final int startX = 0; final int deltaX = 100; ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000); animator.addUpdateListener(new AnimatorUpdateListener(){ @override public void onAnimationUpdate(ValueAnimator animator){ float fraction = animator.getAnimatedFraction(); mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0); } }); animator.start(); 

上面的動畫本質上是沒有作用於任何對象上的,他只是在1000ms內完成了整個動畫過程,利用這個特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,根據比例計算出View所滑動的距離。采用這種方法也可以實現其他動畫效果,我們可以在onAnimationUpdate方法中加入自定義操作。

3.3.3 使用延時策略

延時策略的核心思想是通過發送一系列延時信息從而達到一種漸近式的效果,具體可以通過Hander和View的postDelayed方法,也可以使用線程的sleep方法。 下面以Handler為例:

    private static final int MESSAGE_SCROLL_TO = 1; private static final int FRAME_COUNT = 30; private static final int DELATED_TIME = 33; private int mCount = 0; @suppressLint("HandlerLeak") private Handler handler = new handler(){ public void handleMessage(Message msg){ switch(msg.what){ case MESSAGE_SCROLL_TO: mCount ++ ; if (mCount <= FRAME_COUNT){ float fraction = mCount / (float) FRAME_COUNT; int scrollX = (int) (fraction * 100); mButton1.scrollTo(scrollX,0); mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME); } break; default : break; } } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM