View的事件体系
一、view基礎知識
什么是View
(1)View是Android中所有控件的基類,不管是簡單的Button和TextView還是復雜的RelativeLayout和ListView,它們的共同基類都是View。
(2)ViewGroup也繼承了View,這就意味著View本身就可以使單個控件也可以是有多個控件組成的一組控件,通過這種關系就形成了View樹的結構。
View的位置參數
(1)View的位置主要由它的四個頂點來決定,分別對應于View的四個屬性:top、left、right、bottom。其中top是左上角縱坐標,left是左上角橫坐標,right是右下角橫坐標,bottom是右下角縱坐標。這些坐標是一種相對坐標,都是相對于View的父容器來說的。
(2)可通過以下方式獲取View的四個位置參數:
Left = getLeft(); Right = getRight(); Top = getTop(); Bottom = getBottom();(3)從Android3.0開始,View增加了額外的幾個參數:x、y、translationX和translationY,其中x和y是View內容左上角的坐標,而translationX和translationY是View左上角相對于父容器的偏移量,并且translationX和translationY的默認值是0。這幾個參數的換算關系如下所示:
x = left + translationX y = top + translationY(4)View在平移的過程中,top和left表示的是原始左上角的位置信息,其值并不會發生改變,此時發生改變的是x、y、translationX和translationY這四個參數。
?
MotionEvent和TouchSlop
MotionEvent
(1)在手指接觸屏幕后所產生的一系列事件中,典型的事件類型有如下幾種:
- ACTION_DOWM——手指剛接觸屏幕;
- ACTION_MOVE——手指在屏幕上移動;
- ACTION_UP——手指從屏幕上松開的一瞬間。
(2)正常情況下,一次手指觸摸屏幕的行為會觸發一系列點擊事件,考慮如下幾種情況:
- 點擊屏幕后立即松開,事件序列為DOWM->UP;
- 點擊屏幕滑動一會再松開,事件序列為DOWM->MOVE->...->MOVE->UP。
(3)getX/getY返回的是相對于當前View左上角的x和y坐標,而getRawX/getRawY返回的是相對于屏幕左上角的x和y坐標。
?
TouchSlop
(1)TouchSlop是系統所能識別出的被認為是滑動的最小距離,換句話說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統就不認為你是在進行滑動操作。
(2)通過如下方式即可獲取這個常量:
ViewConfiguration.get(getContext()).getScaledTouchSlop();(3)當我們在處理滑動時,可以利用這個常量來做一些過濾,比如當兩次滑動事件的滑動距離小于這個值,我們就可以認為未達到滑動距離的臨界值,因此就可以認為他們不是滑動。
?
?
VelocityTracker、GestureDetector和Scroller
?
VelocityTracker
?
(1)速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度。在View的onTouchEvent方法中追蹤當前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker。obtain(); velocityTracker.addMovement(event);(2)可以采用如下方式來獲得當前的速度:
velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracket.getYVelocity();在這一步中有兩點需要注意:
?
- 第一點,獲取速度之前必須先計算速度,即必須要調用computeCurrentVelocity方法;
- 第二點,這里的速度是指一段時間內手指所滑過的像素數
(3)當不需要使用VelocityTracker的時候,需要調用clear方法來重置并回收內存:
velocityTracker.clear(); velocityTracker.recycle();?
GestureDetector
gestrue—手勢,什么叫手勢呢?比如說,我們用魅族手機,我們在home鍵的兩側像屏幕內滑動,可以打開后臺任務列表等等,在應用中通過手勢來操作可以大大提升用戶體驗,手勢是連續觸碰的行為,比如左右上下滑等,安卓對上述兩種手勢行為都提供了支持:
?
而安卓中手勢交互的執行順序是:,其中MotionEvent這個類是用來封裝手勢,觸摸筆,軌跡球等等的動作事件,其內部封裝了兩個重要的屬性x和y,這兩個屬性分別用于記錄橫軸和縱軸的坐標,GestureDetector識別各種手勢,OnGestureListener: 這是一個手勢交互的監聽接口,其中提供了多個抽象方法, 并根據GestureDetector的手勢識別結果調用相對應的方法。
?
而對于GestureListener,有以下解釋:
?
案例:下滑關閉Activity,上滑啟動新的Activity
public class MainActivity extends AppCompatActivity {private GestureDetector mDetector;private final static int MIN_MOVE = 200; //最小距離private MyGestureListener mgListener;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//實例化SimpleOnGestureListener與GestureDetector對象mgListener = new MyGestureListener();mDetector = new GestureDetector(this, mgListener);}@Overridepublic boolean onTouchEvent(MotionEvent event) {return mDetector.onTouchEvent(event); //在某個activity或者是view中都可以,注意}//自定義一個GestureListener,這個是View類下的,別寫錯哦!!!private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float v, float v1) {if(e1.getY() - e2.getY() > MIN_MOVE){startActivity(new Intent(MainActivity.this, MainActivity.class));Toast.makeText(MainActivity.this, "通過手勢啟動Activity", Toast.LENGTH_SHORT).show();}else if(e1.getY() - e2.getY() < MIN_MOVE){finish();Toast.makeText(MainActivity.this,"通過手勢關閉Activity",Toast.LENGTH_SHORT).show();}return true;}}}
Scroller
(1)彈性滑動對象,用于實現View的彈性滑動。
(2)Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。
Scroller mScroller = 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); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }?
二、View的滑動
有三種方式可以實現View的滑動:
- 第一種是通過View本身提供的scrollTo/scrollBy方法來實現滑動;
- 第二種是通過動畫給View施加平移效果來實現滑動;
- 第三種是通過改變View的LayoutParams使得View重新布局從而實現滑動。
使用scrollTo/scrollBy
(1)scrollBy實際上也是調用了scrollTo方法,它實現了基于當前位置的相對滑動,而scrollTo則實現了基于所傳遞參數的絕對滑動。
(2)scrollTo和scrollBy只能改變View內容的位置而不能改變View在布局中的位置。
(3)在滑動的過程中,mScrollX的值總是等于View左邊緣和View內容左邊緣在水平方向的距離,而mScrollY的值總是等于View上邊緣和View內容上邊緣在豎直方向的距離。
(4)如果從左向右滑動,那么mScrollX為負值,反之為正;如果從上往下滑動,那么mScrollY為負值,反之為正值。
使用動畫
(1)使用動畫來移動View,主要是操作View的translationX和translationY屬性,既可以采用傳統的View動畫,也可以采用屬性動畫,如果采用屬性動畫的話,為了兼容3.0以下版本,需要采用開源動畫庫nineoldandroids。
(2)在Android3.0以下的手機上通過nineoldandroids來實現的屬性動畫本質上仍然是View動畫。
(3)View動畫是對View的影像做操作,它并不能真正改變View的位置參數,包括寬/高,并且如果希望動畫后的狀態得以保留還必須將fillAfter屬性設置為true,否則動畫完成后其動畫結果會消失。
改變布局參數
改變布局參數,即改變LayoutParams:
?
MarginLayoutParams params = (MarginLayoutParams)mButton.getLayoutParams(); params.width += 100; params.leftMargin += 100; mButton.requestLayout(); //或者mButton.setLayoutParams(params);各種滑動方式的對比
- scrollTo/scrollBy:操作簡單,適合對View內容的滑動;
- 動畫:操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果;
- 改變布局參數:操作稍微復雜,適用于有交互的View。
?
三、彈性滑動
使用Scroller
Scroller的工作原理:Scroller本身并不能實現View的滑動,它需要配合View的computeScroll方法才能完成彈性滑動的效果,它不斷地讓View重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出View當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成View的滑動。就這樣,View的每一次重繪都會導致View進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作機制。
通過動畫
動畫本身就是一種漸近的過程,因此通過它來實現的滑動天然就具有彈性效果。
使用延時策略
延時策略的核心思想是通過發送一系列延時消息從而達到一種漸近式的效果,具體來說可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法。
?
四、View的事件分發機制
點擊事件的傳遞規則
(1)點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
用來進行事件的分發。如果事件能夠傳遞給當前的View,那么此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
在dispatchTouchEvent方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那么在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。
在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。
上述三個方法的關系可以用如下偽代碼表示:
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }
通過上面的偽代碼,我們也可以大致了解點擊事件的傳遞規則:對于一個根ViewGroup來說,點擊事件產生后,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptEvent方法放回false就表示它不攔截當前事件,這時當前事件就會傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調用,如此反復直到事件被最終處理。
(2)給View設置的OnTouchListener,其優先級比onTouchEvent要高,onTouchEvent的優先級比OnClickListener要高。
(3)當一個點擊事件產生后,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最后Window再傳遞給頂級View。頂級View接到事件后,就會按照事件分發機制去分發事件。如果一個View的onTouchEvent返回false,那么它的父容器的onTouchEvent將會被調用,依此類推,如果所有的元素都不處理這個事件,那么這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用。
(4)關于事件傳遞機制,這里給出一些結論,根據這些結論可以更好地理解整個傳遞機制:
五、View的滑動沖突
常見的滑動沖突場景
常見的滑動沖突場景可以簡單分為如下三種:
滑動沖突的處理規則
(1)對于場景1,它的處理規則是:根據滑動是水平滑動還是豎直滑動來判斷到底由誰來攔截事件。
(2)對于場景2,它無法根據滑動的角度、距離差以及速度差來做判斷,但是這個時候一般都能在業務上找到突破點,比如業務上有規定:當處于某種狀態時需要外部View響應用戶的滑動,而處于另外一種狀態是則需要內部View來響應View的滑動,根據這種業務上的需求我們也能得出相應的處理規則。
(3)場景3跟場景2一樣,只能從業務上找到突破點,具體方法和場景2一樣,都是從業務的需求上得出相應的處理規則。
滑動沖突的解決方式
外部攔截法
(1)所謂外部攔截法是指點擊事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動沖突的問題,這種方法比較符合點擊事件的分發機制。這種方法的偽代碼如下:
@Override 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 = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }(2)在onInterceptTouchEvent方法中,首先是ACTION_DOW這個事件,父容器必須返回false,即不攔截ACTION_DOWN事件,這是因為一旦父容器攔截了ACTION_DOWN,那么后續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞給子元素了;其次是ACTION_MOVE事件,這個事件可以根據需要來決定是否攔截,如果父容器需要攔截就返回true,否則返回false;最后是ACTION_UP事件,這里必須要返回false,因為ACTION_UP事件本身沒有太多意義。
(3)考慮一種情況,假設事件交由子元素處理,如果父容器在ACTION_UP時返回了true,就會導致子元素無法接收到ACTION_UP事件,這個時候子元素中的onClick事件就無法觸發,但是父容器比較特殊,一旦它開始攔截任何一個事件,那么后續的事件都會交給它來處理,而ACTION_UP作為最后一個事件也必定可以傳遞給父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP時返回false.
內部攔截法
(1)內部攔截法是指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進行處理,這種方法和Android中的事件分發機制不一致,需要配合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: 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);(2)除了子元素需要做處理以外,父元素也要默認攔截除了ACTION_DOWN以外的其他事件,這樣當子元素調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。
為什么父容器不能攔截ACTION_DOWN事件呢?
那是因為ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT這個標記位的控制,所以一旦父容器攔截ACTION_DOWN事件,那么所有的事件都無法傳遞到子元素中去,這樣內部攔截就無法起作用了。
父容器所做如下所示:
public boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}
?
?
ZX
轉載于:https://www.cnblogs.com/lpd1/p/8016040.html
總結
- 上一篇: MySQL中的外键约束
- 下一篇: HPU1460: 杨八方的表面兄弟