Activity自定义过渡动画-仿某小说APP阅读器启动动画

861 阅读5分钟

A货码厂带你进坑:仿某阅小说APP书籍打开效果

原厂效果图

1645257119065.gif

博主的翻车图集

效果图1.gif

翻车效果1-打开正常,返回会突然归位,停止后又执行向右翻转

翻车效果2.gif

翻车效果2-打开ImageView慢慢放大翻开,图片切已经放大了;返回时视图也是正常的,图片也是先缩小了。

翻车效果3

翻车效果3-打开Activity,未等动画完成,马上关闭,出现了图片飘走的效果。

看到上面的翻车效果是不是感同身受!

原厂效果拆解:

  1. 由ShareElementTransition即Activity共享元素过渡动画(即两个Activity或Fragment之间切换时的共享元素)来完成整体效果;
  2. 封面视图沿着Y轴在X=0的位置执行旋转动画,小说内容区域使用缩放动画;(坑,在这里!!!)
  3. 关闭阅读器时,执行反向旋转动画,并改变小说在书架的位置。

实现过程(被坑的过程):

0. 图片旋转

​ 对博主来说,图片旋转的是比较陌生的,就从图片旋转效果开始实现。

​ 一顿操作猛如虎,在百度找到一个Animation,实现了图片旋转的动画。内心小激动一番,距离实现效果进了一大步,So easy!!!

​ 结果,在开始写自定义过渡动画时发现,实现过渡动画需要使用Animator不是Animation。晴天霹雳啊,重新开始吧!

1. Activity共享元素过渡动画

Android 默认提供共享元素过渡:

  • changeBounds - 为目标视图布局边界的变化添加动画效果。
  • changeClipBounds - 为目标视图裁剪边界的变化添加动画效果。
  • changeTransform - 为目标视图缩放和旋转方面的变化添加动画效果。
  • changeImageTransform - 为目标图片尺寸和缩放方面的变化添加动画效果。

​ 上面已经分析过原厂效果,需要分别实现两个视图的动画,封面视图旋转缩放以及小说内容视图缩放。先从简单的开始,实现“小说内容视图缩放”。缩放效果官方就有案例,这里就直接参考(Ctrl+c)Android官方的代码

    /**
     * 启动Activity
     */
    class StartActivity {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            ivBook.setOnClickListener { startActivity(it) }
        }

        private fun openActivity(view: View) {
            view.transitionName = "open_book"
            val i = Intent(this, EndActivity::class.java)
            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                this,
                Pair(view, "open_reader")
            )
            startActivity(i, options.toBundle())
        }
    }

    /**
     * 结束Activity
     */
    class EndActivity {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            tvReader.transitionName = "open_reader"
            window.sharedElementEnterTransition = createTransition()
            window.sharedElementReturnTransition = createTransition()
        }

        private fun createTransition(): Transition {
            val changeBounds = ChangeBounds()
            changeBounds.targetIds.add(R.id.tv_reader)
            changeBounds.duration = 1000L
            return changeBounds
        }
    }

是不是很简单,这就实现了一个普通的过渡动画。

放大效果
2. 自定义共享元素过渡动画

​ 想要实现打开书本的动画效果,那就必须自定义动画。

​ 要实现自定义过渡动画,需要继承Transition,然后实现一个Animator。

    public class TurnPageTransition extends Transition {

        @Override
        public void captureStartValues(TransitionValues values) {
              // 捕获起始值。
           // 过渡动画添加了几个目标视图(targetIds.add(R.id.xx))就会执行多少次这个方法。下面的captureEndValues()和createAnimator()也是如此。
        }

        @Override
        public void captureEndValues(TransitionValues values) {
           // 捕获结束值
        }

        @Override
        public Animator createAnimator(ViewGroup sceneRoot,
                                       TransitionValues startValues,
                                       TransitionValues endValues) {
           // 创建Animator
        }
    } 

图片旋转动画Animator实现

​ Android提供了ObjectAnimator,通过View的rotationY(沿Y轴旋转)属性,然后改变旋转轴心的位置可以轻而易举实现我们想要的翻页(翻车)动画。

效果图1

​ 博主是开玩笑的吧,这个不是我们想要的啊。

​ 我们预期的效果应该是下面这样的:

效果1-无bug

​ 那打开效果是正常的,为什么返回效果却是出现了跳动BUG呢。主要是由于我们旋转视图时,视图的x,y的位置是没有变化的,所以需要在捕获结束值的方法里做视图位置的偏移,向左偏移一个视图的宽度。

public class TurnPageTransition extends Transition {
    private static final String PROPNAME_ROTATION = "com.example.learnactivitytransition:rotation:rotationY";
    private static final String PROPNAME_RECT = "com.example.learnactivitytransition:rotation:rect";

   // 是否是关闭动画 true=关闭动画,false=打开动画
    private final boolean isClose; 

    public TurnPageTransition(boolean isClose) {
        this.isClose = isClose;
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
         // 记录旋转的开始值角度。打开的是-180,关闭的开始值是0.
        transitionValues.values.put(PROPNAME_ROTATION, isClose ? -180 : 0);
      
       // 记录视图的四个点的值
        Rect startRect = new Rect();
        View view = transitionValues.view;
        startRect.left = view.getLeft();
        startRect.top = view.getTop();
        startRect.right = view.getRight();
        startRect.bottom = view.getBottom();
        transitionValues.values.put(PROPNAME_RECT, startRect);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
       // 记录旋转的结束值角度。打开的是0,关闭的是-180。
        transitionValues.values.put(PROPNAME_ROTATION, isClose ? 0 : -180);

        Rect endRect = new Rect();
        View view = transitionValues.view;
        endRect.left = view.getLeft();
        endRect.top = view.getTop();
        endRect.right = view.getRight();
        endRect.bottom = view.getBottom();
        if (isClose) {
           /*
             *填坑操作-修复结束动画位置跳动BUG,向左偏移视图宽度的值
             */
            int width = view.getMeasuredWidth();
            endRect.left -= width;
            endRect.right -= width;
        }
        transitionValues.values.put(PROPNAME_RECT, endRect);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues, 
                                   TransitionValues endValues) {
        if (null == startValues || null == endValues) {
            return null;
        }

        final View startView = startValues.view;
        final View endView = endValues.view;

        List<Integer> targetIds = getTargetIds();
        Animator animator = null;
        if (targetIds.contains(startView.getId())) {
            final Rect startRect = (Rect) startValues.values.get(PROPNAME_RECT);
            final Rect endRect = (Rect) endValues.values.get(PROPNAME_RECT);
            final Integer start = (Integer) startValues.values.get(PROPNAME_ROTATION);
            final Integer end = (Integer) endValues.values.get(PROPNAME_ROTATION);
            animator = create(startView, start, end, startRect, endRect);
        }
        return animator;
    }

    public AnimatorSet create(View view,
                              Integer startRotationY, Integer endRotationY,
                              Rect startRect, Rect endRect
    ) {
        float cameraDistance = view.getContext().getResources().getDisplayMetrics().density * 3000;
        int height = view.getMeasuredHeight();
        view.setCameraDistance(cameraDistance);
        view.setPivotX(0);
        if (isClose) view.setPivotY(0.45f * height);
        else view.setPivotY(0.2f * height);

        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                 ObjectAnimator.ofFloat(view, "rotationY", startRotationY, endRotationY),
                ObjectAnimator.ofInt(view, "left", startRect.left, endRect.left),
                ObjectAnimator.ofInt(view, "top", startRect.top, endRect.top),
                ObjectAnimator.ofInt(view, "right", startRect.right, endRect.right),
                ObjectAnimator.ofInt(view, "bottom", startRect.bottom, endRect.bottom)
        );
        set.setDuration(getDuration());
        return set;
    }
}
3. 完整效果

​ 到目前为止我们已经完成了所需动画的代码,只需整合完善,我们就可以实现一个完整的效果了。

翻车效果2

​ 博主说好完整的效果呢,怎么动画中的图片旋转缩放跟视图就不同步了呢?

​ 正常的效果应该是这样的:

效果2-正常

​ 博主一时半会也没有搞明白为什么,陷入了沉思。

​ 正当迷茫之际又回去看了官方文档,发现changeImageTransform-为目标图片尺寸和缩放方面的变化添加动画效果。这不正是需要的功能吗!上代码:

    /**
     * 结束Activity
     */
    class EndActivity {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ...
            tvReader.transitionName = "open_reader"
            ivCover.transitionName = "open_book"
            window.sharedElementEnterTransition = createTransition()
            window.sharedElementReturnTransition = createTransition()
        }

        private fun createTransition(): Transition {
            val transitionSet = TransitionSet()
              val changeBounds = ChangeBounds()
              changeBounds.targetIds.add(R.id.tv_reader)
                  
           // 填坑操作-修复动画中图片缩放不同步的BUG。
              val changeImageTransform = ChangeImageTransform()
              changeImageTransform.targetIds.add(R.id.iv_cover)

              val turnPageTransition = TurnPageTransition(isClose)
              turnPageTransition.targetIds.add(R.id.iv_cover)

              transitionSet.addTransition(changeBounds)
              transitionSet.addTransition(changeImageTransform)
              transitionSet.addTransition(turnPageTransition)
              transitionSet.duration = TRANSITION_DURATION
              return transitionSet
        }
    }
4. 最后的坑

​ WTF博主还有最后的坑!其实就是最开看到的封面飞走的BUG:

翻车效果3

​ 当我们在打开Activity时,动画还没有结束便点击返回,结束Activity,就会发生视图飘走的bug。

​ 这里简单的处理,就是监听动画的回调,在动画结束前让返回键不生效即可。这里就贴代码了。在最后直接送上源码。

最终效果

看着还是有模有样的吧,给自己赞一个。

最终效果

写代码总会遇到坑,只有在坑里才会不断促使自己不断的卷 卷卷 卷卷卷。最后回头去看,原来也就这样。

感谢你观看这里,一起共勉。

参考文档

《Android 自定义Activity过场动画》

文章源码