android開發藝術探索——第三章View的事件體系
3.1
View的基礎知識點
(一) View 和 ViewGroup
Veiw 是Android中所有控件的基類。View是一種界面層的控件的抽象。
ViewGroup也是繼承之View,翻譯爲控件組。ViewGroup內部包含了許多控件。所以View本身可以是單個控件也可以是由多個控件組成的一個控件。
例:
LinearLayout不但是一個View還是一個ViewGroup,而ViewGroup內部可以有子View的,這個子View同樣還可以是ViewGroup。
(二)View的位置參數
View的位置由四個頂點來決定,分別對應於四個屬性:top,left,right,bottom。這四個參數都是相對於Veiw的父容器來說的,是相對座標。
(三)MotionEvent 和 TouchSlop
1.MotionEvent
· ACTION_DOWN 手指剛接觸屏幕
· ACTION_MOVE 手指在屏幕上移動
· ACTION_UP 手指從屏幕上鬆開的一瞬間
通過MotionEvent對象可以得到點擊事件發生的x 和 y座標。系統提供了兩組方法:
getX/getY 和 getRawX/getRawY
區別:
getX/getY返回的是相對於當前View的左上角的x和y座標;
getRawX/getRawY返回的是相對於屏幕的左上角的x和y的座標
2.TouchSolp
系統所能識別出的被認爲是滑動的最小距離。也就是說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小於這個常量,
系統會認爲這不是一次滑動。需要注意的是,不同的
設備這個常量值可能是不相同的。可通過如下的方式獲取這個常量值:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
(四)VelocityTracker、GestureDetector和Scroller
1.VelocityTracker
速度跟蹤,用於追蹤手指在滑動過程中的速度,包括水平和垂直方向的速度。
首先,在View的onTouchEvent方法中追蹤當前的點擊事件的速度:
>
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracler.addMovement(event);
接着,
>
velocityTracker.computeCurrentVelocity(1000);
int xVelocity= (int)velocityTracker.getXVelocity();
int yVelocity= (int)velocityTracker.getYVelocity();
這裏需要注意兩點:
第一: 獲取速度之前必須先計算速度,即必須先調用computeCurrentVelocity(1000)之後是getXVelocity(),getYVelocity()
第二: 這裏的速度是在指定時間內劃過的像素數
例:將時間間隔設爲1s,在1s內,手指在水平方向從左向右滑動100像素,水平速度就是100;
速度也可以爲負數。當手指從右向左滑過100像素,水平方向的速度就是負值。
公式表示:
速度 = (終點位置-起始位置)/ 時間段
最後,當不需要的時候,需要調用clear方法重置並回收內存。
>
velocityTracker.clear();
velocityTracker.recycle();
2.GestureDetector
手勢檢測,用於輔助檢測用戶單擊、滑動、長按和雙擊等行爲。(如果只是監聽滑動相關,建議在onTouchEvent;
如果是雙擊,需要使用GestureDetector)
首先,需要創建一個GestureDetector對象實現onGestureListener接口,
這裏根據需要還可以實現OnDoubleTapListener 實現雙擊監聽
>
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕後無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false);
接着,接管目標的View的OnTouchEvent方法,在待監聽的View的OnTouchEvent中添加:
>
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然後,可以有選擇地實現OnGestureListener 和 OnDoubleTapListener中的方法
3.Scroller
Socroller本身無法讓View彈性滑動,需要和View的computeScroll配合使用
>
Scroller scroller = new Scroller(this);
//緩慢滑到指定位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta= destX - getScrollX();
//1000ms內滑向destX,效果就是慢慢滑動
mScroller.startScroll(scrollX,0 , delta,0 , 1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postiInvalidate();
}
}
3.2
View的滑動
一般View的滑動通過三種方式實現:
第一種通過View本身的提供的scrollTo/scrollBy方法實現
第二種通過動畫給View施加平移效果實現滑動
第三種通過改變View的LayoutParams使得View重新佈局從而實現滑動
(一)使用scrollTo/scrollBy
public void scrollTo(int x,int y){
if( mScrollX != x && mScrollY != null){
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX,,mScrollY,oldX, oldY);
if( !awakenScrollBars()){
postInvalidateOnAnimation();
}
}
}
/*************************************/
public void scrollBy(int x , int y){
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy()調用scrollTo()方法。scrollTBy()基於當前位置的相對滑動,scrollTo()實現了基於
所傳遞參數的絕對滑動。
* 注意:*
使用scrollTo()和scrollBy()來實現View的滑動,只能將View上的內容進行移動,
並不能將View本身進行移動。
無論怎麼滑動都不可能將當前的View滑動到附近的View所在區域。
(二)使用動畫
使用動畫主要是操作View的transklationX和transklationY屬性
動畫代碼,此動畫在100ms內將一個View從原始位置向右下角移動100個像素。
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
adnroid:duration = "100"
android:fromXDelta = "0"
android:fromYDelta = "0"
android:interpolator = "@android:anim/linear_interpolator"
android:toXDelta = "100"
android:toYDelta = "100" />
</set>
屬性動畫代碼
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
View的動畫是對View的影像做操作,並不能真正改變View的位置參數,包括寬/高,
如果希望保留動畫後的狀態,將
fillAfter設置爲true;
(三)改變參數佈局
MarginLayoutParams params = (MarginLayoutParams)mButtion1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;0
mButtion1.requestLayout();
//或者mButton1.setLayoutParams(params)
(四)對比
-scrollTo/scrollBy : 操作簡單,適合對View內容的滑動
-動畫 : 操作簡單,適合用於沒有交互的View和實現複雜的動畫效果
-改變佈局參數 : 操作稍微複雜了點,適用於有交互的View
3.3彈性滑動
(一)使用Scroller
Scroller源碼:
Scroller scroller = new Scroller(mContext);
//緩慢滾動到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int deltaX = destX - srollX;
//1000ms內滑向destX,效果就是慢慢滑動
mScroller.startScroll(scrollX,0,deltaX,1,1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
3.4
View的事件分發
(一)點擊事件的傳遞規則
* public boolean dispatchTouchEvent(MotionEvent ev) *
如果事件能夠傳遞給當前View,此方法一定會被調用,返回結果受當前View的oTouchEvent 和
下級View的dispatchTouchEvent 方法的影響,表示是否消耗當前事件
* public boolean onInterceptTouchEvent(MotionEvent event) *
在上述內部調用,用來判斷是否攔截某個事件,如果當前View攔截某個事件,
那麼同一事件序列當中,此方法不會再調用,返回結果表示是否攔截當前事件
* public boolean onTouchEvent(MotionEvent event) *
在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的onInterceptTouchEvent方法返回false就表示不攔截當前事件,
這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會調用,如此反覆,直到事件被處理。
當一個View需要處理事件時,如果它設置了onTouchListener,onTouchListener中的onTouch方法會被回調。這件事如何處理還要
看onTouch的返回值,如果返回false。則當前View的onTouch方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。
由此可見,給View設置onTouchListener,其優先級比onTouchEvent要高。在onTouch方法中,如果設置的有onClickListener,那麼
onClick方法會被調用。
點擊事件的傳遞順序: activity --> window --> View
事件分佈的源碼分析
(一)Activity對點擊事件的分佈 過程
點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳遞給當前的Activicity,由Activity的disaptchTouchEvent
來進行事件的派發,具體的工作是由Activcity內部的Window來完成的。Window會將事件傳遞給decor view, decor view一般就是當前
界面的底層容器,通過Activity.getWindow.getDecorView()獲得
源碼:Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent (ev)){
return true;
}
return onTouchEvent(ev);
}
事件開始交給Acitivity所屬的Winow進行分佈,如果返回true,整個事件循環就結束,返回false
意味着事件沒人處理,所有View的onTouchEvent都返回了false,Activity的onTouchEvent會調用。Window
將事件傳遞給ViewGroup的。
Window是個抽象類,Window的dispatchTouchEvent是個抽象方法,實現類是PhoneWindow。
源碼:PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.superDispatchTouchEvent(ev);
}
PhoneWindow將事件直接傳遞給了DecorView。
通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
獲得Activity所設置的View,這個mDecor就是getWindow().getDecorView()返回的View,通過setContentView
設置的View就是mDecor的子View。目前事件傳遞到了這裏。
(二)頂級View 對點擊事件的分發過程
View事件分發回顧:
點擊事件達到頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的dispatchTouchEvent方法;
此時,
· 如果頂級ViewGroup攔截事件onInterceptTouchEvent返回 true,則事件由ViewGroup本身處理。
這時如果ViewGroup的mOnTouchListener被設置,則OnTouch被調用,否則OnTouchEvent會被調
用。也就是說如果能提供的話,onTouch會屏蔽掉onToucheEvent。在onTouchEvent中,如果設
置了mOnClickListener,則onClick會被調用。
· 如果頂級ViewGroup不攔截事件,則事件會傳遞到它所在的點擊事件鏈上的子View,這個時候子
View的dispatchTouchEvent會被調用。
(三)View對點擊事件的處理過程
* 分發過程 和 處理過程第一遍沒看懂*
View的setClickable 和 setOnLongClickListener會自動將View的 CLICKABLE 和 LONG_CLICKABLE屬性
設置爲true。
3.5
View的滑動衝突
(一)常見的滑動衝突場景
常見的滑動衝突場景,簡單分爲三種:
- 場景1——外部滑動方向和內部滑動方向不一致
- 場景2——外部滑動方向和內部滑動方向一致
- 場景3——上面兩種情況的嵌套
(二)滑動衝突的解決方式
* 針對場景1的解決方式 *
1.外部攔截
所謂外部攔截是指事情都經過父容器的攔截處理,如果父容器需要此事件就攔截;
如果不需要就不攔截。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在
內部做相應的攔截處理。
僞碼如下:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
intercepted = true;
break;
case MotionEvent.Move:
if(父容器需要當前點擊事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
在onInterceptTouchEvent方法中,首先是在ACTION_DOWN,父容器必須返回false,即不攔截
ACTION_DOWN,ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,這個時候事件沒法再傳遞
子元素了;其次是ACTION_MOVE事件,這個事件可以根據需求決定是否需要攔截,如果父容器需要
攔截就返回true,否則返回false;最後是ACTION_UP事件,必須要返回false,因爲ACTION_UP時事
件本身沒有啥意義。
2.內部攔截
內部攔截法指父容器不攔截任何事件,所有的事件都傳給子元素,如果子元素需要此事件就
直接消耗掉,若不需要,就交由父容器進行處理,這種方法和Andorid中的事件分發機制不一致,
需要配合requestDisallowInterceptTouchEvent方法才能正常工作m,使用起來外部攔截稍顯複雜。
僞代碼如下:
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.requestDisallowInterceptTopuchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
break;
}
mLastX = x ;
mLastY = y ;
return super.dispatchTouchEvent(event);
}
當面對不同的滑動策略時只需要修改裏面的條件即可,其他不需要做改動也不能動。除了
子元素需要做處理以外,父元素也要默認攔截了 ACTION_DOWN 以外的其他事件,這樣當子元素調用parent.requestDisalowInterceptTouchEvent(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;
}
}