android - 在 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
了一点,因为我注意到不同的行为,这段代码是效果最好的代码,但是在玩了单元格/滚动/动画之后仍然会崩溃。
解决方案
我相信是 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;
}
}
}
}
推荐阅读
- typescript - 如何创建地图来存储不同数据类型的主题/行为主题
- c# - 在 Unity 中使用 Photon Networking 移动客户端/玩家的问题
- swift - 在 Swift 中,是否可以在默认参数值中使用参数值?
- c# - C#如何用同一个声音播放器播放多个声音/歌曲
- java - Type type-hierarchy 类型应该如何实现?
- unix - 如何用 SED 替换 html 标签?
- microsoft-graph-api - MS Graph API 用户 ID 是否区分大小写?
- symfony - Sylius ShopApiPlugin 无法将产品添加到购物车
- c# - 如何处理刷新令牌过期
- python - 在列表的开头和结尾强制执行项目