在RecyclerView的项目上使用MotionLayout OnSwipe

时间:2019-11-29 12:23:02

标签: android android-recyclerview android-animation android-motionlayout

我尝试在我的回收站视图的某个项目上实现动画,并进行自定义滑动,为此,我尝试使用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>

我的列表开头如何:

enter image description here

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

enter image description here

当我切换到OnSwipe触发器时,动画只有在我严格水平滑动时才起作用,如果错误地使我的手指稍微向上或向下滑动,动画就会停止并且甚至无法完成播放:

enter image description here

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

如果我在滑动时玩得太多,则会出现此错误:

  

java.lang.NullPointerException:尝试在空对象引用上调用虚拟方法'void android.view.VelocityTracker.addMovement(android.view.MotionEvent)'           在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)           在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.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.dispatchTouchEvent(ViewGroup.java:2662)           在android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)           在android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)           在com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:444)           在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)           在android.view.ViewRootImpl $ InputStage.deliver(ViewRootImpl.java:4372)           在android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7034)           在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.nativePollOnce(本地方法)           在android.os.MessageQueue.next(MessageQueue.java:326)           在android.os.Looper.loop(Looper.java:142)           在android.app.ActivityThread.main(ActivityThread.java:6649)           在java.lang.reflect.Method.invoke(本机方法)           在com.android.internal.os.RuntimeInit $ MethodAndArgsCaller.run(RuntimeInit.java:493)

我不知道如何在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是因为我注意到了不同的行为,并且这段代码是效果最好的代码,但是在处理单元格/滚动/动画后仍然会崩溃。

1 个答案:

答案 0 :(得分:1)

我相信是recyclerview的滚动中断了您的动画。我的建议是在刷物品时锁定recyclerviews滚动。

至于坠机事故,我还试图自己解决这个问题。崩溃似乎是由于调用addMovement(android.view.MotionEvent)而不检查对VelocityTracker对象的引用是否为null引起的。这是一个问题运动布局本身。

编辑:添加了代码

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()

        }
    })
上面的

是过渡侦听器。它在我的自定义视图中,因此如果您只是在recyclerview中,则不需要回叫。我要做的就是检查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;
        }
    }
}

}