面试的时候,被问到如何让View 和 其 父View 同时响应长按事件。
我还记得当时自己的回答,子View 里面 处理了长按事件,但是返回了false, 没有处理,然后会继续调用到父View 的长按事件。
回头自己看了事件传递机制的源码之后,有了更好的方法:
上布局:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:id="@+id/linearlayout"
android:background="@color/colorPrimaryDark"
>
<com.example.view.CustomView
android:background="@color/colorAccent"
android:id="@+id/customView"
android:layout_width="match_parent"
android:layout_height="100dp"/>
</LinearLayout>
父View 和 子View 同时设置长按事件:
view.findViewById(R.id.linearlayout).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.d("ceshi2","linearlayout setOnLongClickListener");
return true;
}
});
view.findViewById(R.id.customView).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.d("ceshi2","customView setOnLongClickListener");
return true;
}
});
关键在于子View 的onTouch ,要自己影响本次事件的同时,传递给父View 响应:
package com.example.view;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
/**
* =======================================================================================
* 作 者:caoxinyu
* 创建日期:2019/4/16.
* 类的作用:
* 修订历史:
* =======================================================================================
*/
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
ViewParent parent = getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).onTouchEvent(event);
}
return super.onTouchEvent(event);
}
}
log 如下:
2019-04-16 11:55:01.705 19974-19974/com.pipiyang.cn03 D/ceshi2: linearlayout setOnLongClickListener
2019-04-16 11:55:01.706 19974-19974/com.pipiyang.cn03 D/ceshi2: customView setOnLongClickListener
可以看到两个View 的长按事件都响应了。
第二种方法,就是我面试的时候的思路,是可以的。如下:
package com.example.view;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
/**
* =======================================================================================
* 作 者:caoxinyu
* 创建日期:2019/4/16.
* 类的作用:
* 修订历史:
* =======================================================================================
*/
public class CustomView2 extends View {
public CustomView2(Context context) {
super(context);
}
public CustomView2(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d("CustomView2", event.toString());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("CustomView2", event.toString());
super.onTouchEvent(event);
return false;
}
}
这里就是执行了super.onTouchEvent(event); 但是return 的false. 可以看到View 和 View Parent 都响应了长按事件,但是没有响应点击事件。没有点击事件是因为,因为当前的View onTouchEvent 返回了false,所以以后的事件Move,Up 都不会再传递给它,二performOnclick 是在onAction_Up 的时候执行的,所以不会执行点击事件。
可以接收到长按事件是因为调用了View 的 onTouchEvent,如果发现是Action_Down 在里面会postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
延迟发送一个长按的Runable,时间到了就会执行下面的代码:
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
如果中间你一直按着,那么执行到这个runable 里面去的时候,是可以执行performLongClick 方法的。
但是你如果放手了,虽然会执行到这个runable 里面去,但是mOriginalPressedState == isPressed() 是返回的false,因为当父布局收到Action_Up 的时候,会传递给子View,当前是非press 状态。所以这时候也不会执行长按事件。
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false);
}
}
@Override
protected void dispatchSetPressed(boolean pressed) {
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = 0; i < count; i++) {
final View child = children[i];
// Children that are clickable on their own should not
// show a pressed state when their parent view does.
// Clearing a pressed state always propagates.
if (!pressed || (!child.isClickable() && !child.isLongClickable())) {
child.setPressed(pressed);
}
}
}
所以还是推荐第一种。