Android 5上的共享元素活动过渡

38

我想在从一个活动到另一个活动时设置共享元素转换。

第一个活动包含带有项目的RecyclerView。当单击项目时,该项目应动画到新活动中。

因此,我在最终活动视图以及recycler-view项目视图上都设置了android:transitionName="item"。

在转到下一个活动时,我还使用了以下代码:

this.startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, itemView, "boomrang_item").toBundle());

当单击一个项目时,它会正确转换并显示新视图。这非常好。 然而,当我点击返回按钮时。有时它可以正常工作,但大多数情况下我的活动会因为以下堆栈跟踪而崩溃:

   java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.ViewGroup.transformMatrixToGlobal(android.graphics.Matrix)' on a null object reference
            at android.view.GhostView.calculateMatrix(GhostView.java:95)
            at android.app.ActivityTransitionCoordinator$GhostViewListeners.onPreDraw(ActivityTransitionCoordinator.java:845)
            at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:847)
            at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1956)
            at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1054)
            at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5779)
            at android.view.Choreographer$CallbackRecord.run(Choreographer.java:767)
            at android.view.Choreographer.doCallbacks(Choreographer.java:580)
            at android.view.Choreographer.doFrame(Choreographer.java:550)
            at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:753)
            at android.os.Handler.handleCallback(Handler.java:739)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5221)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

我做错了什么?看起来是Android 5的一个漏洞。


你能添加所有相关的代码,这样就更容易找出你错在哪里了吗? - gaara87
当我打开一个新的活动、旋转屏幕和按下返回按钮时,我也遇到了同样的问题。有什么解决办法吗? - jclova
你解决了吗?我也遇到了同样的异常。 - casolorz
我有同样的问题,但我注意到了一些奇怪的行为。当我从详细信息活动返回时,错误有时仅出现在我的RecyclerView的前三个元素上。我还没有找到解决这个问题的方法。 - Dmytro Karataiev
1
我刚刚在 Android 上报告了这个错误:https://issuetracker.google.com/issues/37943857 - Brais Gabin
显示剩余2条评论
12个回答

8
我遇到了同样的问题,并且注意到如果在返回时原共享元素在上一个屏幕上不再可见(可能在纵向模式下是屏幕上的最后一个元素,但一旦切换到横向模式,它就不再可见),那么转换就没有地方放置共享元素,导致崩溃发生。
我的解决方法是,如果在回退之前屏幕已经旋转,则删除返回转换(在第二个活动中)。但我相信肯定有更好的方法来处理这个问题。
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    mOrientationChanged = !mOrientationChanged;
}

@Override
public void supportFinishAfterTransition() {
    if (mOrientationChanged) {
        /**
         * if orientation changed, finishing activity with shared element
         * transition may cause NPE if the original element is not visible in the returned
         * activity due to new orientation, we just finish without transition here
         */
        finish();
    } else {
        super.supportFinishAfterTransition();
    }
}

4
如果您正在使用ProGuard,则请尝试将此内容添加到规则文件中。我遇到了相同的问题,但似乎这样做有用吗?
-keep public class android.app.ActivityTransitionCoordinator

4
尝试移除最终活动视图中可能存在的任何合并xml标签。 我注意到,转换到一个包含合并标记的视图,在其中过渡元素是合并标记的直接子级,将导致此错误,但如果我将合并标记替换为像CardView这样的不同容器,则动画效果非常好。 还要确保视图中的transitionNames之间存在1:1的关系。
更新: 在进行活动转换时,单击返回按钮返回初始活动,然后再次尝试转换时,我再次遇到了这个问题。 我通过id访问“转换组件”的直接父项(RelativeLayout),使用findViewById()调用,然后调用removeAllViews()。 最终,我将代码更改为在祖先级别上调用'removeAllViews()',并从要在页面加载后取代“转换组件”的元素中删除了一个标签。 这缓解了我的问题。

3

确保第二个Activity中要转换的视图不是根布局。你可以简单地将其包装在一个带有透明windowBackground的FrameLayout中。


这是一个解决方案! - Aksenov Vladimir

3

我遇到了同样的问题,对我来说是由于recyclerview在第一个退出转换之后/期间执行更新所导致的。我认为共享元素视图有时会被回收,这意味着它将不再可用于过渡动画,因此会崩溃(通常在返回转换上,但有时在退出转换上)。如果活动已暂停(使用isRunning标志),则阻止更新 - 请注意,它正在暂停但尚未停止,因为它仍可在后台中看到。另外,如果正在运行转换,则阻止更新过程。我发现监听此回调足够了:

Transition sharedElementExitTransition = getWindow().getSharedElementExitTransition();
        if (sharedElementExitTransition != null) {
            sharedElementExitTransition.addListener(.....);
        }

作为最后一种措施,尽管我不确定这是否有所作为,但我也在 onTransitionStart / onTransitionEnd 中执行了 recyclerView.setLayoutFrozen(true) / recyclerView.setLayoutFrozen(false)

2
我遇到了同样的问题,实际上我使用了Firebase,并且我有一些信息列表,当用户点击时,它将调用带有共享动画的detailActivity,在这个活动中,我使用Firebase更新它作为已读,因此Firebase事件会将列表项更新为已读,但是在这种情况下,这个问题是由于屏幕上的recycler view 布局受到影响而引起的。
它会引发异常,因为我们传递的转换id已经不存在了,所以我使用了这种方法来解决这个问题。 在onPause()中我冻结了布局,在onResume()中将其设置为false;
 @Override
public void onPause() {
    super.onPause();
    mRecycler.setLayoutFrozen(true);
}

@Override
public void onResume() {
    super.onResume();
    mRecycler.setLayoutFrozen(false);
}

它正在运作。


使用 RecyclerView 时遇到了这个问题(在停止状态下触发了更新)。将布局冻结后,问题得以解决。 - Demigod
谢谢,看起来是我的情况,但是setLayoutFrozen()现在已经被弃用了,我使用了suppressLayout()代替。 - Hack06

2
请确保你在传递过渡效果时所传递的“itemView”正是被点击的视图(通过你的onClick()回调函数接收到的视图)。

1
我曾遇到同样的错误,原因与hidro的答案相同,但是由于键盘遮挡了转换返回到的共享元素而导致了这个问题。
我的解决方法是在完成活动之前通过编程方式关闭键盘,以便在上一个活动中不会遮挡共享元素。
View view = this.getCurrentFocus();
if (view != null) {  
    InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
supportFinishAfterTransition();

1
我想到的方法是避免在RecyclerView中返回Activity,或者用其他方式进行回退转换。
禁用所有返回转换:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void finishAfterTransition() {
    finish();
}

或者,如果您只想禁用共享元素返回转换,并能够设置自己的返回转换:

// Track if finishAfterTransition() was called
private boolean mFinishingAfterTransition;

@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mFinishingAfterTransition = false;
}

public boolean isFinishingAfterTransition() {
    return mFinishingAfterTransition;
}

@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void finishAfterTransition() {
    mFinishingAfterTransition = true;
    super.finishAfterTransition();
}

public void clearSharedElementsOnReturn() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        TransitionUtilsLollipop.clearSharedElementsOnReturn(this);
    }
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static final class TransitionUtilsLollipop {

    private TransitionUtilsLollipop() {
        throw new UnsupportedOperationException();
    }

    static void clearSharedElementsOnReturn(@NonNull final BaseActivity activity) {
        activity.setEnterSharedElementCallback(new SharedElementCallback() {

            @Override
            public void onMapSharedElements(final List<String> names,
                    final Map<String, View> sharedElements) {
                super.onMapSharedElements(names, sharedElements);
                if (activity.isFinishingAfterTransition()) {
                    names.clear();
                    sharedElements.clear();
                }
            }
        });
    }

在基础活动中实现后,您可以轻松地在onCreate()方法中使用它。

@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    clearSharedElementsOnReturn(this);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // set your own transition
        getWindow().setReturnTransition(new VerticalGateTransition());
    }
}

1
正如@Fabio Rocha所说,确保从ViewHolder中检索itemView
您可以通过位置获取ViewHolder
mRecyclerView.findViewHolderForAdapterPosition(position);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接