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_container
和 v_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 区域冲突的可点击视图,同时即使在可点击视图上开始滑动也允许滑动。
我在场景中使用 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_container
和 v_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 区域冲突的可点击视图,同时即使在可点击视图上开始滑动也允许滑动。