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