MotionLayout 阻止所有视图上的 ClickListener

MotionLayout prevents ClickListener on all Views

我在场景中使用 MotionLayout-xml:

<Transition
    motion:constraintSetStart="@+id/start"
    motion:constraintSetEnd="@+id/end"
    >
    <OnSwipe
        motion:touchAnchorId="@+id/v_top_sheet"
        motion:touchRegionId="@+id/v_top_sheet_touch_region"
        motion:touchAnchorSide="bottom"
        motion:dragDirection="dragDown" />
</Transition>

2 个 ConstraintSets 仅引用 2 个视图 ID:v_notifications_containerv_top_sheet

在我的 Activity 中,我想将普通的 ClickListener 设置为此 MotionLayout 中的其他视图之一:

iv_notification_status.setOnClickListener { Timber.d("Hello") }

此行已执行,但从未触发 ClickListener。我搜索了其他帖子,但大多数帖子都涉及在 motion:touchAnchorId 的同一视图上设置 ClickListener。这里不是这种情况。 ClickListener 设置为在 MotionLayout 设置中未曾提及的视图。如果我删除 app:layoutDescription 属性,则点击有效。

我也试过用setOnTouchListener,但也从来没有调用过。

如何在 MotionLayout 中设置点击侦听器?

要在视图上设置 onClick 操作,请使用:

android:onClick="handleAction"

在 MotionLayout 文件中,并在 class.

中定义 "handleAction"

this great medium article 的帮助下,我发现 MotionLayout 正在拦截点击事件,即使运动场景只包含一个 OnSwipe 转换。

所以我编写了一个自定义的 MotionLayout 来仅处理 ACTION_MOVE 并将所有其他触摸事件传递到视图树中。很有魅力:

/**
 * MotionLayout will intercept all touch events and take control over them.
 * That means that View on top of MotionLayout (i.e. children of MotionLayout) will not
 * receive touch events.
 *
 * If the motion scene uses only a onSwipe transition, all click events are intercepted nevertheless.
 * This is why we override onInterceptTouchEvent in this class and only let swipe actions be handled
 * by MotionLayout. All other actions are passed down the View tree so that possible ClickListener can
 * receive the touch/click events.
 */
class ClickableMotionLayout: MotionLayout {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
        if (event?.action == MotionEvent.ACTION_MOVE) {
            return super.onInterceptTouchEvent(event)
        }
        return false
    }

}

@muetzenflo 的回复是我迄今为止看到的最有效的解决方案。

但是,仅检查 Event.Action 是否有 MotionEvent.ACTION_MOVE 会导致 MotionLayout 响应不佳。最好使用 ViewConfiguration.TapTimeout 来区分移动和单击,如下例所示。

public class MotionSubLayout extends MotionLayout {

    private long mStartTime = 0;

    public MotionSubLayout(@NonNull Context context) {
        super(context);
    }

    public MotionSubLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MotionSubLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
            mStartTime = event.getEventTime();
        } else if ( event.getAction() == MotionEvent.ACTION_UP ) {
            if ( event.getEventTime() - mStartTime <= ViewConfiguration.getTapTimeout() ) {
                return false;
            }
        }

        return super.onInterceptTouchEvent(event);
    }
}

@TimonNetherlands 代码的小修改也适用于 pixel4

class ClickableMotionLayout: MotionLayout {
    private var mStartTime: Long = 0
    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {

        if ( event?.action == MotionEvent.ACTION_DOWN ) {
            mStartTime = event.eventTime;
        }

        if ((event?.eventTime?.minus(mStartTime)!! >= ViewConfiguration.getTapTimeout()) && event.action == MotionEvent.ACTION_MOVE) {
            return super.onInterceptTouchEvent(event)
        }

        return false;
    }
}

@Finn Marquardt 的解决方案是有效的,但仅对 ViewConfiguration.getTapTimeout() 进行检查在我看来并不是 100% 可靠的,对我来说,有时点击事件不会触发,因为点击的持续时间是大于 getTapTimeout()(仅 100 毫秒)。也没有处理长按。

这是我的解决方案,使用 GestureDetector:

class ClickableMotionLayout : MotionLayout {
 
private var isLongPressing = false
private var compatGestureDetector : GestureDetectorCompat? = null
var gestureListener : GestureDetector.SimpleOnGestureListener? = null

init {
      setupGestureListener()
      setOnTouchListener { v, event ->
          if(isLongPressing && event.action == MotionEvent.ACTION_UP){
              isPressed = false
              isLongPressing = false
              v.performClick()
          } else {
              isPressed = false
              isLongPressing = false
              compatGestureDetector?.onTouchEvent(event) ?: false
          }
      }
  }

其中setupGestureListener()是这样实现的:

 fun setupGestureListener(){
        gestureListener = object : GestureDetector.SimpleOnGestureListener(){
            override fun onLongPress(e: MotionEvent?) {
                isPressed = progress == 0f
                isLongPressing = progress == 0f
            }

            override fun onSingleTapUp(e: MotionEvent?): Boolean {
                isPressed = true
                performClick()
                return true
            }
        }
        compatGestureDetector = GestureDetectorCompat(context, gestureListener)
    }

GestureDetector仅在点击或长按时处理触摸事件(并且会手动触发“按下”状态)。一旦用户抬起手指并且触摸事件实际上是长按,则触发点击事件。在任何其他情况下,MotionLayout 将处理该事件。

恐怕 none 的其他答案对我有用,我不知道这是因为库中的更新,但我没有收到 ACTION_MOVE 事件onSwipe.

中设置的区域

相反,这最终对我有用:

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.children

/**
 * This MotionLayout allows handling of clicks that clash with onSwipe areas.
 */
class ClickableMotionLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        // Take all child views that are clickable,
        // then see if any of those have just been clicked, and intercept the touch.
        // Otherwise, let the MotionLayout handle the touch event.
        if (children.filter { it.isClickable }.any {
                it.x < event.x && it.x + it.width > event.x &&
                        it.y < event.y && it.y + it.height > event.y
            }) {
            return false
        }
        return super.onInterceptTouchEvent(event)
    }
}

基本上,当我们收到触摸事件时,我们会遍历 MotionLayout 的所有子项并查看它们中的任何一个(可单击的)是否是事件的目标。如果是这样,我们拦截触摸事件,否则我们让 MotionLayout 做它的事情。

这允许用户点击与 onSwipe 区域冲突的可点击视图,同时即使在可点击视图上开始滑动也允许滑动。