一、基礎知識
1、View的座標系
View的座標系統是相對於父控件的,如下圖:
getTop(); //獲取子View左上角距父View頂部的距離
getLeft(); //獲取子View左上角距父View左側的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側的距離
getX()、getTranslationX()
Android3.0後增加了:
x、y : 表示View左上角座標。用getX()、 getY()獲得
translationX、translationY : 表示View的左上角相對於父容器的偏移量,
通過 getTranslationX()、getTranslationY()獲得。 默認爲0
其中:
其中:
x = getLeft() + translationX ;
y = getTop() + translationY ;
2、MotionEvent
表示觸摸屏幕產生的一系列事件。常用的有如下三種:
- ACTION_DOWN : 手指剛開始觸摸屏幕,事件的起始位置。
- ACTION_MOVE :手指在屏幕上移動。
- ACTION_UP :手指離開屏幕的瞬間觸發。
從事件開始到結束任意時間內,都可以通過 MotionEvent 內部的 getX/getY和getRawX/getRayY獲得相應座標,兩種方式的區別如下圖:
兩種方式的含義:
event.getX(); //觸摸點相對於其所在組件座標系的座標
event.getY();
event.getRawX(); //觸摸點相對於屏幕默認座標系的座標
event.getRawY();
3、VelocityTracker、GestureDetector、Scroller
①、VelocityTracker速度追蹤
用法如下:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//1000ms內速度
velocityTracker.computeCurrentVelocity(1000);
//x軸方向速度
int xVelocty = (int) velocityTracker.getXVelocity();
//y方向速度
int yVelocty = (int) velocityTracker.getYVelocity();
//釋放
velocityTracker.clear();
velocityTracker.recycle();
速度的單位是: 像素/毫秒(px/ms),eg:100像素/每毫秒
②、GestureDetector 手勢檢測
包含一下方法:
- onDown:觸摸到屏幕
- onShowPress:
- onSingleTapUp:單擊
- onScroll:手指滾動
- onLongPress:長按
- onFling: 手指離開,頁面滑動
③、Scroller
彈性滑動對象,用於實現view的彈性滑動。
二、View的滑動
1、scrollTo/scrollBy
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
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方法實現移動。
特點:
- 無論是scrollTo還是scrollBy都無法改變view的位置,移動的是view的內部位置。
- scrollTo屬於絕對滑動,移動的位置是相對於View的。即:無論移動多少次,位置都是在第一次移動的位置。
- scrollBy屬於相對滑動,移動的位置是相對自己的。即:每次點擊移動,都會相對自己的位置再次移動。
- 移動的距離scrollX和scrollY正負和Android座標系相反。即x移動正100,view的內容向左移動100(不是向右),y移動負100,view內容向下移動100(不是向上)。
2、使用動畫實現view的滑動
★ 使用屬性動畫可以實現view的滑動。
view動畫,不能真正改變動畫的位置。即位置改變了,但是view的事件還留在原來的位置
nineoldandroids動畫兼容庫
3、使用LayoutParams改變位置參數。
可用通過改變view的margin屬性,或者改變父view的padding屬性。實現view的滑動
三、彈性滑動
1、使用scroller
2、使用動畫
3、使用延時策略
四、View的事件分發機制
view的事件分發機制指的是從手指按下屏幕開始,事件從屏幕傳遞到指定view的一系列過程。
1、點擊事件的傳遞規則
View的事件分發其實是對MotionEvent事件的分發過程。
而事件的分發過程由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。
- dispatchTouchEvent(MotionEvent event) :用來分發事件。返回結果受當前view的onTouchEvent和下級View的dispatchTouchEvent方法影響。
- onInterceptTouchEvent(MotionEvent ev) :用來攔截事件。
- onTouchEvent(MotionEvent event) :在dispatchTouchEvent方法中調用,表示是否消耗當前事件
三者之間的關係
viewgroup的事件分發可以用下面僞代碼表示三者之間的關係:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
//當前view是否攔截
if (onInterceptTouchEvent(event)){
//攔截後,則調用自己的onTouchEvent,
//如果onTouchEvent消耗事件則返回true,否則false,交由父控件處理
consume = onTouchEvent(event);
}else {
//如果不攔截,則獲得子view是否消耗
consume = child.dispatchTouchEvent(event);
}
return consume;
}
2、事件分發源碼
當我們點擊屏幕產生事件時,最先接收事件的是Activity。所以事件先從Activity的dispatchTouchEvent開始分發。
1、Activity事件分發
Activity中 dispatchTouchEvent 方法源碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
從上面源碼可以看到,activity事件分發受到Widow的superDispatchTouchEvent方法影響。
可以看到Window是一個抽象方法。註釋方法裏面說它有一個子類PhoneWindow。
可以全局搜索PhoneWindow。找到PhoneWindow的位置,在com.android.internal.policy包中
PhoneWindow
查看PhoneWindow的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看到該方法的返回值又受到 mDecor 中的方法影響。
查看mDecor 聲明的地方
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
從註釋中可以看到,DecorView 是PhoneWindow的頂層視圖。
DecorView
可以看到DecorView 繼承FrameLayout。DecorView 的superDispatchTouchEvent
方法源碼如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
因爲DecorView 繼承自FrameLayout,所以這裏DecorView 調用ViewGroup的dispatchTouchEvent將事件向下傳遞分發。
這個時候我們的事件已經傳遞到了DecorView 了。 傳遞順序如下:
Activity --> PhoneWindow --> DecorView
事件是怎麼從DecorView傳遞到我們自己的Layout中的?
Activity & setContentView()
在Activity中我們通過 setContentView()來加載我們的佈局。源碼如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
可以看到,它會調用PhoneWindow的setContentView()方法來加載我們的佈局文件。
PhoneWindow & setContentView()
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//加載佈局
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
1、當 mContentParent
爲空時,會執行 installDecor()
方法。因爲mContentParent是在installDecor()方法中賦值的,所以一定會先執行installDecor()方法來初始化。
2、當mContentParent不爲空,則移除mContentParent內部的view,將佈局文件添加到mContentParent中。
PhoneWindow & installDecor()
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
可以看到,在installDecor中會初始化mDecor
和 mContentParent
。
而mContentParent = generateLayout(mDecor);
PhoneWindow & generateLayout(mDecor)
從方法名就可以看出來了,這個方法是在mDecor 中生成一個layout佈局。
protected ViewGroup generateLayout(DecorView decor) {
...//省略資源加載
mDecor.startChanging();
//layoutResource 在上面加載過了,省略
//mDecor 加載layoutResource佈局
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//通過findViewById找到contentParent
// int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
mDecor.finishChanging();
return contentParent;
}
佈局文件如下:
在上面的方法中,DecorView會加載layoutResource佈局文件,layoutResource如上圖,通過findviewbyid找到contentParent 控件,也就是上圖紅框代表的FrameLayout。
而我們加載的佈局文件就是放在紅框的contentParent 中。
用一張圖來展示他們之間的層級關係如下:
這個時候我們的事件就傳遞到了ContentParent中了,然後再由ContentParent傳遞到我們佈局文件的最外層View即根View
。
findViewById(id)
這裏面既然用到了findViewById(id)那我們不妨看一下findViewById的源碼:
Activity & findViewById
@Nullable
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
Window & findViewById
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
我的findViewById其實也是在DecorView中查找控件id的
事件從Activity到根View傳遞順序:
Activity -> PhoneWindow -> DecorView -> ContentParent -> 根View
3、根View對點擊事件的分發
①、ViewGroup事件分發
如果根View是ViewGroup,則會調用ViewGroup 的 dispatchTouchEvent方法,
dispatchTouchEvent 攔截部分源碼如下:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//子view是否調用requestDisallowInterceptTouchEvent()
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
默認ViewGroup是不會攔截事件分發的。
可以看到子View可以調用requestDisallowInterceptTouchEvent來影響父view是否攔截。
1、ViewGroup不攔截事件
- 如果viewgroup不攔截事件的話,viewgroup會遍歷所有子view,並調用dispatchTransformedTouchEvent方法,把事件分發給子view。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...//省略部分情況判斷
}
可以看到,當子view不爲空時,如果child是ViewGroup則會再次執行ViewGroup的dispatchTouchEvent。如果子View爲空則執行View的dispatchTouchEvent
view的dispatchTouchEvent
方法放到下面講。
2、ViewGroup攔截事件
如果ViewGroup攔截分發事件,則執行自己的OnTouchEvent()方法。而ViewGroup沒有專門實現自己的OnTouchEvent方法的邏輯,仍然使用的是view的OnTouchEvent邏輯。view的OnTouchEvent方法下面講。
②View的事件分發
上面說viewgroup的事件分發的時候,在ViewGroup的dispatchTouchEvent
方法中,不攔截的話最終會執行view的dispatchTouchEvent
方法。
view的dispatchTouchEvent部分源碼如下:
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
1、可以看到,當view設置了setOnTouchListener
的時候,mOnTouchListener
不爲null,此時view的dispatchTouchEvent方法的返回值受mOnTouchListener.onTouch()
方法影響。
如果在onTouch()
方法中返回true,則view的dispatchTouchEvent方法返回值就爲true。而如果view上面還有ViewGroup,則ViewGroup的dispatchTouchEvent方法也就返回true,則不再繼續分發事件。
2、如果沒有設置setOnTouchListener
或者mOnTouchListener.onTouch()
方法返回false,則執行View的onTouchEvent(event)
方法
View的onTouchEvent
方法
onTouchEvent部分源碼如下:
public boolean onTouchEvent(MotionEvent event) {
...
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
...
switch (action) {
case MotionEvent.ACTION_UP:
...
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
...
}
1、如果給view設置setTouchDelegate()
此時onTouchEvent方法返回值受mTouchDelegate.onTouchEvent(event)
方法影響。
2、在MotionEvent.ACTION_UP的時候,會執行performClick()
方法,即點擊事件的方法。源碼如下:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
當我們給View設置點擊事件的時候,則在此執行mOnClickListener.onClick()
方法。
到這裏一個事件從Activity的dispatchTouchEvent
方法開始分發,一直到View的onClick()
方法響應的整個過程已經分析完了。
View事件優先級總結
dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick
五、View的滑動衝突解決方式
1、外部攔截法
在外部佈局的onInterceptTouchEvent 方法中ACTION_MOVE事件中判斷是否攔截子view的事件,並 在ACTION_UP和ACTION_DOWN中釋放攔截。
僞代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當前事件){
//攔截
intercept = true;
}else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
return intercept;
}
2、內部攔截法
指父容器不攔截任何事件,所有的事件都交給子view處理,如果子view需要就消耗掉,否則交給父容器處理。需要配合requestDisallowInterceptTouchEvent使用。
子元素的dispatchTouchEvent方法如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//屏蔽父容器事件
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當前事件){
//交給父容器處理
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
父容器需要將ACTION_DOWN的攔截事件接觸,不然在需要父容器接收的時候,父容器也沒有地方接收。
父元素修改如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
相比較內部攔截法,外部攔截更加方便,只需要在一個view內做攔截就行了
參考:Android開發藝術探索。
安卓中的座標系