最近空闲的时候喜欢看看之前的东西,温故而知新。发现一个之前忽略的问题。自己学着总结一下。这些年没有自己总结自己的知识体系是最大的失误。
问题
自己在自定义控件一个侧滑控件的时候发现,在一个继承了ViewGroup的自定义控件中,onInterceptTouchEvent没有响应MOVE事件和UP事件。
示例demo中,控件继承的是ViewGroup,
public class SlideView extends ViewGroup{
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.v("qhh_slide", "============================== onInterceptTouchEvent ==============================");
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();
mLastXMove = mXDown;
Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_DOWN mLastXMove " + mLastXMove);
break;
case MotionEvent.ACTION_MOVE: //需要好好总结事件分发
mXMove = ev.getRawX();
mLastXMove = mXMove;
Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_MOVE mLastXMove " + mLastXMove);
float diff = Math.abs(mXMove - mXDown);
if (diff >= mTouchSlop) {
return true;
}
break;
case MotionEvent.ACTION_UP:
Log.d("qhh_slide", ">>> onInterceptTouchEvent ACTION_UP mLastXMove " + mLastXMove);
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("qhh_slide", "============================== onTouchEvent ==============================");
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mXMove = event.getRawX();
int srolledX = (int) (mLastXMove - mXMove);
//Log.i("qhh_move", ">>> getScrollX() = " + getScrollX());
/*if(getScrollX() + mViewWidth >= mRightBorder){
//scrollTo(mLeftBorder,0);
return true;
}else if(getScrollX() + mRightChildW >= mLeftBorder){
//scrollTo(mRightBorder,0);
return true;
}*/
scrollBy(srolledX, 0);
mLastXMove = mXMove;
break;
case MotionEvent.ACTION_UP:
/*int rightThrehold = mViewWidth + mRightChildW / 2;
Log.d("qhh_up", ">>> rightThrehold = " + rightThrehold + " , getScrollX() " + getScrollX());
if (getScrollX() + mViewWidth >= rightThrehold) { //左滑
Log.d("qhh_up", ">>> left");
mScroller.startScroll(getScrollX(), 0, mRightChildW - getScrollX(), 0);
} else {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);
}*/
float xUp = event.getRawX();
float dx = xUp - mXDown;
if(dx < 0){
mScroller.startScroll(getScrollX(), 0, mRightChildW - getScrollX(), 0);
}else{
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);
}
invalidate();
break;
default:
break;
}
return super.onTouchEvent() ;
}
}
单纯是这么写,就会出现 onInterceptTouchEvent中就没有办法响应MOVE事件。具体原因留在后边慢慢梳理分析。
问题分析
聚焦问题,先分析事件在 onInterceptTouchEvent 和 onTouchEvent中的传递关系以及影响。简单的梳理了一下事件传递的流程图,
分析
情形一:
onInterceptTouchEvent 返回值为 false ,则在 onInterceptTouchEvent 中只收到 down 事件,并且将事件传递到自身的 onTouchEvent 。onTouchEvent 返回 false,则事件down之后的事件,该view都不再响应,直接传递到更上一层的View进行处理。反之,返回的是true,则本身自己消费move、up等事件。
情形二:
onInterceptTouchEvent 返回值为 true , 而在 onIntercepterTouchEvent 中还是只收到 down 事件。并且其余的事件则不会往子控件分发传递,会在本身的 onTouchEvent 中进行消费。最后事件在onTouchEvent中的消费逻辑和情形一中的一样。
情形三:
最终会发现,onInterceptTouchEvent 没有响应 move 和 up 事件,这就是文章前面说的问题。但是记得在郭霖大佬的一篇自定义ViewPager的博客中 (https://blog.csdn.net/guolin_blog/article/details/48719871)是可以响应 move 和 up。还是老样子,先来画画图。
通过情形三可以知道,要是想在 ParentView 中 onInterceptorTouchEvent 判断 MOVE 事件,则需要 ChildrenView 中的 onTouchEvent 返回 true 。那么问题来了,如果是 ParentView 和 ChildrenView 都是自己自定义的可以这样实现。但是自己只是负责自定义 ParentView 这一层,ChildrenView 随意放任何控件,该怎么处理呢?
然后看到 郭霖 大佬的博客文章,他确实是在 onInterceptorTouchEvent 中通过判断 MOVE 来拦截事件。但是我这里却拦截不到。这种情况就和子控件有关系了。对比了一下,他的子控件是 Button 而我的是 TextView 。那就来分析分析这两个控件。
Button 和 TextView 的源码分析
看了一下源码,主要关注两个控件的 onTouchEvent 。由于 Button控件是继承自 TextView控件,故而先看看TextView中的 onTouchEvent 。主要看看 onTouchEvent retrun true 的条件是什么。
在 TextView 源码中看到,onTouchEvent 有一处return 和 Editor 有关。
final int action = event.getActionMasked();
if (mEditor != null) {
mEditor.onTouchEvent(event);
if (mEditor.mSelectionModifierCursorController != null
&& mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
return true;
}
}
在看看 Editor 中类的解释
/**
* Helper class used by TextView to handle editable text views.
*
* @hide
*/
在这里看到这个并不是我想要的答案,因为不可能所有的控件都会有这个 Editor ,因此不符合我预期想要的设置。在接着往下看,看到 final boolean superResult = super.onTouchEvent(event);
, TextView 的 super 则是 View 中的 onTouchEvent ,接着跟进。在 View 中的 onTouchEvent 去查找,return true 的条件。
首先第一处看到的是和 clickable 有关的。
public boolean onTouchEvent(MotionEvent event) {
//... 省略无关代码
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//...省略无关代码
return true;
}
}
再看看 CLICKABLE 在源码中的注释:
/**
* Indicates this view can be clicked. When clickable, a View reacts
* to clicks by notifying the OnClickListener.
* {@hide}
*/
static final int CLICKABLE = 0x00004000;
这就是和点击事件有关的变量,然后再想到每个 View 都算是继承 View 的,并且都能通过 setClickable 设置该变量,响应点击事件。
/**
* Enables or disables click events for this view. When a view
* is clickable it will change its state to "pressed" on every click.
* Subclasses should set the view clickable to visually react to
* user's clicks.
*
* @param clickable true to make the view clickable, false otherwise
* @attr ref android.R.styleable#View_clickable
* @see #isClickable()
*/
public void setClickable(boolean clickable) {
setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
}
可以知道,通过 setClickable 设置了 ChildrenView 的 clickable ,则ChildrenView 在 onTouchEvent 中是会返回 true 的。知道了这点,可以在进一步分析 TextView 和 Button 来验证我们的想法。
在 TextView 中的 onTouchEvent 中没有看到可以直接返回 true 的条件,并且看不到设置了 clickable 。Ctrl + F 了一下 clickable 关键字 。在 TextView 的构造方法中发现一段注释。
/*
* Views are not normally clickable unless specified to be.
* However, TextViews that have input or movement methods *are*
* clickable by default. By setting clickable here, we implicitly set focusable as well
* if not overridden by the developer.
*/
a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
boolean clickable = canInputOrMove || isClickable();
boolean longClickable = canInputOrMove || isLongClickable();
int focusable = getFocusable();
注释中解释,除非特殊指定,否则不能够单击Views。所以感觉这里应该是 TextView clickable 是 false。Button 是默认可点击的事件,进入源码看看。 然而 Button 的源码少得可怜。所以,又好奇那里设置了 clickable 呢?
看到构造函数中的注释以及代码,发现 Button 其实是由自己的 styles 的,但是点不进去,所以需要借助源码查看工具。使用线上 http://androidxref.com
找了一番,以及上网搜索了一番,该 style 在源码的路径
/frameworks/base/core/res/res/values/themes.xml
搜索找到 buttonStyle
- @style/Widget.Button
在找到 Widget.Button ,路径在:/frameworks/base/core/res/res/values/styles.xml
至此,可以看到 Button 实际是在 style 文件中配置了 clickable 为 true 。
实际代码操作验证
在文章中最开始的 SlideView demo中修改一下代码。在 onLayout 的时候给每个子view设置了 clickable 。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("qhh", "onLayout changed " + changed);
if (changed) {
int childCount = getChildCount();
int previousWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
/*childView.layout(i * childView.getMeasuredWidth() + previousWidth, 0,
(i + 1) * childView.getMeasuredWidth() + previousWidth,
childView.getMeasuredHeight());*/
childView.setClickable(true);
childView.layout(previousWidth, 0,
childView.getMeasuredWidth() + previousWidth,
childView.getMeasuredHeight());
previousWidth += childView.getMeasuredWidth();
}
mLeftBorder = getChildAt(0).getLeft();
mRightBorder = getChildAt(childCount - 1).getRight();
mRightChildW = getChildAt(childCount - 1).getWidth();
mViewWidth = getWidth();
Log.i("qhh_move", ">>> mLeftBorder = " + mLeftBorder);
Log.i("qhh_move", ">>> mRightChildW = " + mRightChildW);
Log.i("qhh_move", ">>> width = " + mViewWidth);
}
}
这样之后,在 ParentView 中 onIntercepterTouchEvent 返回 false 的情况下可以响应到 move等事件。
参考的文章
https://blog.csdn.net/Zheng548/article/details/84028561
https://blog.csdn.net/xiayu98020214/article/details/80277835
https://blog.csdn.net/guolin_blog/article/details/48719871