首页 > 解决方案 > 在 RecyclerView 的项目上使用 MotionLayout OnSwipe

问题描述

我尝试在我的回收站视图的项目上实现动画,自定义滑动,为此,我尝试使用 MotionLayout。实际上,如果动画的触发是单击,它可以正常工作,但是当我想要滑动时,它会干扰回收器视图的滑动。这是我的场景:

<?xml version="1.0" encoding="utf-8"?>
<MotionScene
    xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
    motion:constraintSetStart="@layout/chat_item_start"
    motion:constraintSetEnd="@layout/chat_item_end"
    motion:duration="500">

    <OnClick
        motion:targetId="@+id/background"
        motion:clickAction="toggle" />

    <!-- <OnSwipe
        motion:touchAnchorId="@+id/background"
        motion:touchAnchorSide="right"
        motion:dragDirection="dragLeft" /> -->

    </Transition>

</MotionScene>

如何在开始时查看我的列表:

在此处输入图像描述

最后,当我点击一个项目时:

在此处输入图像描述

当我切换到 OnSwipe 触发器时,动画有效,但只有当我严格水平滑动时,如果我的手指错误地向上或向下移动,动画就会停止并且甚至不会自行完成:

在此处输入图像描述

我认为问题出现是因为回收器视图也处理滑动手势,因为当动画停止时(如果我稍微向上/向下),回收器会像往常一样移动。

如果我用滑动玩太多,我会收到这个错误:

java.lang.NullPointerException:尝试在 androidx.constraintlayout.motion.widget.MotionLayout$MyTracker.addMovement(MotionLayout. java:985) 在 androidx.constraintlayout.motion.widget.MotionScene.processTouchEvent(MotionScene.java:1043) 在 androidx.constraintlayout.motion.widget.MotionLayout.onTouchEvent(MotionLayout.java:2992) 在 android.view.View.dispatchTouchEvent (View.java:12515) 在 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3024) 在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2705) 在 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java: 3030) 在 android.view.ViewGroup。dispatchTouchEvent(ViewGroup.java:2662) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java :3030) 在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 在 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 在 android .view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 在 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) 在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 在 android.view.ViewGroup。 dispatchTouchEvent(ViewGroup.java:2662) 在 com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:444) 在 android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662) 的 android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030) ) 在 com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1829) 在 android.app.Activity.dispatchTouchEvent(Activity.java:3397) 在 androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java: 69) 在 com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:402) 在 android.view.View.dispatchPointerEvent(View.java:12754) 在 android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java :5052) 在 android.view。ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4855) 在 android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4372) 在 android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4425) 在 android.view .ViewRootImpl$InputStage.forward(ViewRootImpl.java:4391) 在 android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4531) 在 android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4399) 在 android。 view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4588) 在 android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4372) 在 android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4425) 在 android .view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4391) 在 android.view。ViewRootImpl$InputStage.apply(ViewRootImpl.java:4399) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4372) at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7034) at android.view.ViewRootImpl .doProcessInputEvents(ViewRootImpl.java:7007) 在 android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6968) 在 android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7137) 在 android.view.InputEventReceiver.dispatchInputEvent( InputEventReceiver.java:186) 在 android.os.MessageQueue.next(MessageQueue.java:326) 在 android.os.MessageQueue.nativePollOnce(Native Method) 在 android.os.Looper.loop(Looper.java:142) 在 android .app.ActivityThread.main(ActivityThread.java:6649) 在 java.lang。com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) 上的 reflect.Method.invoke(Native Method)

我不知道如何正确调度 MotionLayout 和 RecyclerView 之间的触摸事件,如果在这个阶段甚至可以使用动作布局。

如果有人有想法,请提前致谢!

更新 :

这是处理触摸事件的代码(在recyclerView上禁用/启用)

recyclerView 项上的 onTouchListener

holder.itemView.setOnTouchListener { view, motionEvent ->

        val progress = holder.motion.progress

        when(motionEvent.action){
            MotionEvent.ACTION_DOWN -> {
                x = motionEvent.x
                y = motionEvent.y
                Log.d("Event Down", "X : $x, Y : $y")
            }

            MotionEvent.ACTION_UP->{
                finalx = motionEvent.x
                finaly = motionEvent.y
                Log.d("Event Up", "X : $finalx, Y : $finaly")

                if(progress > 0.5 && progress != 1.0F ){
                    holder.motion?.transitionToEnd()
                }else{
                    holder.motion?.transitionToStart()
                }
                holder.itemView.parent.requestDisallowInterceptTouchEvent(false)
            }
            MotionEvent.ACTION_MOVE ->{
                val currentX = motionEvent.x
                val currentY = motionEvent.y

                val deltaX = abs(x - currentX)
                val deltaY = abs(y - currentY)


                if (deltaX > 1 && deltaY > 1 && deltaX > deltaY ){
                    holder.itemView.parent.requestDisallowInterceptTouchEvent(true)
                }
                Log.d("Event Move", "X : $currentX, Y : $currentY")
            }
        }
        false
    }

过渡监听器

holder.motion.setTransitionListener(object: MotionLayout.TransitionListener{

        override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {}
        override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
            Log.d("Animation started", "Start id : $p1")
            holder.itemView.parent.requestDisallowInterceptTouchEvent(true)
        }

        override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
            Log.d("Animation change ", "progress : $p3")
        }

        override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
            val progress = holder.motion.progress
            opened = progress == 1.0f
            Log.d("Animation Completed", "progress : $progress")
            holder.itemView.parent.requestDisallowInterceptTouchEvent(false)
        }
    })

我在任何地方都使用requestDisallowInterceptTouchEvent了一点,因为我注意到不同的行为,这段代码是效果最好的代码,但是在玩了单元格/滚动/动画之后仍然会崩溃。

标签: androidandroid-recyclerviewandroid-animationandroid-motionlayout

解决方案


我相信是 recyclerview 的滚动中断了你的动画。我的建议是在滑动项目时锁定 recyclerviews 滚动。

至于崩溃,我也在试图自己解决这个问题。崩溃似乎是由于调用了 addMovement(android.view.MotionEvent) 而没有检查对 VelocityTracker 对象的引用是否为空。这是运动布局本身的问题。

编辑:添加代码

class CustomLayoutManager(val context: Context?) : LinearLayoutManager(context) {
private var isScrollEnabled = true
fun setScrollEnabled(flag: Boolean) {
    isScrollEnabled = flag
}

override fun canScrollVertically(): Boolean {
    return isScrollEnabled && super.canScrollVertically()
}

上面是布局管理器。它非常简单,只需覆盖垂直滚动以使用稍后设置的我的标志来设置它。

MotionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
        override fun onTransitionTrigger(motionLayout: MotionLayout?, idStart: Int, idEnd: Boolean, progress: Float) {
        }

        override fun onTransitionStarted(motionLayout: MotionLayout?, idStart: Int, idEnd: Int) {
        }

        override fun onTransitionChange(motionLayout: MotionLayout?, idStart: Int, idEnd: Int, progress: Float) {
            //Timber.d("ML: CHANGED ${p0?.currentState} endId:$p2 startid:$p1")
            if (motionLayout?.currentState != idStart && motionLayout?.currentState != idEnd) motionListener?.onTriggered()

        }

        override fun onTransitionCompleted(motionLayout: MotionLayout?, idCurrent: Int) {
            motionListener?.onComplete()

        }
    })

上面是过渡监听器。它在我的自定义视图中,因此如果您的只是在回收站视图中,则不需要回调。我所做的只是检查 OnChanged 它不在开始或结束 id 处。这就是您知道该项目在运动中的方式。然后在我的适配器中我有这个

interface ScrollLockListener {
    fun onLockUnlock(shouldLock: Boolean)
}
fun setScrollLockListener(listener: (Boolean) -> Unit) {
    scrollLockListener = object : ScrollLockListener {
        override fun onLockUnlock(shouldLock: Boolean) = listener(shouldLock)
    }
}

holder.item.setMotionListener(
                    { scrollLockListener?.onLockUnlock(false) },
                    { scrollLockListener?.onLockUnlock(true) })

这会为您的适配器创建一个回调,允许您从活动或片段设置滚动锁定。然后在您的活动/片段中,您只需执行

cardLayoutRecycler.layoutManager = CustomLayoutManager(context).also {
            recyclerAdapter.setScrollLockListener { isLocked -> it.setScrollEnabled(isLocked) }
        }

编辑:为了修复崩溃我最终这样做了

public class CustomMotionLayout extends MotionLayout {

public CustomMotionLayout(Context context) {
    super(context);
}

public CustomMotionLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public CustomMotionLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
protected MotionTracker obtainVelocityTracker() {
    return CustomMotionLayout.MyTracker.obtain();
}

/**
 * The only functional changes to this class are added null-checks and recursion avoidance in
 * @see MyTracker#getYVelocity(int)
 *
 * A possible desync issue in the official MotionLayout class can trigger a NullPointerException
 * here when the VelocityTracker object is accessed immediately after being recycled.
 */
private static class MyTracker implements MotionLayout.MotionTracker {
    VelocityTracker tracker;
    private static CustomMotionLayout.MyTracker me = new CustomMotionLayout.MyTracker();

    private MyTracker() {
    }

    public static CustomMotionLayout.MyTracker obtain() {
        me.tracker = VelocityTracker.obtain();
        return me;
    }

    public void recycle() {
        if (this.tracker != null) {
            this.tracker.recycle();
            this.tracker = null;
        }
    }

    public void clear() {
        if (this.tracker != null) {
            this.tracker.clear();
        }
    }

    public void addMovement(MotionEvent event) {
        if (this.tracker != null) {
            this.tracker.addMovement(event);
        }
    }

    public void computeCurrentVelocity(int units) {
        if (this.tracker != null) {
            this.tracker.computeCurrentVelocity(units);
        }
    }

    public void computeCurrentVelocity(int units, float maxVelocity) {
        if (this.tracker != null) {
            this.tracker.computeCurrentVelocity(units, maxVelocity);
        }
    }

    public float getXVelocity() {
        if (this.tracker != null) {
            return this.tracker.getXVelocity();
        } else {
            return 0f;
        }
    }

    public float getYVelocity() {
        if (this.tracker != null) {
            return this.tracker.getYVelocity();
        } else {
            return 0f;
        }
    }

    public float getXVelocity(int id) {
        if (this.tracker != null) {
            return this.tracker.getXVelocity(id);
        } else {
            return 0f;
        }
    }

    public float getYVelocity(int id) {
        if (this.tracker != null) {
            return this.tracker.getYVelocity(id);
        } else {
            return 0f;
        }
    }
}

}


推荐阅读