配置 Activity 的转场动画

使用揭露动画,主要是使用到了 ViewAnimationUtils.createCircularReveal() 这个 API ,官方已经给我们封装好了,我们具体看下各个参数是什么意思,就知道如何使用了

1
2
3
4
 public static Animator createCircularReveal(View view,
            int centerX,  int centerY, float startRadius, float endRadius) {
        return new RevealAnimator(view, centerX, centerY, startRadius, endRadius);
 }

这几个参数也很好理解,基本看见名字就知道是什么意思了:

  • 第一个参数是执行揭露动画的 View 视图
  • 第二个参数是相对于视图 View 的坐标系,动画圆的中心的x坐标
  • 第三个参数是相对于视图 View 的坐标系,动画圆的中心的y坐标
  • 第四个参数是动画圆的起始半径
  • 第五个参数动画圆的结束半径

方便理解从网络找了一张图(来源:https://github.com/OCNYang/Android-Animation-Set/tree/master/reveal-animation)

通过坐标系分析各个参数

这里有一个很详细的库,大家可以看一下,也有很多其他的动画的使用 https://github.com/lgvalle/Material-Animations

转场使用

如下是在项目中的使用的揭露动画,我是在 baseActivity 中做的处理,有那个页面需要转场的话,就继承这个 Actiivty ,我这里使用的是整个 activity 的 DecorView

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//将 Activity 的揭露效果写在 Base 类中,需要揭露动画效果时继承
abstract class BaseAnimRevealActivity : BaseActivityWrapper() {
    companion object {
        //手动往 intent 里传入上个界面的点击位置坐标
        val CLICK_X = "CLICK_X"
        val CLICK_Y = "CLICK_Y"


        var time = 300L
    }

    private var onGlobalLayout: ViewTreeObserver.OnGlobalLayoutListener? = null

    //揭露(进入)动画
    var mAnimReveal: Animator? = null

    //反揭露(退出)动画
    var mAnimRevealR: Animator? = null


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        circularReveal(intent)
    }

    //Activity 揭露(进入)动画,进入时使用
    private fun circularReveal(intent: Intent?) {
        //系统提供的揭露动画需 5.0 及以上的 sdk 版本,当我们获取不到上个界面的点击区域时就不展示揭露动画,因为此时没有合适的锚点
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ||
            (intent?.sourceBounds == null && intent?.hasExtra(CLICK_X)?.not() ?: true)
        ) return


        val rect = intent?.sourceBounds
        val v = window.decorView
        v.visibility = View.INVISIBLE

        @SuppressWarnings
        onGlobalLayout = object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {//此时既是开始揭露动画的最佳时机
                println("centerX ${rect?.centerX() ?: intent?.getIntExtra(CLICK_X, 0)}")
                println("centerY ${rect?.centerY() ?: intent?.getIntExtra(CLICK_Y, 0)}")

//                mAnimReveal?.removeAllListeners()
//                mAnimReveal?.cancel()
                if (mAnimReveal == null) {
                    mAnimReveal = ViewAnimationUtils.createCircularReveal(
                        v,
                        rect?.centerX() ?: intent?.getIntExtra(CLICK_X, 0) ?: 0,
                        rect?.centerY() ?: intent?.getIntExtra(CLICK_Y, 0) ?: 0,
                        0f,
                        v.height.toFloat()
                    )

                    mAnimReveal?.duration = time
                    mAnimReveal?.interpolator = LinearInterpolator()
                    mAnimReveal?.addListener(object : Animator.AnimatorListener {
                        override fun onAnimationRepeat(animation: Animator?) {
                        }

                        override fun onAnimationEnd(animation: Animator?) {
                            onGlobalLayout?.let {
                                //我们需要在揭露动画进行完后及时移除回调
                                v?.viewTreeObserver?.removeOnGlobalLayoutListener(it)
                            }
                        }

                        override fun onAnimationCancel(animation: Animator?) {
                        }

                        override fun onAnimationStart(animation: Animator?) {
                        }
                    })
                }

                mAnimReveal?.isStarted?.let {
                    if (!it) {
                        mAnimReveal?.start()
                    }
                }

            }
        }
        //视图可见性发生变化时的回调,回调里正是开始揭露动画的最佳时机
        v.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayout)
    }

    //Activtiy 反揭露(退出)动画,即退出时的过渡动画,
    // 这么起名可能不恰当,其实还是同样的动画,
    // 只不过揭露的起始和终结半径跟上面相比反过来了
    private fun circularRevealReverse(intent: Intent?) {
        //系统提供的揭露动画需 5.0 及以上的 sdk 版本,当我们获取不到上个界面的点击区域时就不展示揭露动画,因为此时没有合适的锚点
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ||
            (intent?.sourceBounds == null && intent?.hasExtra(CLICK_X)?.not() ?: true)
        ) {
            super.onBackPressed()
            return
        }
        val rect = intent?.sourceBounds
        val v = window.decorView
        mAnimRevealR?.removeAllListeners()
        mAnimRevealR?.cancel()
        mAnimRevealR = ViewAnimationUtils.createCircularReveal(
            v,
            rect?.centerX() ?: intent?.getIntExtra(CLICK_X, 0) ?: 0,
            rect?.centerY() ?: intent?.getIntExtra(CLICK_Y, 0) ?: 0,
            v.height.toFloat(),
            0f

        )
        mAnimRevealR?.duration = time
        mAnimRevealR?.interpolator = LinearInterpolator()
        mAnimRevealR?.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                println("onAnimationEnd")
                v.visibility = View.GONE
                this@BaseAnimRevealActivity.finish()
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
                println("onAnimationStart")
            }
        })
        mAnimRevealR?.start()
    }

    //回退时应用反揭露动画
    override fun onBackPressed() {
        circularRevealReverse(intent)
    }

    override fun onDestroy() {
        //及时释放资源以保证代码健壮性
        mAnimReveal?.removeAllListeners()
        mAnimReveal?.cancel()
        mAnimRevealR?.removeAllListeners()
        mAnimRevealR?.cancel()
        //及时释放资源以保证代码健壮性
        onGlobalLayout?.let {
            window.decorView.viewTreeObserver?.removeOnGlobalLayoutListener(it)
        }
        super.onDestroy()
    }

    //这个方法很重要,如果我们应用的启动图标在桌面上的位置有变化,可在此收到新的位置信息,然而经作者本人实践作用十分有限
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        println("---------onNewIntent")
        circularReveal(intent)
        //更新intent
        this.intent = intent
    }
}

跳转使用

1
2
3
4
5
6
7
val location = IntArray(2)
view.getLocationInWindow(location)
val intent1 =  Intent(this@VoiceChatActivity, MusicPlayActivity::class.java).apply {
                   putExtra("currentPlayPosition", currentPlayPosition)
                   putExtra(BaseAnimRevealActivity.CLICK_X, view.width / 2)
                   putExtra(BaseAnimRevealActivity.CLICK_Y, location[1] + it.height / 2)
               }

这里跳转的时候,我将中心点 X 的位置设置为屏幕中间的位置,Y 设置为手指按下的位置。

具体使用的代码都在这里了,由于隐私问题,就不贴效果图了。效果基本和上文贴的几个连接差不多。