Android View的事件传递机制一直都是开发者绕不开的一个知识点,即使你工作中不需要处理事件冲突,但是面试官总喜欢拿出来问。
我以前也是在里面挣扎过一段时间,说明白吧但好像有些地方总是模模糊糊,说不明白吧,却也能说出个大概来。后来也是反复跳坑研究忽然就顿悟了,而且悟了之后我深刻明白了之前迷惑自己认清真相的点在哪里,今天就斗胆用踩坑过来人的方式写一篇讲解事件传递的博文,如果能给新进的开发者提供点帮助那也是美事一件。
Android事件的传递无非就是ViewGroup和它的子孩子View之间的逻辑关系,要搞明白他们之间的事件分发,看源码必然不可少,在这之前首先强调几个重要且容易模糊的知识点:
-
ViewGrop的子View是指ViewGrop容器里装的childView
-
ViewGrop本身是View的子类(注意是子类,不是上面说的子View!!)
-
处理事件和拦截事件是两码事,你可以处理事件但不拦截它
-
事件有多个动作,down,move,up这些,注意每次动作都是会走一遍分发代码
-
dispatchTouchEvent方法主要负责分发拦截逻辑,viewGroup写好了这个方法,一般不需要开发者重写;onTouchEvent方法负责具体的事件动作逻辑,它的返回值跟dispatchTouchEvent一样决定事件是否继续往下传,这是比较容易让人记忆错乱的地方。
着重强调上面概念,是因为笔者自己以前在分析中看到子类和子View的时候没有额外注意,导致后面很多逻辑不知不觉就迷糊了。
为了使分析流程简单化,我们假定ViewGrop是LinearLayout,子View就是一个TextView。我们分析好LinearLayout传递到TextView的事件逻辑就明白了无论多少层级的View都是一样的逻辑,因为多层逻辑无非就是重复这一层关键逻辑。
我们向上追溯到Activity开始事件的传递(至于事件怎么传递到activity的有兴趣的自行研究),然后Activity会把事件传递到phoneWindow,phoneWindow传递到decorView,decorView传递到我们的例子LinnearLayout上,再传到TextView上。
部分代码如下,首先是Activity里的事件分发:
/**
* ActivitydispatchTouchEvent
**/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//空实现,若需要了解用户点击界面,可以自行实现该接口
// 这里是页面down事件的必经之路,可以在这里统计页面点击等情况
onUserInteraction();
}
//这个getWindow()得到的就是phoneWindow
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果页面上的控件都没有拦截事件,则会走到activity的onTouchEvent
return onTouchEvent(ev);
}
再看看phoneWindow的superDispatchTouchEvent:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//可以看到其实是交给了mDecor去处理,这个mDecor看名字就知道是DecorView
return mDecor.superDispatchTouchEvent(event);
}
继续跟踪DecorView的superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event) {
//调用的是父类的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
然而我们知道DecorView就是个FrameLayout,FrameLayout本身没有重写dispatchTouchEvent,所以这个就是ViewGroup的dispatchTouchEvent方法。
接下来事件的传递都是ViewGroup之间,直至最后到我们上面的例子TextView上。
以上的事件传递顺序不是重点,目的只是说一下事件的来源。我们重点分析之前说的例子,LinearLayout上的点击事件是怎么传递到他的子View TextView上的,也即是ViewGroup和子View的事件关系。
ViewGroup的dispatchTouchEvent方法的源码太多,没必要一次性贴出来,我们逐步分析,免得懵逼,结合官方的注释来捋逻辑,首先说一下MotionEvent的主要的几个类型,就是DOWM、MOVE、UP:
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
我们以典型的手指摁下→然后移动小短距离→最后抬起这一次事件为例,实际传递过程中触发了DOWM、MOVE、UP三次MotionEvent 的ViewGroup的dispatchTouchEvent(MotionEvent ev)方法,第一次是Down事件,我们看一段ViewGroup的dispatchTouchEvent(MotionEvent ev)方法里的源码:
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down. 处理初始化的down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//开始一次新的事件的时候丢弃掉之前所有的状态,down事件标志此时是一次新的点击事件
//framework层可能由于应用程序切换、ANR 或其他一些状态更改,放弃了上一次手势的up或者cancel事件
cancelAndClearTouchTargets(ev); // 清除掉点击对象
resetTouchState(); // 重置拦截标志
}
// Check for interception.
// 检查interception参数,这里梳理viewGroup是否拦截的逻辑
final boolean intercepted;
// 第一个是DOWN事件,则肯定会进入if语句里,mFirstTouchTarget这个参数很重要,后面会多次提到
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// disallowIntercept 代表ViewGroup是否允许拦截,这个参数一般由子View调用
//requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法以达到控制事件
//冲突的目的,但是这个标志会在每次的down事件都清除,就是前面的resetTouchState()方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 由上面的代码可知,只要是down事件,都会执行重置这个标志的动作,也就是说这里必然是false,
// 这个概念非常重要,说明每次事件的开始的down动作都会进入到onInterceptTouchEvent()方
//法里,父View有权利在事件的一开始就决定要不要传给孩子View
if (!disallowIntercept) {
// 如果不是down事件disallowIntercept也为false,代表子View允许ViewGroup拦截事
//件,所以会走ViewGroup的onInterceptTouchEvent方法,很多事件冲突就是在这处理的
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
//注意这句英文很重要:如果没有点击的目标(可以理解成没有子view消费事件),并且
//不是初始化的Down事件,那么viewGroup就会一直拦截它
intercepted = true;
}
这里贴一下requestDisallowInterceptTouchEvent(boolean disallowIntercept)这个方法,这是一个公开方法,一般被子view调度处理事件冲突:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// disallowIntercept 为true表示孩子view不希望这个viewGroup拦截事件,那么
// (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0则会是true,对应上面viewGroup的那个判断
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
// 根据情况处理这些标记,一些或与运算,常用来作为标记状态
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
// 这个方法还会向上层的viewGroup传递,可见权限非常大
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
以上是down事件和interecepted初步拦截逻辑处理,我们假设ViewGroup的onInterceptTouchEvent()方法不拦截事件,也就是intercepted为false,点击事件继续往下传,代码逻辑会走到这里:
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
// 这是针对焦点处理相关的,我们不在这里深究
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// ACTION_POINTER_DOWN指的是已经有手指在屏幕上的down事件,即多指触控
// ACTION_HOVER_MOVE是鼠标移动、
// 我们主要关注ACTION_DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// always 0 for down,down事件的index总是0
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
// newTouchTarget此时必然是null,childrenCount 不为零,代表有子View
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
// 从上到下扫描孩子View,找到可以接收事件的孩子View
// buildTouchDispatchChildList()方法会把所有的子View按照z轴的大
//小排序在一个list里,z轴越大的View越靠后,用到了插入排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
// 会根据绘制顺序来找孩子View响应事件的优先级
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 排除掉不可响应的view和不在事件坐标范围内的view
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 这里普通down事件newTouchTarget肯定还是null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 重点,开始分发事件了,这个方法后面会单独讲,先走主流程!!
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
// 进到这里表示孩子view想要在它的大小范围内响应事件
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 将当前孩子View设置为mFirstTouchTarget,newTouchTarget 此时不为空了
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
// 没有找到接收事件的孩子。
// 将newTouchTarget 分配给最近添加的目标。
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
上面说到了一个插入排序,感兴趣的可以看看这篇文章:常见排序算法
接着往主干代码走:
// Dispatch to touch targets.
// 注意,down的事件在上面的代码里走过一次了,到了这里则有两种情况,
// 1.mFirstTouchTarget 还是为null,代表事件没被孩子View消费,则不管此时
//是什么事件,都将由viewGroup自己去处理该事件
// 2.mFirstTouchTarget 不为null,代表事件down被孩子view拦截了
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//没有触摸目标就把这个viewGroup当作普通的view处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 进入到这里,可能是ACTION_DOWN事件,也可能是其它类型的事件
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
// 分发到触摸目标,排除新的触摸目标,如果我们已经分发了事件
// 如有必要,取消触摸目标。
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 这里是处理了down的事件,alreadyDispatchedToNewTouchTarget在上面被置为了true
handled = true;
} else {
// 处理其他事件,这时候alreadyDispatchedToNewTouchTarget为false
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 继续分发下去给子view处理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
// up等cancel事件 重置状态
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
回过头再来看看 dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)方法,这里处理了分发给孩子view事件和孩子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) {
// viewGroup自己处理,调用父类(注意是父类,不是父亲view)view的分发方法
handled = super.dispatchTouchEvent(event);
} else {
// 分发下去给孩子view处理,并且拿到结果是否拦截
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...略
return handled;
}
再来看看View的dispatchTouchEvent()和onTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//重点,有设置的mOnTouchListener的会先调用
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//重点,调用onTouchEvent,我们耳熟能详的onClick点击事件是在onTouchEvent()方法里的up事件触发的
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
View的onTouchEvent(MotionEvent event)方法
public boolean onTouchEvent(MotionEvent event) {
...略
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...略
if (!post(mPerformClick)) {
performClickInternal(); // 点击事件!!!!
}
}
}
...略
下面来个处理事件冲突的例子,一个ViewPager + ListView,ViewPager能左右滑动,ListView支持上下滑动。
ViewPager :
package com.hans.viewexe.views
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Scroller
import kotlin.math.absoluteValue
/**
* @author: lookey
* @date: 2021/12/19
*/
class DefinedViewPager:ViewGroup {
private var mLastX: Float = 0f
private var mLastY: Float = 0f
private var mChildWidth: Int = 0
private var mChildrenSize: Int = 0
private var currentPage = 0
private var isNextMove = true
private var mScroller:Scroller = Scroller(context)
companion object{
private const val TAG: String = "DefinedViewPager"
}
constructor(context: Context, attributes: AttributeSet):super(context,attributes)
var eventX = 0f
var eventY = 0f
var eventDownX = 0f
var eventDownY = 0f
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
val x = event.x.toInt()
val y = event.y.toInt()
val action = event.action
return if (action == MotionEvent.ACTION_DOWN) {
Log.d(TAG, "父View onInterceptTouchEvent ACTION_DOWN")
eventDownX = event.x
eventDownY = event.y
mLastX = x.toFloat()
mLastY = y.toFloat()
if (!mScroller.isFinished) {
mScroller.abortAnimation()
return true
}
false // 只是记录一下坐标,不拦截
} else {
// 不是down事件,此时还能进来onInterceptTouchEvent()方法则表示此时是ListViewEx故意放给父
// View处理的,这里就返回true,拦截掉自己处理,这样才会走到自己的onTouchEvent(event: MotionEvent)方法
true
}
}
val TAG = "DefinedViewPager"
// 具体事件的效果实现
override fun onTouchEvent(event: MotionEvent): Boolean {
eventX = event.x //相对父view的x坐标
eventY = event.y //相对父view的y坐标
// down事件是不会流转到这里的,无需处理
when(event.action){
MotionEvent.ACTION_MOVE -> {
//防止向左滚动过头
if((scrollX <= 0 && (eventX - mLastX) > 0) || (eventX - mLastX) > scrollX){
mLastX = eventX
return true
}
//防止向右滚过头
if((scrollX >= mChildWidth*(childCount-1) && (eventX - mLastX) < 0) || (mLastX - eventX) + scrollX > mChildWidth*(childCount-1)){
mLastX = eventX
return true
}
val deltaX = eventX - mLastX
scrollBy(-deltaX.toInt() , 0)
}
//手离开屏幕的时候判断根据move时候的滑动方向和滑动距离判断要不要翻过当前页
MotionEvent.ACTION_UP -> {
Log.d("11scrollX","${event.x - eventDownX}")
val deltaX2 = event.x - eventDownX
if(deltaX2.absoluteValue > 200){ //防止误触
if(deltaX2 < 0){
Log.d("22scrollX ==>","next下一页")
moveToNextPage()
}else if(deltaX2 > 0){
Log.d("22scrollX ==>","pre上一页")
moveToPrePage()
}
}else{
restorePosition()
}
}
}
mLastX = event.x
mLastY = event.y
return true
}
private fun restorePosition() {
val targetScrollX = currentPage * mChildWidth
smoothScroll(targetScrollX - scrollX)
}
private fun moveToNextPage(){
if(currentPage == childCount - 1){
return
}
currentPage ++
val targetScrollX = currentPage * mChildWidth
smoothScroll(targetScrollX - scrollX)
}
private fun moveToPrePage(){
if(currentPage == 0){
return
}
currentPage --
val targetScrollX = currentPage * mChildWidth
smoothScroll(targetScrollX - scrollX)
}
private fun smoothScroll(dx:Int){
mScroller.startScroll(scrollX,0,dx,0,500)
invalidate()
}
override fun computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.currX,mScroller.currY)
postInvalidate()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//调用viewGroup提供的测量子View的方法
measureChildren(widthMeasureSpec,heightMeasureSpec)
var widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec)
var widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
var heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
if(childCount == 0){
setMeasuredDimension(0,0)
}else if(heightSpecMode == MeasureSpec.AT_MOST){
val childView = getChildAt(0)
val measuredHeight = childView.measuredHeight
setMeasuredDimension(widthSpaceSize,measuredHeight)
}else if(widthSpecMode == MeasureSpec.AT_MOST){
val childView = getChildAt(0)
val measuredWidth = childView.measuredWidth
setMeasuredDimension(measuredWidth,heightSpecSize)
}else{
val childView = getChildAt(0)
val measuredWidth = childView.measuredWidth * childCount
val measuredHeight = childView.measuredHeight
setMeasuredDimension(measuredWidth,measuredHeight)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
Log.d(TAG,"width:${width}")
var childLeft = 0
val childCounts = childCount
mChildrenSize = childCounts
for (index in 0 until childCounts){
val childView = getChildAt(index)
if(childView.visibility != View.GONE){
val childWidth = childView.measuredWidth
mChildWidth = childWidth
childView.layout(childLeft,0,childLeft + childWidth,childView.measuredHeight)
childLeft += childWidth
}
}
}
}
ListView:
package com.hans.viewexe.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ListView;
/**
* @author: lookey
* @date: 2021/12/19
*/
public class ListViewEx extends ListView {
private static final String TAG = "ListViewEx";
private DefinedViewPager mHorizontalScrollViewEx2;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
public ListViewEx(Context context) {
super(context);
}
public ListViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListViewEx(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setHorizontalScrollViewEx2(
DefinedViewPager definedViewPager) {
mHorizontalScrollViewEx2 = definedViewPager;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d(TAG, "孩子 dispatchTouchEvent ACTION_DOWN");
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true); //父容器不拦截
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);//父容器可以拦截
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.hans.viewexe.views.DefinedViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
activity:
package com.hans.viewexe
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.MotionEvent
import android.widget.AdapterView.OnItemClickListener
import android.util.Log
import android.view.ViewGroup
import com.hans.viewexe.views.HorizontalScrollViewEx2
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import com.hans.viewexe.utils.MyUtils
import com.hans.viewexe.views.DefinedViewPager
import com.hans.viewexe.views.ListViewEx
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private var mListContainer: DefinedViewPager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.demo_2)
Log.d(TAG, "onCreate")
initView()
}
private fun initView() {
val inflater = layoutInflater
mListContainer = findViewById(R.id.container)
val screenWidth: Int = MyUtils.getScreenMetrics(this).widthPixels
val screenHeight: Int = MyUtils.getScreenMetrics(this).heightPixels
for (i in 0..2) {
val layout = inflater.inflate(
R.layout.content_layout2, mListContainer, false
) as ViewGroup
layout.layoutParams.width = screenWidth
val textView = layout.findViewById<View>(R.id.title) as TextView
textView.text = "page " + (i + 1)
layout.setBackgroundColor(
Color
.rgb(255 / (i + 1), 255 / (i + 1), 0)
)
createList(layout)
mListContainer!!.addView(layout)
}
}
private fun createList(layout: ViewGroup) {
val listView: ListViewEx = layout.findViewById(R.id.list)
val datas = ArrayList<String>()
for (i in 0..49) {
datas.add("name $i")
}
val adapter = ArrayAdapter(
this,
R.layout.content_list_item, R.id.name, datas
)
listView.setAdapter(adapter)
listView.setHorizontalScrollViewEx2(mListContainer)
listView.setOnItemClickListener(OnItemClickListener { parent, view, position, id ->
Toast.makeText(
this, "click item",
Toast.LENGTH_SHORT
).show()
})
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
Log.d(TAG, "dispatchTouchEvent action:" + ev.action)
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
Log.d(TAG, "onTouchEvent action:" + event.action)
return super.onTouchEvent(event)
}
}
网友评论