Android仿小紅書/Lemon8共享元素動(dòng)畫的實(shí)現(xiàn)方式

最近小伙伴有個(gè)需求资溃,就是實(shí)現(xiàn)類似于小紅書武翎、Lemon8的共享元素轉(zhuǎn)場(chǎng)效果,查了一圈發(fā)現(xiàn)并沒有特別合適的Library溶锭,于是便做了一個(gè)開源Library項(xiàng)目宝恶,方便大家集成后,一行代碼實(shí)現(xiàn)Android仿小紅書趴捅、Lemon8的共享元素轉(zhuǎn)場(chǎng)效果卑惜。

Lemon8的共享元素轉(zhuǎn)場(chǎng)效果

1.實(shí)現(xiàn)思路:

經(jīng)過分析,如果要實(shí)現(xiàn)上圖的效果驻售,我們需要解決以下問題:

  • (1)實(shí)現(xiàn)自定義的共享元素:包括圓角過渡,TextView過渡更米,不同圖片間過渡等
  • (2)實(shí)現(xiàn)多個(gè)連續(xù)頁(yè)面的共享元素過渡(Q及以上系統(tǒng)欺栗,3個(gè)及以上連續(xù)的activity擁有共享元素動(dòng)畫時(shí),會(huì)有共享元素動(dòng)畫丟失的BUG)
  • (3)實(shí)現(xiàn)拖拽退出效果

問題已經(jīng)分析出來(lái)了征峦,接下來(lái)我們逐個(gè)解決:

  • (1)實(shí)現(xiàn)自定義的共享元素我們通過自定義Transition迟几,在createAnimator中返回響應(yīng)的動(dòng)畫來(lái)實(shí)現(xiàn)
  • (2)實(shí)現(xiàn)多頁(yè)面的共享元素過渡我們通過反射修復(fù)BUG(如果你對(duì)反射有顧慮或沒有該功能場(chǎng)景,則不需關(guān)注該方式)
  • (3)實(shí)現(xiàn)拖拽退出效果栏笆,這里我通過另外一個(gè)開源項(xiàng)目來(lái)更加完整的解決該問題:FastDragExitLayout

2.集成方式:

allprojects {
    repositories {
        ...
        maven { url 'https://www.jitpack.io' }
    }
}
 // 注意:本Library基于androidx
 implementation 'com.github.Arcns:fast-transition:1.0.2'
 // 可選:如果你需要使用FastRoundedItem(圓角的共享元素動(dòng)畫)类腮,那么你項(xiàng)目中需要引入fast rounded
 implementation 'com.github.Arcns.arc-fast:rounded:1.23.1'
 // 可選:如果你需要使用FastDisposableFastTextViewItem(圓角的共享元素動(dòng)畫),那么你項(xiàng)目中需要引入fast textview
 implementation 'com.github.Arcns.arc-fast:text-view:1.23.1'

3.使用方式

本Library對(duì)共享元素轉(zhuǎn)場(chǎng)的配置進(jìn)行了簡(jiǎn)化蛉加,減少了使用的復(fù)雜度蚜枢,在簡(jiǎn)單場(chǎng)景集成時(shí)僅需兩步:

  • (1)在轉(zhuǎn)場(chǎng)開始頁(yè)跳轉(zhuǎn)到目標(biāo)頁(yè)時(shí),使用FastTransitionViewManager配置共享元素動(dòng)畫和啟動(dòng)目標(biāo)頁(yè)
// 在開始頁(yè) StartActivity.kt
// 跳轉(zhuǎn)到目標(biāo)頁(yè)
fun goTarget(){
     // 1针饥、添加需要參與轉(zhuǎn)場(chǎng)的共享元素并配置所需動(dòng)畫
    val fastTransitionViewManager = FastTransitionViewManager()
    fastTransitionViewManager.addView(
        "IMAGE", // 共享元素的key
        ivImage, // 共享元素view
        FastRoundedItem(FastRoundedValue(12f.dpToPx)),//共享元素動(dòng)畫:圓角動(dòng)畫
        FastSystemTransitionItem(FastSystemTransitionType.ChangeImageTransform),//共享元素動(dòng)畫:圖片切換動(dòng)畫
        ... // 可以配置更多動(dòng)畫
    )
    fastTransitionViewManager.addView(...)
    fastTransitionViewManager.addView(...)
    fastTransitionViewManager.addView(...) // 可以添加更多需要參與轉(zhuǎn)場(chǎng)的共享元素
    // 2厂抽、通過startActivity啟動(dòng)目標(biāo)頁(yè)
    fastTransitionViewManager.startActivity(
        activity = this, // 當(dāng)前頁(yè)activity
        targetActivityCLass = TargetActivity::class.java, // 目標(biāo)頁(yè)Class
        targetDataID = "1", // 可選:目標(biāo)頁(yè)對(duì)應(yīng)的數(shù)據(jù)ID,默認(rèn)為null
        applyIntent = { intent-> 
            // 可選:intent回調(diào)丁眼,你可以在這里為intent添加更多數(shù)據(jù)
        }
    )
}
  • (2)在到目標(biāo)頁(yè)的onCreate中筷凤,使用FastTransitionTargetManager配置與開始頁(yè)對(duì)應(yīng)的共享元素并應(yīng)用轉(zhuǎn)場(chǎng)動(dòng)畫
// 在目標(biāo)頁(yè) TargetActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val transitionTargetManager = FastTransitionTargetManager.getManager(this)
    // 1、配置與`開始頁(yè)`對(duì)應(yīng)的共享元素
    transitionTargetManager?.setTransitionView(
         "IMAGE", // 與`開始頁(yè)`對(duì)應(yīng)的共享元素的key
         ivImage // 與`開始頁(yè)`對(duì)應(yīng)的共享元素view
    )
    transitionTargetManager?.setTransitionView(...)
    // 2苞七、應(yīng)用轉(zhuǎn)場(chǎng)動(dòng)畫
    transitionTargetManager?.applyTransitionEnterAndReturnConfig(
        duration = 150, // 可選:轉(zhuǎn)場(chǎng)動(dòng)畫時(shí)長(zhǎng)藐守,默認(rèn)為150,
        postponeEnterTransition = false, // 可選:是否暫停轉(zhuǎn)場(chǎng)動(dòng)畫挪丢,直到用戶調(diào)用startTransitionEnter再開始轉(zhuǎn)場(chǎng)動(dòng)畫,默認(rèn)為false
        postponeEnterTransitionTimeout = 500, // 可選:如果暫停轉(zhuǎn)場(chǎng)動(dòng)畫卢厂,那么達(dá)到該超時(shí)時(shí)間仍未調(diào)用startTransitionEnter時(shí)乾蓬,管理器將自動(dòng)開始轉(zhuǎn)場(chǎng)動(dòng)畫
        pageCurrentScale = { 1f }, // 可選:返回當(dāng)前頁(yè)面的縮放比例,該方法一般用于與拖拽退出結(jié)合使用足淆,默認(rèn)為null
        onTransitionEnd = {
            // 可選:轉(zhuǎn)場(chǎng)動(dòng)畫結(jié)束的回調(diào)巢块,默認(rèn)為null
        }
,
    )
}
// 可選:修復(fù)Q及以上系統(tǒng),activity調(diào)用onStop后共享元素動(dòng)畫丟失的BUG
override fun onStop() {
    transitionTargetManager?.onStop()
    super.onStop()
}
  • 補(bǔ)充巧号,如何在跳轉(zhuǎn)到新頁(yè)面后族奢,在返回時(shí)更改兩邊的共享元素。
    例如從RecyclerView頁(yè)跳轉(zhuǎn)到ViewPager頁(yè)丹鸿,然后用戶在ViewPager頁(yè)滑動(dòng)到了其他Item越走,在返回時(shí)希望能夠看到ViewPager頁(yè)該Item與RecyclerView頁(yè)對(duì)應(yīng)Item的共享元素動(dòng)畫
// 在開始頁(yè) ListActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    // 由于在目標(biāo)頁(yè),我們可能瀏覽到了其他的Item靠欢,因此返回到開始頁(yè)時(shí)廊敌,我們需要把共享元素更新到對(duì)應(yīng)的Item中
    setExitSharedElementCallback(object : SharedElementCallback() {
        override fun onMapSharedElements(
            names: MutableList<String>,
            sharedElements: MutableMap<String, View>
        ) {
            // 在此處通過transitionTargetManager.changeExitSharedElements對(duì)共享元素的Item進(jìn)行更改,以下為演示代碼
            if (viewPagerItem == -1) return
            val selectedViewHolder =
                recyclerView.findViewHolderForAdapterPosition(viewPagerItem) ?: return
            transitionTargetManager.changeExitSharedElements(
                sharedElements,
                TestData.KEY_IMAGE to selectedViewHolder.itemView.findViewById(R.id.ivImage)
            )
        }
    })
}

override fun onActivityReenter(resultCode: Int, data: Intent?) {
    // 由于在目標(biāo)頁(yè)门怪,我們可能瀏覽到了其他的Item骡澈,因此返回到開始頁(yè)時(shí),我們需要把共享元素更新到對(duì)應(yīng)的Item中
    // 在此處根據(jù)實(shí)際需求更改布局掷空,以下為演示代碼:先把動(dòng)畫暫停肋殴,然后把列表移動(dòng)到目標(biāo)Item龄捡,再恢復(fù)動(dòng)畫床嫌,以便在setExitSharedElementCallback回調(diào)中把共享元素更新到對(duì)應(yīng)的Item中
    if (ViewPagerDataSource.currentItem == -1) return
    postponeEnterTransition() // 暫停動(dòng)畫
    recyclerView.layoutManager?.scrollToPosition(viewPagerItem)
    recyclerView.post {
        // 更改布局后恢復(fù)執(zhí)行動(dòng)畫
        startPostponedEnterTransition()
    }
}

// 在目標(biāo)頁(yè) ViewPagerActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    // 用戶切換Item時(shí),更新共享動(dòng)畫元素到當(dāng)前Item
    viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            // 切換后怒详,通過transitionTargetManager.setTransitionView重新更改需要參與轉(zhuǎn)場(chǎng)的共享元素
            // 以下為演示代碼:
            ViewPagerDataSource.currentItem = position
            val fragment = viewPager.findFragmentAtPosition(
                supportFragmentManager,
                position
            ) as? ViewPagerFragment
            if (fragment != null) {
                // 用戶切換Item時(shí)酿傍,更新共享動(dòng)畫元素到當(dāng)前Item
                transitionTargetManager?.setTransitionView(
                    TestData.KEY_IMAGE,
                    fragment.binding.ivImage
                )
            }
            super.onPageSelected(position)
        }
    })
}

 override fun onBackPressed() {
    // 注意返回到開始頁(yè)之前烙懦,您必須先調(diào)用setResult,以便開始頁(yè)觸發(fā)onActivityReenter回調(diào)(進(jìn)行布局更新處理)
    setResult(100)
    super.onBackPressed()
}

4.修復(fù)多個(gè)連續(xù)頁(yè)面的共享元素過渡時(shí)赤炒,共享元素動(dòng)畫丟失的BUG

注意:該BUG需要使用反射進(jìn)行修復(fù)氯析,截至到目前最新的API 33,該方法仍然能夠有效修復(fù)該BUG可霎,但如果你對(duì)反射有顧慮或沒有該功能場(chǎng)景魄鸦,則不要使用以下方法。

// 在自定義Application MyApplication.kt
class MyApplication : Application() {
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        // 啟用多個(gè)連續(xù)頁(yè)面的共享元素過渡功能
        FastTransitionUtils.enableMultipleActivityTransition(this)
    }
}
// 在目標(biāo)頁(yè) TargetActivity.kt
// 修復(fù)Q及以上系統(tǒng)癣朗,3個(gè)及以上連續(xù)的activity擁有共享元素動(dòng)畫時(shí)拾因,共享元素動(dòng)畫丟失的BUG(使用反射)
override fun finishAfterTransition() {
    transitionTargetManager?.finishAfterTransition()
    super.finishAfterTransition()
}

5.支持的動(dòng)畫

本Library在原有系統(tǒng)自帶的共享元素動(dòng)畫基礎(chǔ)上,擴(kuò)展了一些常用的動(dòng)畫效果,所有內(nèi)置動(dòng)畫如下:

動(dòng)畫名 簡(jiǎn)介
FastTextViewItem TextView的共享元素動(dòng)畫绢记,能夠?qū)崿F(xiàn)文字大小扁达、顏色、行高蠢熄、間距跪解、粗體等屬性的過渡動(dòng)畫
FastRoundedItem 圓角的共享元素動(dòng)畫(需要使用圓角控件FastRounded
FastToggleImageViewItem 支持根據(jù)狀態(tài)切換圖片的共享元素動(dòng)畫,通常用于點(diǎn)贊签孔、收藏叉讥、關(guān)注等需要根據(jù)狀態(tài)同時(shí)切換開始頁(yè)目標(biāo)頁(yè)圖片的場(chǎng)景
FastSimpleItem 可實(shí)現(xiàn)漸變或縮放的簡(jiǎn)單共享元素動(dòng)畫,通常用于那些內(nèi)部不一致的共享元素容器控件饥追,避免內(nèi)部不同導(dǎo)致過渡時(shí)突兀
FastImageItem 支持切換圖片或背景的共享元素動(dòng)畫图仓,通常用于不同圖片間的過渡,該動(dòng)畫會(huì)通過漸變效果的過渡到另一張圖片
FastBackgroundFadeItem 背景漸變顯示或隱藏的共享元素動(dòng)畫但绕,通常用于只有一邊有背景而另一邊沒有的場(chǎng)景
FastDisposableFastTextViewItem FastTextView漸變消失的共享元素動(dòng)畫救崔,該動(dòng)畫會(huì)在目標(biāo)頁(yè)面創(chuàng)建相同控件以完成漸變消失動(dòng)畫
FastSystemTransitionItem 系統(tǒng)自帶的共享元素動(dòng)畫,用于實(shí)現(xiàn)在本庫(kù)中使用系統(tǒng)自帶的動(dòng)畫

6.擴(kuò)展自定義的動(dòng)畫

如果內(nèi)置的動(dòng)畫不符合你的需求場(chǎng)景捏顺,或者你需要讓你的其他控件也參與共享元素動(dòng)畫六孵,那么你可以擴(kuò)展自定義的動(dòng)畫.
本Library對(duì)擴(kuò)展自定義的動(dòng)畫也進(jìn)行了簡(jiǎn)化,通常情況下你只需兩步即可實(shí)現(xiàn)擴(kuò)展自定義的動(dòng)畫:

  • (1)創(chuàng)建自定義的動(dòng)畫計(jì)算器
// 1.1 繼承FastBaseCalculator<計(jì)算器的數(shù)據(jù)類型幅骄,控件的類型>
class CustomCalculator(
    _first: Float,// 動(dòng)畫起始數(shù)據(jù)劫窒,演示用法,你可以按需替換為你自己的構(gòu)造參數(shù)
    _last: Float, // 動(dòng)畫結(jié)束數(shù)據(jù)拆座,演示用法烛亦,你可以按需替換為你自己的構(gòu)造參數(shù)
) : FastBaseCalculator<Float, View>( // 此處<Float, View>僅為演示用法,你可以按需替換為你自己的數(shù)據(jù)類型與控件類型
    viewClass = View::class,
    first = _first,
    last = _last
) {
    // 1.2 返回動(dòng)畫起始數(shù)據(jù)與結(jié)束數(shù)據(jù)的差額
    override val differ: Float by lazy { 
         last - first // 此處僅為演示用法懂拾,你可以按需替換為你自己的差額計(jì)算方式
    }

    // 1.3 返回某個(gè)進(jìn)度下的動(dòng)畫數(shù)據(jù)(progress的區(qū)間為0至1)
    override fun getValue(progress: Float): Float =
        calculatorFloatValue(first, last, differ, progress) // 此處僅為演示用法,你可以按需替換為你自己的進(jìn)度數(shù)據(jù)計(jì)算方式

    // 1.4 把某個(gè)進(jìn)度下的動(dòng)畫數(shù)據(jù)設(shè)置到你的控件中
    override fun setView(view: View, progress: Float, value: Float) {
        // 此處僅為演示用法铐达,你可以按需替換為你自己的控件設(shè)置方式
        view.alpha = value
    }
}
  • (2)創(chuàng)建自定義的動(dòng)畫Item岖赋,并返回上一步的動(dòng)畫計(jì)算器
@Parcelize
data class CustomItem(
    var start: Float,
    var end: Float,
) : FastTransitionItem() {

    // 返回動(dòng)畫計(jì)算器
    override fun getCalculator(
        isEnter: Boolean,
        pageCurrentScale: Float?
    ): FastSimpleCalculator {
        // 此處僅為演示用法,你可以按需替換為你自己的計(jì)算器構(gòu)建及返回方式
        return if (isEnter) CustomCalculator(start, end)
        else CustomCalculator(end, start)
    }
    
    // 可選:校驗(yàn)動(dòng)畫Item當(dāng)前是否可用瓮孙,如果不可用將不調(diào)用getCalculator
    override val enable: Boolean get() = start != end && start >= 0f && end >= 0f // 此處僅為演示用法唐断,你可以按需替換為你自己的校驗(yàn)方法
    
    // 可選:視圖動(dòng)畫準(zhǔn)備(下一步將創(chuàng)建計(jì)算器)的回調(diào),您可以在此處進(jìn)行視圖相關(guān)的初始化杭抠,例如根據(jù)視圖準(zhǔn)備目標(biāo)頁(yè)對(duì)應(yīng)的共享元素?cái)?shù)據(jù)
    override fun onViewAnimReady(isEnter: Boolean, view: View, pageCurrentScale: Float?) {
        // 此處僅為演示用法脸甘,你可以按需替換為你自己的初始化方法
        end = view.alpha
    }
    
    // 可選:執(zhí)行進(jìn)入動(dòng)畫前的回調(diào),您可以在此進(jìn)行進(jìn)入動(dòng)畫前的初始化工作
    override fun onEnterBefore(activity: Activity, transitionConfig: FastTransitionConfig) {
    }

    // 可選:執(zhí)行離開動(dòng)畫前的回調(diào)偏灿,您可以在此進(jìn)行離開動(dòng)畫前的初始化工作
    override fun onReturnBefore(view: View, pageCurrentScale: Float?) {
    }
}

項(xiàng)目地址:
https://github.com/Arcns/fast-transition

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丹诀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铆遭,老刑警劉巖硝桩,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異枚荣,居然都是意外死亡碗脊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門橄妆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)衙伶,“玉大人,你說我怎么就攤上這事害碾∈妇ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵蛮原,是天一觀的道長(zhǎng)卧须。 經(jīng)常有香客問我,道長(zhǎng)儒陨,這世上最難降的妖魔是什么花嘶? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蹦漠,結(jié)果婚禮上椭员,老公的妹妹穿的比我還像新娘。我一直安慰自己笛园,他們只是感情好隘击,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著研铆,像睡著了一般埋同。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上棵红,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天凶赁,我揣著相機(jī)與錄音,去河邊找鬼逆甜。 笑死虱肄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的交煞。 我是一名探鬼主播咏窿,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼素征!你這毒婦竟也來(lái)了集嵌?” 一聲冷哼從身側(cè)響起萝挤,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纸淮,沒想到半個(gè)月后平斩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咽块,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年绘面,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侈沪。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡揭璃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亭罪,到底是詐尸還是另有隱情瘦馍,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布应役,位于F島的核電站情组,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏箩祥。R本人自食惡果不足惜院崇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袍祖。 院中可真熱鬧底瓣,春花似錦、人聲如沸蕉陋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凳鬓。三九已至茁肠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缩举,已是汗流浹背官套。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚁孔,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓惋嚎,卻偏偏與公主長(zhǎng)得像杠氢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子另伍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容