美文网首页MobDevGroup程序猿学习view
Android共享元素转场动画兼容实践

Android共享元素转场动画兼容实践

作者: sheepm | 来源:发表于2016-10-03 15:26 被阅读11342次

    原文地址

    Android Shared-Element Transitions for all

    我们都希望我们的app有自己特殊的地方,转场动画就是一个比较好的方式让用户记住我们的应用。在Lollipop+ 上的版本实现起来十分的简单,但是如果想兼容低于5.0的版本,你或许需要检查Android系统的版本来做一些功能上的削减,或者你可以勇敢的手动来实现这个转换,疯狂的想法,但是我们可以来这么尝试一下。

    共享元素变换步骤

    当你想要从一个Activity A转换到Activity B,而且他们共享一个元素(比如是一个view),在这种场景下,最好的用户体验可能就是将共享的元素直接变换到最终的地方和大小,这会使用户专注于应用而且有一种连贯性的表达。那么怎么实现这样的过渡呢?可能需要一些步骤

    • Activity A解析共享元素的开始值然后通过intent传递给Activity B
    • Activity B开始的时候是完全透明的
    • Activity B从Bundle里取出值并准备场景
    • Activity B开始做共享元素变换的动画

    我将会在这篇文章中展示如何实现这些步骤的细节和一些示例代码。首先,先进行命名,我们将在 Activity A中的共享view称之为origin view(初始视图),并将Activity B中的共享view称之为destination view(目标视图)。虽然这两个view被称之为共享view,但实际上他们只是碰巧有相同内容的完全不相关的view。

    Activity A解析开始值并传递给Activity B

    当我们想要创建一个从Activity A到Activity B转换的视觉过渡效果,第一步就是解析出两个Activity中的共享视图内开始和结束值这样,后期需要这个数据来做变换。
    在Activity B中我们只能解析后目标视图的属性,对于初始视图的属性需要通过intent传递过来

            Intent intent = new Intent(context, ArticleImageActivity.class);
            intent.putExtra(IMAGE_URL_EXTRA, imageUrl);
            intent.putExtra(VIEW_INFO_EXTRA, /* start values */ captureValues(originView));
    
            startActivity(intent);
            overridePendingTransition(0, 0);
    

    为了去掉默认的转场效果,我们需要在 startActivity 后面调用一次 overridePendingTransition(0,0) ,然后就能实现我们自己的效果。
    这个 captureValues(View) 的方法会将view的大小和位置打包成一个bundle返回出来

        private Bundle captureValues(@NonNull View view) {
            Bundle b = new Bundle();
            int[] screenLocation = new int[2];
            view.getLocationOnScreen(screenLocation);
            b.putInt(PROPNAME_SCREENLOCATION_LEFT, screenLocation[0]);
            b.putInt(PROPNAME_SCREENLOCATION_TOP, screenLocation[1]);
            b.putInt(PROPNAME_WIDTH, view.getWidth());
            b.putInt(PROPNAME_HEIGHT, view.getHeight());
            return b;
        }
    

    Activity B设置背景透明

    在startActivity之后就跳转到了Activity B中,但是我们不希望用户一开始就能看到整个布局直到我们的过渡动画准备好开始运动。解决方法十分的简单,就是保证这个activity一开始是透明的,而且将layout都设置为 INVISIBLE

        <style name="Transparent" parent="AppTheme">
            <item name="android:windowIsTranslucent">true</item>
            <item name="android:windowBackground">@android:color/transparent</item>
        </style>
    

    Activity B场景布置

    这一步是最为重要的,首先,确保所有的资源文件准备完毕,假设这个共享view是一个 ImageView 而且图片已经从URL中加载出来了。这个并不难可以使用比如 Picasso, Glide, 或者其他的library。

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_article_image);
            ...
            // start the information passed in the Bundle
            extractViewInfoFromBundle(getIntent());
            // only now we load the image
            Picasso.load(mImageUrl)
                    .into(mDestinationView, new Callback() {
                        @Override
                        public void onSuccess() {
                            // we've got the image loaded, we can start prepping the scene
                            onUiReady();
                        }
                        @Override
                        public void onError() {...}
                    });
            ...
        }
    

    这个 onUiReady() 方法会在所有的资源获取之后准备界面以及过渡动画的实现,现在已经通过intent传递过来了初始视图的属性值,然后我们需要拿到目标视图的属性值,所以需要一个在view被layout之后还没有draw之前的时机,官方给我们提供了一个很好的回调方式 onPreDraw()

        private void onUiReady() {
            mDestinationView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    // remove previous listener
                    mDestinationView.getViewTreeObserver().removeOnPreDrawListener(this);
                    // prep the scene
                    prepareScene();
                    // run the animation
                    runEnterAnimation();
                    return true;
                }
            });
        }
    

    prepareScene() 中我们需要拿到目标视图的属性,并且根据在屏幕上的位置和大小做一个差值的属性转换

        private void prepareScene() {
            // capture the end values in the destionation view
            mEndValues = captureValues(mDestinationView);
    
            // calculate the scale and positoin deltas
            float scaleX = scaleDelta(mStartValues, mEndValues);
            float scaleY = scaleDelta(mStartValues, mEndValues);
            int deltaX = translationDelta(mStartValues, mEndValues);
            int deltaY = translationDelta(mStartValues, mEndValues);
    
            // scale and reposition the image
            mDestinationView.setScaleX(scaleX);
            mDestinationView.setScaleY(scaleY);
            mDestinationView.setTranslationX(deltaX);
            mDestinationView.setTranslationY(deltaY);
        }
    

    准备好之后就是设置过渡动画了

    Activity B过渡动画

    目标视图现在和初始视图在同样的地方大小也一样,然后我们做动画配合千米那设置的属性变换来让它移动到最后的位置,并且缩放到适当的大小

        private void runEnterAnimation() {
            // We can now make it visible
            mDestinationView.setVisibility(View.VISIBLE);
            // finally, run the animation
            mDestinationView.animate()
                    .setDuration(DEFAULT_DURATION)
                    .setInterpolator(DEFAULT_INTERPOLATOR)
                    .scaleX(1f)
                    .scaleY(1f)
                    .translationX(0)
                    .translationY(0)
                    .start();
        }
    

    在我们需要从Activity B返回到Activity A,只需要再做一个反转的动画,你可以在 onBackPressed() 的回调或者是其他会返回到前面的Activity的地方调用下面这段代码。

        private void runExitAnimation() {
            mDestinationView.animate()
                    .setDuration(DEFAULT_DURATION)
                    .setInterpolator(DEFAULT_INTERPOLATOR)
                    .scaleX(scaleX)
                    .scaleY(scaleY)
                    .translationX(deltaX)
                    .translationY(deltaY)
                    .withEndAction(new Runnable() {
                        @Override
                        public void run() {
                            finish();
                            overridePendingTransition(0, 0);
                        }
                    }).start();
        }
    

    到这里整个步骤就结束了,而且这种方式能兼容所有的Android版本,不限于5.0以上,下面的采取这种方式实践的效果。

    效果演示

    备注

    推荐一个图片手势操作的library,其中包含了这个共享元素的使用,可以看一下具体的实现方式

    https://github.com/alexvasilkov/GestureViews

    相关文章

      网友评论

      • f447dd509039:您好,sheepm,仿照github上给demo做了一遍,效果大致可以兼容4.4的了。但是有时候起始位置和回归的位置不对。是什么原因导致的?
        f447dd509039:可以了。谢谢 。但是点击图片没有反应
      • JacksonWen:我靠 楼主挖坑不填,有没有完整的代码
      • Runtime123:很好
      • smallstrong:推荐的库 看了下 是真牛啊
      • 71e834d2a99b:怎么控制A界面的控件呢?
        958f755e74a4:请问找到scaleDelta()和translationDelta()这两个方法的源码了吗?原文链接我打不开了
      • byd666:你好,请问有demo吗?能分享一下吗?
      • 逮虾户max:转场到第二个Activity或者Fragment中的ViewPager,应该怎么做,探探做的就很好
      • Clement_wu:已经贴了大部分代码,为什么不全贴出来,scaleDelta()和translationDelta()这个核心变换没有
        71e834d2a99b:@w启 源码 原文连接有
        w启:@sheepm 可以的话我还是想求一下这部分的代码。。。:flushed:
        sheepm:@Clement_wu 这个只是属性动画的基础,这里直接手写一下,ObjectAnimator.ofFloat(view,"scaleX",startScaleX,endScaleY).setDuration(time).start(),大概就是这样,关键是如何确定起始和结束,这个上面已经有了
      • 花香_Android:感觉很多人说的白屏问题跟这个activityB准备工作有关
        PS 我说的是人们用的系统APi实现的
      • 五谷观精分道长:我看过一个开原库,外国人写的。也是用来向下兼容元素共享
        sheepm:@FuckskyZhao 有名字么,我学习一下
      • niceWind:这种效果官方叫做场景转换,直接使用官方提供的API 就好了. 效果比这种好看的多,官方的基本上所有的元素都会有动画,颜色也会根据图片的真题效果来汲取后设置.
        非著名程序员:@niceWind 你是没认真看文章吧?人家这篇文章讲的怎么适配5.0以下的系统。
        sheepm:@niceWind 官方的只支持5.0以上的系统 这种是针对5.0以下的兼容
      • ImmortalZ:不错👍
      • breakingbad:棒棒的

      本文标题:Android共享元素转场动画兼容实践

      本文链接:https://www.haomeiwen.com/subject/ffwdyttx.html