Android仿小红书/Lemon8共享元素动画的实现方式

最近小伙伴有个需求,就是实现类似于小红书、Lemon8的共享元素转场效果,查了一圈发现并没有特别合适的Library,于是便做了一个开源Library项目,方便大家集成后,一行代码实现Android仿小红书、Lemon8的共享元素转场效果。

Lemon8的共享元素转场效果

1.实现思路:

经过分析,如果要实现上图的效果,我们需要解决以下问题:

  • (1)实现自定义的共享元素:包括圆角过渡,TextView过渡,不同图片间过渡等
  • (2)实现多个连续页面的共享元素过渡(Q及以上系统,3个及以上连续的activity拥有共享元素动画时,会有共享元素动画丢失的BUG)
  • (3)实现拖拽退出效果

问题已经分析出来了,接下来我们逐个解决:

  • (1)实现自定义的共享元素我们通过自定义Transition,在createAnimator中返回响应的动画来实现
  • (2)实现多页面的共享元素过渡我们通过反射修复BUG(如果你对反射有顾虑或没有该功能场景,则不需关注该方式)
  • (3)实现拖拽退出效果,这里我通过另外一个开源项目来更加完整的解决该问题:FastDragExitLayout

2.集成方式:

allprojects {
    repositories {
        ...
        maven { url 'https://www.jitpack.io' }
    }
}
 // 注意:本Library基于androidx
 implementation 'com.github.Arcns:fast-transition:1.0.2'
 // 可选:如果你需要使用FastRoundedItem(圆角的共享元素动画),那么你项目中需要引入fast rounded
 implementation 'com.github.Arcns.arc-fast:rounded:1.23.1'
 // 可选:如果你需要使用FastDisposableFastTextViewItem(圆角的共享元素动画),那么你项目中需要引入fast textview
 implementation 'com.github.Arcns.arc-fast:text-view:1.23.1'

3.使用方式

本Library对共享元素转场的配置进行了简化,减少了使用的复杂度,在简单场景集成时仅需两步:

  • (1)在转场开始页跳转到目标页时,使用FastTransitionViewManager配置共享元素动画和启动目标页
// 在开始页 StartActivity.kt
// 跳转到目标页
fun goTarget(){
     // 1、添加需要参与转场的共享元素并配置所需动画
    val fastTransitionViewManager = FastTransitionViewManager()
    fastTransitionViewManager.addView(
        "IMAGE", // 共享元素的key
        ivImage, // 共享元素view
        FastRoundedItem(FastRoundedValue(12f.dpToPx)),//共享元素动画:圆角动画
        FastSystemTransitionItem(FastSystemTransitionType.ChangeImageTransform),//共享元素动画:图片切换动画
        ... // 可以配置更多动画
    )
    fastTransitionViewManager.addView(...)
    fastTransitionViewManager.addView(...)
    fastTransitionViewManager.addView(...) // 可以添加更多需要参与转场的共享元素
    // 2、通过startActivity启动目标页
    fastTransitionViewManager.startActivity(
        activity = this, // 当前页activity
        targetActivityCLass = TargetActivity::class.java, // 目标页Class
        targetDataID = "1", // 可选:目标页对应的数据ID,默认为null
        applyIntent = { intent-> 
            // 可选:intent回调,你可以在这里为intent添加更多数据
        }
    )
}
  • (2)在到目标页的onCreate中,使用FastTransitionTargetManager配置与开始页对应的共享元素并应用转场动画
// 在目标页 TargetActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val transitionTargetManager = FastTransitionTargetManager.getManager(this)
    // 1、配置与`开始页`对应的共享元素
    transitionTargetManager?.setTransitionView(
         "IMAGE", // 与`开始页`对应的共享元素的key
         ivImage // 与`开始页`对应的共享元素view
    )
    transitionTargetManager?.setTransitionView(...)
    // 2、应用转场动画
    transitionTargetManager?.applyTransitionEnterAndReturnConfig(
        duration = 150, // 可选:转场动画时长,默认为150,
        postponeEnterTransition = false, // 可选:是否暂停转场动画,直到用户调用startTransitionEnter再开始转场动画,默认为false
        postponeEnterTransitionTimeout = 500, // 可选:如果暂停转场动画,那么达到该超时时间仍未调用startTransitionEnter时,管理器将自动开始转场动画
        pageCurrentScale = { 1f }, // 可选:返回当前页面的缩放比例,该方法一般用于与拖拽退出结合使用,默认为null
        onTransitionEnd = {
            // 可选:转场动画结束的回调,默认为null
        }
,
    )
}
// 可选:修复Q及以上系统,activity调用onStop后共享元素动画丢失的BUG
override fun onStop() {
    transitionTargetManager?.onStop()
    super.onStop()
}
  • 补充,如何在跳转到新页面后,在返回时更改两边的共享元素。
    例如从RecyclerView页跳转到ViewPager页,然后用户在ViewPager页滑动到了其他Item,在返回时希望能够看到ViewPager页该Item与RecyclerView页对应Item的共享元素动画
// 在开始页 ListActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    // 由于在目标页,我们可能浏览到了其他的Item,因此返回到开始页时,我们需要把共享元素更新到对应的Item中
    setExitSharedElementCallback(object : SharedElementCallback() {
        override fun onMapSharedElements(
            names: MutableList<String>,
            sharedElements: MutableMap<String, View>
        ) {
            // 在此处通过transitionTargetManager.changeExitSharedElements对共享元素的Item进行更改,以下为演示代码
            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?) {
    // 由于在目标页,我们可能浏览到了其他的Item,因此返回到开始页时,我们需要把共享元素更新到对应的Item中
    // 在此处根据实际需求更改布局,以下为演示代码:先把动画暂停,然后把列表移动到目标Item,再恢复动画,以便在setExitSharedElementCallback回调中把共享元素更新到对应的Item中
    if (ViewPagerDataSource.currentItem == -1) return
    postponeEnterTransition() // 暂停动画
    recyclerView.layoutManager?.scrollToPosition(viewPagerItem)
    recyclerView.post {
        // 更改布局后恢复执行动画
        startPostponedEnterTransition()
    }
}

// 在目标页 ViewPagerActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    // 用户切换Item时,更新共享动画元素到当前Item
    viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            // 切换后,通过transitionTargetManager.setTransitionView重新更改需要参与转场的共享元素
            // 以下为演示代码:
            ViewPagerDataSource.currentItem = position
            val fragment = viewPager.findFragmentAtPosition(
                supportFragmentManager,
                position
            ) as? ViewPagerFragment
            if (fragment != null) {
                // 用户切换Item时,更新共享动画元素到当前Item
                transitionTargetManager?.setTransitionView(
                    TestData.KEY_IMAGE,
                    fragment.binding.ivImage
                )
            }
            super.onPageSelected(position)
        }
    })
}

 override fun onBackPressed() {
    // 注意返回到开始页之前,您必须先调用setResult,以便开始页触发onActivityReenter回调(进行布局更新处理)
    setResult(100)
    super.onBackPressed()
}

4.修复多个连续页面的共享元素过渡时,共享元素动画丢失的BUG

注意:该BUG需要使用反射进行修复,截至到目前最新的API 33,该方法仍然能够有效修复该BUG,但如果你对反射有顾虑或没有该功能场景,则不要使用以下方法。

// 在自定义Application MyApplication.kt
class MyApplication : Application() {
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        // 启用多个连续页面的共享元素过渡功能
        FastTransitionUtils.enableMultipleActivityTransition(this)
    }
}
// 在目标页 TargetActivity.kt
// 修复Q及以上系统,3个及以上连续的activity拥有共享元素动画时,共享元素动画丢失的BUG(使用反射)
override fun finishAfterTransition() {
    transitionTargetManager?.finishAfterTransition()
    super.finishAfterTransition()
}

5.支持的动画

本Library在原有系统自带的共享元素动画基础上,扩展了一些常用的动画效果,所有内置动画如下:

动画名 简介
FastTextViewItem TextView的共享元素动画,能够实现文字大小、颜色、行高、间距、粗体等属性的过渡动画
FastRoundedItem 圆角的共享元素动画(需要使用圆角控件FastRounded
FastToggleImageViewItem 支持根据状态切换图片的共享元素动画,通常用于点赞、收藏、关注等需要根据状态同时切换开始页目标页图片的场景
FastSimpleItem 可实现渐变或缩放的简单共享元素动画,通常用于那些内部不一致的共享元素容器控件,避免内部不同导致过渡时突兀
FastImageItem 支持切换图片或背景的共享元素动画,通常用于不同图片间的过渡,该动画会通过渐变效果的过渡到另一张图片
FastBackgroundFadeItem 背景渐变显示或隐藏的共享元素动画,通常用于只有一边有背景而另一边没有的场景
FastDisposableFastTextViewItem FastTextView渐变消失的共享元素动画,该动画会在目标页面创建相同控件以完成渐变消失动画
FastSystemTransitionItem 系统自带的共享元素动画,用于实现在本库中使用系统自带的动画

6.扩展自定义的动画

如果内置的动画不符合你的需求场景,或者你需要让你的其他控件也参与共享元素动画,那么你可以扩展自定义的动画.
本Library对扩展自定义的动画也进行了简化,通常情况下你只需两步即可实现扩展自定义的动画:

  • (1)创建自定义的动画计算器
// 1.1 继承FastBaseCalculator<计算器的数据类型,控件的类型>
class CustomCalculator(
    _first: Float,// 动画起始数据,演示用法,你可以按需替换为你自己的构造参数
    _last: Float, // 动画结束数据,演示用法,你可以按需替换为你自己的构造参数
) : FastBaseCalculator<Float, View>( // 此处<Float, View>仅为演示用法,你可以按需替换为你自己的数据类型与控件类型
    viewClass = View::class,
    first = _first,
    last = _last
) {
    // 1.2 返回动画起始数据与结束数据的差额
    override val differ: Float by lazy { 
         last - first // 此处仅为演示用法,你可以按需替换为你自己的差额计算方式
    }

    // 1.3 返回某个进度下的动画数据(progress的区间为0至1)
    override fun getValue(progress: Float): Float =
        calculatorFloatValue(first, last, differ, progress) // 此处仅为演示用法,你可以按需替换为你自己的进度数据计算方式

    // 1.4 把某个进度下的动画数据设置到你的控件中
    override fun setView(view: View, progress: Float, value: Float) {
        // 此处仅为演示用法,你可以按需替换为你自己的控件设置方式
        view.alpha = value
    }
}
  • (2)创建自定义的动画Item,并返回上一步的动画计算器
@Parcelize
data class CustomItem(
    var start: Float,
    var end: Float,
) : FastTransitionItem() {

    // 返回动画计算器
    override fun getCalculator(
        isEnter: Boolean,
        pageCurrentScale: Float?
    ): FastSimpleCalculator {
        // 此处仅为演示用法,你可以按需替换为你自己的计算器构建及返回方式
        return if (isEnter) CustomCalculator(start, end)
        else CustomCalculator(end, start)
    }
    
    // 可选:校验动画Item当前是否可用,如果不可用将不调用getCalculator
    override val enable: Boolean get() = start != end && start >= 0f && end >= 0f // 此处仅为演示用法,你可以按需替换为你自己的校验方法
    
    // 可选:视图动画准备(下一步将创建计算器)的回调,您可以在此处进行视图相关的初始化,例如根据视图准备目标页对应的共享元素数据
    override fun onViewAnimReady(isEnter: Boolean, view: View, pageCurrentScale: Float?) {
        // 此处仅为演示用法,你可以按需替换为你自己的初始化方法
        end = view.alpha
    }
    
    // 可选:执行进入动画前的回调,您可以在此进行进入动画前的初始化工作
    override fun onEnterBefore(activity: Activity, transitionConfig: FastTransitionConfig) {
    }

    // 可选:执行离开动画前的回调,您可以在此进行离开动画前的初始化工作
    override fun onReturnBefore(view: View, pageCurrentScale: Float?) {
    }
}

项目地址:
https://github.com/Arcns/fast-transition

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容