該動(dòng)畫(huà)目前僅支持5.0以上,文中使用了幾個(gè)庫(kù)渠抹,主要用于輔助操作撵溃,跟動(dòng)畫(huà)毫無(wú)關(guān)系
平常我們的 Activity 跳轉(zhuǎn)都是默認(rèn)動(dòng)畫(huà)邢笙,或者我們自定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)芋齿,5.0以上對(duì)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)進(jìn)行了更新,效果大大提升成翩,最近又發(fā)現(xiàn)一個(gè) View 的動(dòng)畫(huà) Circular Reveal 觅捆,注意,是作用于 View 的捕传,但是這也不能阻擋我們使用在 Activity 轉(zhuǎn)場(chǎng)上啊惠拭,畢竟人的操作才是最騷氣的扩劝。下面我們來(lái)看一下怎么作用于 Activity 的轉(zhuǎn)場(chǎng)上庸论。
當(dāng)前 Demo 所使用的庫(kù)
-
Glide 加載圖片
implementation 'com.github.bumptech.glide:glide:4.7.1' kapt "com.github.bumptech.glide:compiler:4.7.1"
-
Palette 用于吸取圖片色值
implementation 'com.android.support:palette-v7:28.0.0-alpha3'
-
CircleImageView 圓形頭像
implementation 'de.hdodenhof:circleimageview:2.2.0'
-
EasyBlur 高斯模糊封裝
implementation 'com.github.pinguo-zhouwei:EasyBlur:v1.0.0'
準(zhǔn)備工作
沒(méi)什么要準(zhǔn)備的,先寫(xiě)個(gè)界面吧棒呛,代碼太多影響觀(guān)感小的這就把源碼雙手奉上聂示,點(diǎn)擊跳轉(zhuǎn)到源碼,界面效果如下簇秒,這界面想必實(shí)現(xiàn)起來(lái)應(yīng)該沒(méi)有任何難題吧鱼喉,源碼就不放了自己寫(xiě)就行了。
首先還是需要先簡(jiǎn)單去看一下 CircularReveal 動(dòng)畫(huà)如何作用于 View 上趋观,然后我們?cè)龠M(jìn)行下面的操作扛禽,本篇文章僅介紹如何用作 Activity 的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),不介紹 CircularReveal 的使用皱坛。
實(shí)現(xiàn)效果
廢話(huà)不多說(shuō)编曼,開(kāi)始干活,我就不分步來(lái)做了剩辟,雖然很詳細(xì)但是實(shí)在是太啰嗦了掐场,直接來(lái)實(shí)現(xiàn)最終的效果,先上圖贩猎,GIF 太渣熊户,有轉(zhuǎn)換的高清的,但是圖太大了吭服,不合適
我們就是要實(shí)現(xiàn)這個(gè)效果嚷堡,我們這里是不針對(duì)單個(gè) Activity 跳轉(zhuǎn)的,所以我們要新建一個(gè) BaseActivity 來(lái)統(tǒng)一給所有 Activity 加上該效果艇棕。
主要實(shí)現(xiàn)邏輯:
? 我們的動(dòng)畫(huà)邏輯主要是在目標(biāo) Activity 之中蝌戒,在觸發(fā)的 Activity 之中僅僅告訴目標(biāo) Activity 需不需要啟動(dòng)該動(dòng)畫(huà),然后把動(dòng)畫(huà)的起始點(diǎn) X欠肾,Y 坐標(biāo)瓶颠,還有傳遞給目標(biāo) Activity 揭露動(dòng)畫(huà)的顏色,基本上所有的操作都在目標(biāo) Activity 之中刺桃,目標(biāo) Activity 需要在 setContentView 之中開(kāi)始揭露動(dòng)畫(huà)粹淋,這樣我們的動(dòng)畫(huà)看上去很自然,沒(méi)有生硬的切換動(dòng)畫(huà)。我們還需要在目標(biāo) Activity finish之前執(zhí)行返回的動(dòng)畫(huà)桃移,有始有終屋匕,比較和諧。
注意源碼中的主題
- 先在 BaseActivity 之中定義好觸發(fā) Activity 的要使用的 Key
companion object {
// 坐標(biāo)值
const val REVEAL_POINT_Y = "reveal_y"
const val REVEAL_POINT_X = "reveal_x"
// 動(dòng)畫(huà)中的色值
const val REVEAL_COLOR_START = "color_start"
}
- 在 onCreate() 中 setContentView() 之前設(shè)置狀態(tài)欄為透明(可以刪掉查看動(dòng)畫(huà)效果)
private var point = arrayListOf<Int>() // 坐標(biāo)點(diǎn)合集
private var colorReveal = 0 // 動(dòng)畫(huà)的色值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
}
getIntentPointDot(intent)
}
- getIntentPointDot(intent) 是個(gè)啥借杰?
private fun getIntentPointDot(intent: Intent) {
val extraY = intent.getIntExtra(REVEAL_POINT_Y, 0)
val extraX = intent.getIntExtra(REVEAL_POINT_X, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
colorReveal = intent.getIntExtra(REVEAL_COLOR_START,ContextCompat.getColor(this, R.color.colorAccent))
// 清除坐標(biāo)合集过吻,確保數(shù)據(jù)不會(huì)錯(cuò)亂
point.clear()
// 確保數(shù)據(jù)值為默認(rèn)值,都是0就認(rèn)為不開(kāi)啟動(dòng)畫(huà)
if ((extraX != 0) or (extraY != 0)) {
point.add(extraX)
point.add(extraY)
}
}
}
啟動(dòng)動(dòng)畫(huà)
- 基本數(shù)據(jù)獲取完蔗衡,重寫(xiě) setContentView() 方法纤虽,添加視圖樹(shù)預(yù)加載監(jiān)聽(tīng),在監(jiān)聽(tīng)中執(zhí)行動(dòng)畫(huà)绞惦,這個(gè)視覺(jué)上比較順暢
override fun setContentView(layoutResID: Int) {
val view = LayoutInflater.from(this).inflate(layoutResID, null)
setContentView(view)
}
override fun setContentView(view: View) {
super.setContentView(view)
// 只有在有坐標(biāo)值的時(shí)候才執(zhí)行動(dòng)畫(huà)
if ((point.size >= 2) and (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
// 使用 decorView 的原因是因?yàn)槲覀円饔玫秸麄€(gè)屏幕逼纸,包括狀態(tài)欄,這樣視覺(jué)效果很好
val decorView = window.decorView as ViewGroup
// 創(chuàng)建一個(gè) View 放到 decorView 中用于設(shè)置背景顏色济蝉,填充執(zhí)行動(dòng)畫(huà)的時(shí)候的背景杰刽,動(dòng)畫(huà)默認(rèn)是透明背景的
val view1 = View(this)
view1.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
// 給View設(shè)置背景色填充動(dòng)畫(huà)執(zhí)行時(shí)的顏色
view1.setBackgroundColor(colorReveal)
decorView.addView(view1)
}
// 添加視圖樹(shù)預(yù)加載監(jiān)聽(tīng),在第一次加載時(shí)再執(zhí)行動(dòng)畫(huà)王滤,直接執(zhí)行會(huì)出異常贺嫂。
decorView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// 需要在該回調(diào)方法中移除監(jiān)聽(tīng),否則會(huì)收到多次監(jiān)聽(tīng)回調(diào)雁乡,這樣動(dòng)畫(huà)就重復(fù)執(zhí)行了
decorView.viewTreeObserver.removeOnPreDrawListener(this)
// 創(chuàng)建揭露動(dòng)畫(huà)第喳,false 表示是start的動(dòng)畫(huà)而不是finish的動(dòng)畫(huà)
val animator = createRevealAnimator(false, decorView)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 700
// 當(dāng)前動(dòng)畫(huà)是從動(dòng)畫(huà)執(zhí)行時(shí)的顏色改變到透明色,用作動(dòng)畫(huà)執(zhí)行完成之后逐漸恢復(fù)應(yīng)有界面而不是立刻
val valueAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorReveal, Color.TRANSPARENT)
valueAnimator.duration = 500
valueAnimator.addUpdateListener {
// 在監(jiān)聽(tīng)里面設(shè)置當(dāng)前進(jìn)度所產(chǎn)生的色值
view1.setBackgroundColor(it.animatedValue as Int)
}
val animatorSet = AnimatorSet()
// 順序執(zhí)行蔗怠,也可以把 before 改成 with 同時(shí)執(zhí)行看看效果墩弯,同時(shí)執(zhí)行的話(huà)推薦兩個(gè)動(dòng)畫(huà)的時(shí)長(zhǎng)設(shè)置成相同的
animatorSet.play(animator).before(valueAnimator)
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
// 動(dòng)畫(huà)執(zhí)行完畢 view1 這個(gè)View已經(jīng)是不需要的了,沒(méi)必要再留在decorView里面
decorView.removeView(view1)
}
})
animatorSet.start()
return false
}
})
}
- 啥是 createRevealAnimator() 寞射?
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun createRevealAnimator(isFinish: Boolean, view: View): Animator {
val hypo = Math.hypot(view.height.toDouble(), view.width.toDouble())
val startRadius = if (isFinish) hypo.toFloat() else 0f
val endRadius = if (isFinish) 0f else hypo.toFloat()
// 創(chuàng)建揭露動(dòng)畫(huà)渔工,view是作用目標(biāo),point[0] 和 point[1]桥温,是x引矩,y坐標(biāo)是放大開(kāi)始的坐標(biāo)也是縮小結(jié)束的坐標(biāo)
val animator = ViewAnimationUtils.createCircularReveal(view, point[0], point[1], startRadius, endRadius)
animator.duration = 500
return animator
}
到這里啟動(dòng)動(dòng)畫(huà)就已經(jīng)完成了,可以先看看效果了侵浸,下面我們來(lái)實(shí)現(xiàn) finish() 動(dòng)畫(huà)旺韭,這樣看起來(lái)就更順暢了
結(jié)束動(dòng)畫(huà)
- finish 動(dòng)畫(huà)就要多監(jiān)聽(tīng)?zhēng)讉€(gè)方法了
我們常使用到的就是 finish() , onBackPressed() ,兩個(gè)方法, supportFinishAfterTransition() 這個(gè)方法可以單獨(dú)搜一下是有什么不同掏觉。
override fun finish() {
// 如果坐標(biāo)點(diǎn)是空就執(zhí)行正常的 finish 方法区端,下面兩個(gè)方法同理
if (point.size >= 2) {
finishAnimator(1)
} else {
super.finish()
}
}
override fun supportFinishAfterTransition() {
if (point.size >= 2) {
finishAnimator(2)
} else {
super.supportFinishAfterTransition()
}
}
override fun onBackPressed() {
if (point.size >= 2) {
finishAnimator(3)
} else {
super.onBackPressed()
}
}
- finishAnimator() 里面是啥?
實(shí)現(xiàn)方式還是跟啟動(dòng)的時(shí)候差不多澳腹。只是我們是 finish 并且多個(gè) finish 的方法织盼,所以傳個(gè) index 值以便區(qū)分
private fun finishAnimator(index: Int) {
if ((point.size >= 2) and (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
val decorView = window.decorView as ViewGroup
val view1 = View(this)
decorView.addView(view1)
// 給View設(shè)置背景色填充動(dòng)畫(huà)執(zhí)行時(shí)的顏色
view1.setBackgroundColor(colorReveal)
// 創(chuàng)建 finish 動(dòng)畫(huà)杨何,參數(shù)傳 true
val animator = createRevealAnimator(true, decorView)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 600
val animatorSet = AnimatorSet()
animatorSet.play(animator)
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
// 動(dòng)畫(huà)執(zhí)行完畢直接把 decorView 設(shè)置成透明,這樣 Activity finish 時(shí)的默認(rèn)動(dòng)畫(huà)就看不到了
decorView.alpha = 0f
// view1 沒(méi)用了沥邻,刪掉
decorView.removeView(view1)
// 把坐標(biāo)點(diǎn)清除危虱,再次調(diào)用 finish 方法退出當(dāng)前 activity, finish 方法中已做校驗(yàn)
point.clear()
// 根據(jù)傳的 index 調(diào)用不同的 finish 方法。
when (index) {
1 -> finish()
2 -> supportFinishAfterTransition()
3 -> onBackPressed()
}
}
})
animatorSet.start()
}
}
好了唐全,關(guān)閉動(dòng)畫(huà)也完成了埃跷,效果看不了啊,因?yàn)槲覀冞€沒(méi)弄啟動(dòng)的觸發(fā)啊邮利。
觸發(fā)動(dòng)畫(huà)
觸發(fā)動(dòng)畫(huà)弥雹,直接把上面定義好的值傳過(guò)去就行了,不過(guò)我們得先獲取值才能傳啊近弟。
val intentActivityReveal = intentActivityReveal(viewSetting, OrderManagerActivity::class.java)
startActivity(intentActivityReveal, ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle())
就是這兩句缅糟,intentActivityReveal() 是個(gè) Kotlin 的擴(kuò)展方法,請(qǐng)勿吐槽這個(gè)方法名祷愉,我不會(huì)英語(yǔ)。赦颇。二鳄。
- intentActivityReveal() 方法里是啥?
/**
* 使用揭露動(dòng)畫(huà)啟動(dòng)activity
*/
fun Activity.intentActivityReveal(view: View, clazz: Class<*>): Intent {
return initIntent(this, view, clazz)
}
/**
* 使用揭露動(dòng)畫(huà)啟動(dòng)activity
*/
fun Fragment.intentActivityReveal(view: View, clazz: Class<*>): Intent {
return initIntent(activity!!, view, clazz)
}
就是這個(gè)媒怯,我是真的皮订讼,-__-
- initIntent()
private fun initIntent(activity: Activity, view: View, clazz: Class<*>): Intent {
val location = IntArray(2)
view.getLocationOnScreen(location)
var rgb = ContextCompat.getColor(activity, R.color.colorAccent)
try {
if (view is ImageView) {
val drawable = view.drawable as BitmapDrawable
// 使用 Palette 來(lái)吸取圖片的主要色值
val palette = Palette.from(drawable.bitmap)
val generate = palette.generate()
rgb = getRgb(generate, activity)
location[0] = location[0] + view.width / 2
} else {
val background = view.background
if (background is BitmapDrawable) {
val palette = Palette.from(background.bitmap)
val generate = palette.generate()
getRgb(generate, activity)
location[0] = location[0] + view.width / 2
} else if (background is ColorDrawable) {
rgb = if (view is TextView) {
if (view.compoundDrawables.isNotEmpty()) {
val bitmapDrawable = when {
view.compoundDrawables[0] != null -> {
location[0] = view.paddingLeft + view.compoundDrawablePadding
view.compoundDrawables[0] as BitmapDrawable
}
view.compoundDrawables[1] != null -> {
location[0] = location[0] + view.width / 2
view.compoundDrawables[1] as BitmapDrawable
}
view.compoundDrawables[2] != null -> {
location[0] = view.paddingRight + view.compoundDrawablePadding
view.compoundDrawables[2] as BitmapDrawable
}
view.compoundDrawables[3] != null -> {
location[0] = location[0] + view.width / 2
view.compoundDrawables[3] as BitmapDrawable
}
else -> {
BitmapDrawable()
}
}
val palette = Palette.from(bitmapDrawable.bitmap)
val generate = palette.generate()
getRgb(generate, activity)
} else {
background.color
}
} else {
background.color
}
}
}
} catch (e: Exception) {
Log.e("lmy", "沒(méi)有讀取到顏色或者背景轉(zhuǎn)換失敗了")
}
val intent = Intent(activity, clazz)
intent.putExtra(BaseActivity.REVEAL_POINT_X, location[0])
intent.putExtra(BaseActivity.REVEAL_POINT_Y, location[1] + (view.height / 2))
intent.putExtra(BaseActivity.REVEAL_COLOR_START, rgb)
return intent
}
原諒我這么垃圾的代碼,主要以實(shí)現(xiàn)效果為主扇苞,請(qǐng)盡量忽略欺殿,使用 Palette 來(lái)吸取圖片的主要色值,獲取 x 鳖敷,y 坐標(biāo)脖苏,計(jì)算 view 的中心點(diǎn),如果是 TextView 的話(huà)定踱,獲取 drawableTop 等屬性設(shè)置的圖片棍潘。如果有背景就獲取圖片的背景色,判斷不完美敬請(qǐng)諒解崖媚。
- getRgb()
private fun getRgb(palette: Palette, activity: Activity): Int {
return when {
palette.lightVibrantSwatch != null -> {
palette.lightVibrantSwatch?.rgb!!
}
palette.lightMutedSwatch != null -> {
palette.lightMutedSwatch?.rgb!!
}
palette.vibrantSwatch != null -> {
palette.vibrantSwatch?.rgb!!
}
palette.darkVibrantSwatch != null -> {
palette.darkVibrantSwatch?.rgb!!
}
palette.darkMutedSwatch != null -> {
palette.darkMutedSwatch?.rgb!!
}
palette.dominantSwatch != null -> {
palette.dominantSwatch?.rgb!!
}
else -> {
ContextCompat.getColor(activity, R.color.colorAccent)
}
}
}
因?yàn)?Palette 也不能保證能夠拿到你所需要的值亦歉,所有有可能為空。
這就結(jié)束了畅哑,這么多代碼很影響觀(guān)感肴楷,沒(méi)辦法,以后直接弄截圖試試荠呐。如果出現(xiàn)錯(cuò)誤啥的請(qǐng)對(duì)比一下源碼赛蔫,如果源碼有啥錯(cuò)誤的話(huà)绷杜,請(qǐng)告訴我,我就看看濒募,不改鞭盟。