一.View的基礎(chǔ)知識
1.View的位置參數(shù)
View的位置主要由它的四個(gè)定點(diǎn)來決定,分別對應(yīng)View的四個(gè)屬性:top、left、right、bottom,
這下坐標(biāo)都是相對父容器而言的。從3.0開始View增加了x、y、translationX、translationY;
x和y是View左上角的坐標(biāo),translationX和translationY是View左上方相對父容器的偏移量。
x = left + translationX;
y = top + translationY;
View平移的過程中,top和left表示的是原始左上角的位置信息;其值并不會改變,
此時(shí)發(fā)生改變的是x、y、translationX和translationY。
2.MotionEvent
點(diǎn)擊屏幕后松開,事件序列 DOWN->UP
點(diǎn)擊屏幕滑動(dòng)一會再松開,事件序列為 DOWN->MOVE->...->MOVE->UP。
getX/getY 獲取相對當(dāng)前View左上角的x和y坐標(biāo)
getRawX/getRawY 獲取相對手機(jī)屏幕左上角的x和y坐標(biāo)。
3.TouchSlop
是系統(tǒng)能識別滑動(dòng)的最小距離
ViewConfiguration.get(getContext()).getScaledTouchSlop()
4.VelocityTracker
用于追蹤手指在滑動(dòng)過程中的速度。使用前在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//想知道滑動(dòng)速度時(shí)
//獲取速度前需計(jì)算速度 參數(shù) 時(shí)間間隔 單位ms
velocityTracker.computeCurrentVelocity(1000);
//獲取速度
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
5.GestureDetector
用于輔助檢測用戶的單擊、滑動(dòng)、長按、雙擊等行為;建議:如果只是監(jiān)聽滑動(dòng)相關(guān)的推薦
在onTouchEvent中實(shí)現(xiàn),如果需要監(jiān)聽雙擊,使用GeststureDetector。
eg: GestureDetector mGes = new GestureDetecotr(this);
在View的OnTouchEvent方法中傳遞給mGes對象:
return mGes.onTouchEvent(event);
6.Scroller
用來實(shí)現(xiàn)View的彈性滑動(dòng),View的scrollTo/scrollBy是瞬間完成的,
使用Scroller配合View的computeScroll方法配合使用達(dá)到彈性滑動(dòng)的效果
二.View的滑動(dòng)
3種滑動(dòng)方式:
1.scrollTo和scrollBy
2.動(dòng)畫來移動(dòng)View
3.改變控件位置
1.ScrollTo 和 ScrollBy
scrollTo和scrollBy只能改變View內(nèi)容而不能改變View本身的位置。scrollBy內(nèi)部
也是調(diào)用了scrollTo,它是基于當(dāng)前位置的相對滑動(dòng),scrollTo是基于所傳遞參數(shù)
的絕對滑動(dòng)。在滑動(dòng)過程中mScrollX/mScrollY總是等于View邊緣與View內(nèi)容邊緣
的距離,這兩個(gè)屬性用getScrollX/getScrollY方法獲取
2.使用動(dòng)畫
幀動(dòng)畫 View動(dòng)畫 屬性動(dòng)畫
使用動(dòng)畫來移動(dòng)View,主要是操作View的translationX和translationY屬性。需要
注意的是,View動(dòng)畫只是對View的影像做操作,它并不能真正改變View的位置參
數(shù),如果這個(gè)View設(shè)置了點(diǎn)擊事件,點(diǎn)擊動(dòng)畫后的新位置無法觸發(fā)點(diǎn)擊事件的,
使用屬性動(dòng)畫沒有此問題,但3.0之前系統(tǒng)無屬性動(dòng)畫。
eg: xml設(shè)置
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
代碼中設(shè)置
ScaleAnimation sAnima = new ScaleAnimation(0, 5, 0, 5);
scale.startAnimation(sAnima);
屬性動(dòng)畫:
ObjectAnimator//
.ofFloat(view, "rotationX", 0.0F, 360.0F)//
.setDuration(500)//
.start();
3.改變布局 參數(shù)
改變布局參數(shù)實(shí)現(xiàn)滑動(dòng),即改變LayoutParams,如想將一個(gè)View右平移100px,
只需要將該View的LayoutParams里的marginLeft增加100px即可
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mBtn.getLayoutParams();
params.leftMargin += 100;
mBtn.requestLayout();
//或者mBtn.setLayoutParams(params);
==總結(jié):==
scrollTo/scrollBy: 操作簡單,適合對View內(nèi)容的滑動(dòng)
動(dòng)畫: 操作簡單,適合沒有交互的View和實(shí)現(xiàn)負(fù)責(zé)的動(dòng)畫效果
改變布局參數(shù):操作稍微復(fù)雜,適合有交互的View
三.彈性滑動(dòng)
1.使用Scroller原理: startScroll()保存了我們傳遞的幾個(gè)參數(shù) ——> invalidate()會導(dǎo)致View重繪 ——> draw() ——> computeScroll()該方法為空實(shí)現(xiàn),我們內(nèi)部調(diào)用scrollTo(x,y)實(shí)現(xiàn)滑動(dòng) 和 postInvalidate() 繼續(xù)重繪,反復(fù)下去完成彈性滑動(dòng)。
Scroller scroller=new Scroller(context);
//緩慢滾動(dòng)到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollx=getScrollX();
int deltaX=destX-scrollX;
//1000ms內(nèi)滑向destX,效果就是慢慢滑動(dòng)
scroller.startScroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
2.通過動(dòng)畫可以直接實(shí)現(xiàn)彈性滑動(dòng)
3.使用延時(shí)策略完成滑動(dòng),核心思想 就是通過發(fā)送一系列延時(shí)消息從而達(dá)到一種漸進(jìn)式的效果。用Handler或View的postDelayed方法,postDelayed發(fā)送延時(shí)消息,然后消息中進(jìn)行View滑動(dòng),接連不斷的發(fā)送這種延時(shí)消息,達(dá)到彈性滑動(dòng)的效果。也可以使用線程的sleep方法來實(shí)現(xiàn)。
四.View的事件分發(fā)機(jī)制
1. 點(diǎn)擊事件的傳遞規(guī)則
dispatchTouchEvent(MotionEvent event) 用來處理事件的分發(fā),返回結(jié)果受當(dāng)前View的onTouchEvent和下級View的
dispatchTouchEvent方法影響,表示是否消耗該事件。
onInterceptTouchEvent(MotionEvent event)
在dispatchTouchEvent方法內(nèi)部調(diào)用,用來判斷是否攔截某個(gè)事件,如果
當(dāng)前View攔截了某個(gè)事件,那在同一個(gè)事件序列中,此方法不會再次調(diào)用,
返回結(jié)果表示是否攔截當(dāng)前事件
onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗
,在同一事件序列里,當(dāng)前View無法再次接收到事件三者關(guān)系可以用如下偽代碼表示
==原理==:dispatchTouchEvent方法中判斷是否需要攔截,需要的話,就不再調(diào)用onInterceptTouchEvent,并調(diào)用onTouchEvent方法,如果不攔截,就會調(diào)用子VIew的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
事件順序: OnTouListener的OnTouch > onTouchEvent > onClickListener
如果OnTouch中返回true,就不會調(diào)用onTouchEvent方法,返回false就會調(diào)用,如果onTouchEvent中存在onClickListener,onClick一定會被調(diào)用。除非設(shè)置該VIew的onClickible 和 onLongClickible都為false。
1.對于一個(gè)根ViewGroup,點(diǎn)擊事件產(chǎn)生后,首先會傳遞給它,這時(shí)他的dispatchTouchEvent會調(diào)用,
如果它的onInterceptTouchEvent返回true表示要攔截當(dāng)前事件,接下來事件會交給這個(gè)ViewGroup
處理,它的onTouchEvent就會被調(diào)用,如果這個(gè)ViewGroup的onInterceptTouchEvent返回false,
則事件會繼續(xù)傳遞給子元素,子元素的dispatchTouchEvent會調(diào)用,如此反復(fù)直到事件被處理。
view事件傳遞

2.當(dāng)一個(gè)View需要處理事件時(shí),如果設(shè)置了OnTouchListener,那么OnTouchListener的onTouch方法
會回調(diào),如果onTouch返回false,則當(dāng)前View的onTouchEvent方法會被調(diào)用;如果返回true,那么
onTouchEvent方法將不會調(diào)用。由此可見,OnTouchListener優(yōu)先級高于onTouchEvent。
OnClickListener優(yōu)先級處在事件傳遞的尾端。
3.一個(gè)點(diǎn)擊事件產(chǎn)生后,傳遞順序:Activity->Window->View;如果一個(gè)View的onTouchEvent返回false,
那么它的父容器的onTouchEvent會被調(diào)用,以此類推,所有元素都不處理該事件,最終將傳遞給Activity
處理,即Activity的onTouchEvent會被調(diào)用。
4.同一個(gè)事件序列是指從手指觸摸屏幕那一刻開始,到手指離開屏幕那一刻(down->move...move->up)
5.一個(gè)事件序列只能被一個(gè)View攔截且消耗,同一個(gè)事件序列所有事件都會直接交給它處理,
并且它的onInterceptTouchEvent不會再被調(diào)用。
6.某個(gè)View一旦開始處理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件
序列中其他事件都不會再交給它來處理,事件將重新交給他的父元素處理,即父元素的onTouchEvent會被調(diào)用。
7.如果某個(gè)View不消耗除ACTION_DOWN以外的其他事件,那么這個(gè)點(diǎn)擊事件會消失,此時(shí)父元素的onTouchEvent
并不會被調(diào)用,并且當(dāng)前View可以收到后續(xù)事件,最終這些消失的點(diǎn)擊事件會傳遞給Activity處理
8.ViewGroup默認(rèn)不攔截任何事件,ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false。
9.View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。
10.View的onTouchEvent方法默認(rèn)消耗事件(返回true),除非他是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)。
View的longClickable屬性默認(rèn)都為false,clickable屬性分情況,Button默認(rèn)為true,TextView默認(rèn)為false。
onClick發(fā)生的前提是View可點(diǎn)擊,并且它收到了down和up事件。
11.事件傳遞過程是由內(nèi)而外,事件總是先傳遞給父元素,然后在由父元素分發(fā)給子View,通過
requestDisallowInterceptTouchEvent方法可以在子元素干預(yù)父元素的事件分發(fā)過程,但ACTION_DOWN事件除外。
2.頂級View對點(diǎn)擊事件的分發(fā)過程
①.ViewGroup對事件攔截的處理。
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null){
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT)!=0;
if(!disallowIntercept){
intercepted = onInterceptTouchEvent(Ev);
ev.setAction(action);
}else{
intercepted = false;
}
}else{
intercepted = true;
}
原理:如果事件類型是MotionEvent.ACTION_DOWN 或者 已經(jīng)由子元素成功處理時(shí),既mFirstTouchTarget指向子元素,不為null,當(dāng)ViewGroup攔截事件時(shí),當(dāng)ACTION_MOVE 和 ACTION_UP來時(shí), 將不會再去調(diào)用onInterceptTouchEvent。并且其他的事件都會交給它處理。
1.ViewGroup會在down事件到來時(shí)重置狀態(tài),會將FLAG_DISALLOW_INTERCEPT重置,所以這個(gè)標(biāo)志不能攔截down事件。
2.FLAG_DISALLOW_INTERCEPT可以請求不要攔截事件,(move 和 up),前提是ViewGroup不攔截down事件。
②.ViewGroup不攔截事件,分發(fā)給子View的處理
子View接受點(diǎn)擊事件的標(biāo)準(zhǔn):
1子View在播放動(dòng)畫 2.點(diǎn)擊事件坐標(biāo)落在子View內(nèi)
子View如果設(shè)置了onClick事件,則會默認(rèn)設(shè)置onClickable為true 長按事件一樣,
3.View的滑動(dòng)沖突
1.常見滑動(dòng)沖突場景
- 場景1 —— 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致,比如ViewPager中包含ListView;
- 場景2 —— 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致,比如ScrollView中包含ListView;
- 場景3 —— 上面兩種情況的嵌套

2.攔截方法: 外部攔截 內(nèi)部攔截
外部攔截:

在這里,首先down事件父容器必須返回false ,因?yàn)槿羰欠祷豻rue,也就是攔截了down事件,那么后續(xù)的move和up事件就都會傳遞給父容器,子元素就沒有機(jī)會處理事件了。
其次是up事件也返回了false,一是因?yàn)閡p事件對父容器沒什么意義,其次是因?yàn)槿羰录亲釉靥幚淼模瑓s沒有收到up事件會讓子元素的onClick事件無法觸發(fā)。
內(nèi)部攔截:

修改父類的攔截方法:

總結(jié):外部優(yōu)先于內(nèi)部攔截