觸摸事件分發這是我之前寫的一篇事件分發的博客,這篇文章是在看了《Android開發藝術探索》後寫的,書中已經給出了【外部攔截法】和【內部攔截法】的模板代碼,我們可以直接拿來使用即可,書中也給出了看源碼後的重要結論,博客裏我寫了個demo,以打印log的方式驗證了一遍,幫助理解了一遍事件分發的流程。更加詳細內容可參考觸摸事件分發。
本篇博客貼出【外部攔截法】和【內部攔截法】代碼,供大家上手使用,然後給出事件分發機制核心流程的僞代碼。(其實看源碼,不就是揣測源碼意圖,加以驗證的過程嗎,誰能把所有源碼都搞懂呢,關鍵也沒這個必要)
知識點
先上模板代碼
(來自《Android開發藝術探索》)
大神給出的模板代碼,很好理解,理解不了,記住就行了,用的時候,把模板複製進去,只是修改需要按自己需求變的那塊邏輯就好了。
基本知識點
- dispatchTouchEvent 事件分發,這個方法是入口,如果事件能傳遞到給View,則一定會被調用的
- onInterceptTouchEvent 事件攔截,ViewGroup獨有的,如果事件能傳遞到該View,也不一定每次事件都會被調用到,
如果當前View攔截了事件,在同一個事件序列中,沒必要再詢問當前View的onInterceptTouchEvent,如果是當前View的子View攔截了事件,那麼onInterceptTouchEvent會一直被調用,他在時刻等待子View想要交出事件處理權的那一剎那(requestDisallowInterceptTouchEvent())。
注意當前ViewGroup一旦攔截,一次事件序列中就再也不會調用onInterceptTouchEvent了,所以子View再也不會得到事件處理的機會了
爲了解決這個問題,就引出了《嵌套滑動》這個新的事物,見下篇博客
3. onTouchEvent 事件消費,如果返回true,表示消費事件,並導致該View的dispatchTouchEvent返回true
4. 三者關係是通過dispatchTouchEvent方法組織起來的
ViewGroup的dispatchTouchEvent方法
View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果控件可用&&設置了mOnTouchListener,
//&&onTouch方法返回true,該方法直接返回true,
//不去調用onTouchEvent方法
if (mOnTouchListener != null && enable
&& mOnTouchListener.onTouch(ev)) {
return true;
} else {
return onTouchEvent(ev);
}
}
應用
做一個下拉刷新的自定義View
只是最基本的實現,沒有做封裝,沒有做下拉刷新、刷新中等接口回調,最核心原理就是前面說到的,view的滑動+平滑移動+事件分發,到現在這三件套在一起就可以做很多自定義View的效果了
其實onMeasure和onLayout只要你不是直接繼承自View或ViewGroup,一般都是不需要重寫的,我們很懶,也不想處理,所以繼承一個現成的View即可,迫不得已才重寫onMeasure和onLayout
說一個小問題:上文中的SwitchView開關,我們是在LinearLayout裏添加了一個ImageView,如果,你添加的是ImageButton,會發現拖動滑塊後,不動了,這是本節觸摸事件分發的知識點,因爲ImageButton天生可點擊,而LinearLayout默認是不攔截事件的,所以手指觸摸ImageButton時候,會走ImageButton的onTouchEvent方法,LinearLayout裏的onTouchEvent就不生效了,所以不會拖動滑塊,所以:
- 你可以使用ImageView,因爲LinearLayout裏沒有子View能消費事件,會走LinearLayout的onTouchEvent方法,觸摸事件分發博客裏我打印了log就驗證了這個結論
- 或者使用ImageButton後,把他的clickable=false,
- 或者在重寫onInterceptTouchEvent方法,讓LinearLayout永遠攔截事件
上面這點,不注意會經常遇到類似的問題,爲啥拖不動呀,看看你拖的是不是Button,ImageButton這種天生可以消費事件的view
直接上代碼
package com.view.custom.dosometest.view;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
* 描述當前版本功能
*
* @Project: DoSomeTest
* @author: cjx
* @date: 2019-12-01 10:06 星期日
*/
public class RefreshView extends LinearLayout {
private ScrollView mScrollView;
private View mHeader;
private int mHeaderHeight;
private MarginLayoutParams mLp;
public RefreshView(Context context) {
super(context);
init(context);
}
public RefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
setBackgroundColor(Color.GRAY);
post(new Runnable() {
@Override
public void run() {
initView();// 因爲涉及到獲取控件寬高的問題,所以寫到post裏
}
});
}
private void initView() {
if (getChildCount() > 2) {
// 給刷新頭設置負高度的margin,讓他隱藏
mHeader = getChildAt(0);
mHeaderHeight = mHeader.getMeasuredHeight();
mLp = (MarginLayoutParams) mHeader.getLayoutParams();
mLp.topMargin = -mHeaderHeight;
mHeader.setLayoutParams(mLp);
// 得到第二個view,scrollView
View child1 = getChildAt(1);
if (child1 instanceof ScrollView) {
mScrollView = (ScrollView) child1;
}
}
}
float mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = (int) (y - mLastY);
if (needIntercept(deltaY)) {//外部攔截的模板代碼,只要重寫needIntercept方法邏輯就行
//注意當前ViewGroup一旦攔截,一次事件序列中就再也不會調用onInterceptTouchEvent了,
// 所以子View再也不會得到事件處理的機會了
// 爲了解決這個問題,就引出了《嵌套滑動》這個新的事物,見下文
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
mLastY = y;
return intercept;
}
private boolean needIntercept(int deltaInteceptY) {
// mScrollView已經下拉到最頂部&&你還在下來,那麼父容器攔截
if (!mScrollView.canScrollVertically(-1) && deltaInteceptY > 0) {
Log.e("ccc", "不能再往下拉了&&你還在往下拉,父佈局攔截,開始拉出刷新頭");
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float deltaY = y - mLastY;
// 防止刷新頭被無限制下拉,限定個高度
if (mLp.topMargin + deltaY > mHeaderHeight) {
deltaY = mHeaderHeight - mLp.topMargin;
}
// 動態改變刷新頭的topMargin
mLp.topMargin += (int) deltaY;
Log.e("ccc", "y:" + y + "mLastY:" + mLastY + "deltaY:" + deltaY + "mLp.topMargin:" + mLp.topMargin);
mHeader.setLayoutParams(mLp);
break;
case MotionEvent.ACTION_UP:
//鬆手後,看位置,如果過半,刷新頭全部顯示,沒過半,刷新頭全部隱藏
if (mLp.topMargin > -mHeaderHeight / 2) {
smoothChangeTopMargin(mLp.topMargin, 0);
} else {
smoothChangeTopMargin(mLp.topMargin, -mHeaderHeight);
}
break;
}
mLastY = y;
return true;
}
/**
* 使用屬性動畫平滑地過度topMargin
*
* @param start
* @param end
*/
private void smoothChangeTopMargin(int start, int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(start, end);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLp.topMargin = (int) animation.getAnimatedValue();
mHeader.setLayoutParams(mLp);
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
}
}