“用Android复刻Apple产品UI”(2)——丝滑的AppStore卡片转场动画_android卡片切换

private val itemSpaceDistance = 24f.dp.toInt()
private val horizontalSpace = 18f.dp.toInt()

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
    super.getItemOffsets(outRect, view, parent, state)
    outRect.apply {
        this.left = horizontalSpace
        this.right = horizontalSpace
        this.bottom = itemSpaceDistance
    }
    if (parent.getChildAdapterPosition(view) == 0) {
        outRect.top = itemSpaceDistance
    }
}

}


* 给每一个卡片创建点击事件,跳转到DetailFragment,并将卡片对应的数据加载进去:


这里,我使用ViewModel来实现Fragment之间的数据传输:将ViewModel的Provider设置为Activity,这样我们的ViewModel生命周期就跟随着Activity变化,以此帮助我们实现数据传输。


1. 初始化ViewModel,让其生命周期跟着activity走



//我们在HomeFragment.kt
articleCardViewModel = ViewModelProvider(activity).get(ArticleDetailViewModel::class.java)


2. 在这个activity内的任意fragment内,用同样的方式,获取这个viewModel



//我们在DetailFragment.kt
viewModel = ViewModelProvider(activity!!).get(ArticleDetailViewModel::class.java)


3. 卡片点击事件:当前卡片向viewModel传入这个卡片的值,随后由DetailFragment接收,它就能在渲染自身页面的时候获取这些值了,并成为了那个卡片的详情页。



//给recyclerView的每个Item添加点击事件
override fun onItemClick(viewHolder: RecyclerView.ViewHolder?) {
var position = cardRecyclerView.getChildLayoutPosition(viewHolder!!.itemView)
GlobalScope.launch(Dispatchers.Default) {
//更新主副标题、摘要等
articleDetailViewModel.articleCardData = cardArray[position]
//更新背景图片
articleDetailViewModel.updateBackGroundImage(resources, activity!!)
//传入当前item的位置,position
articleDetailViewModel.position = position.toString()
}

    //使用Navigation跳转至下一个页面。
} 

DetailFragment的布局在\_**article\_detail\_layout.xml**\_的基础上,外部添加了一层ScrollView来展示比较长的正文,并在内部添加了contentText的TextView,整体结构与预览如下所示:


![](https://img-blog.csdnimg.cn/img_convert/5856199794b01f160018c7acccfd8fbb.png)) ![](https://img-blog.csdnimg.cn/img_convert/636ef51bdebf3f3e53c2ca694f70989f.png)


DetailFragment接收数据,并渲染自己的画面:



//in DetailFragment.kt
//viewModel中传入的卡片相关数据
viewModel.articleCardData.apply {
view.findViewById(R.id.mainTitle).text = this.mainTitle
view.findViewById(R.id.cardTitle).text = this.cardTitle
view.findViewById(R.id.rootText).text = this.rootText
view.findViewById(R.id.mainTitle).setTextColor(this.mainTitleColor)
//设置正文
if (this.contentText != “”) {
view.findViewById(R.id.contentText).text = this.contentText
}
//设置背景图
view.findViewById(R.id.cardLinearLayout).background = viewModel.backGroundImage
}
view.findViewById(R.id.backGroundCard).transitionName = “backGroundCard${viewModel.position}”


* 至此,我们完成了静态页面的布局。最后,再用图片的形式梳理一下流程!


![image.png](https://img-blog.csdnimg.cn/img_convert/af7bcd0b17f5ce6344102b012f2b385a.png)


### 2.2 卡片与详情页之间的转场动画


终于到了最有意思的部分,这一环节我们请出最核心的角色:`SharedElementTransition共享元素动画`


#### 共享元素动画的使用介绍



> 
> 共享元素动画的官方介绍请跳转:[使用过渡为布局变化添加动画效果 | Android 开发者 | Android Developers (google.cn)]( )
> 
> 
> 



> 
> 附一个用得比较多的共享元素动画库:[Material-Motion]( )
> 
> 
> 


这里,我用自己的方式介绍一下:


* 共享元素动画既可以用于Fragment间,也可以用于Activity间,使用起来是相当便捷的,**只需要保证共享元素在两个Fragment的TransitionName一致,并在跳转前将其绑定即可。**
* 在这个切换过程,我们可以指定一个Transition动画来实现我们想要的效果,比如Fade()可以渐入渐出,ChangeTransform()实现尺寸变化。
* **Transition动画的底层是属性动画**,他会获取FragmentA中共享元素的某个值**作为起点**,比如位置x=0,y=0,再获取到FragmentB中共享元素的位置x=100,y=100**作为终点**,接着执行一个属性动画,来让这个共享元素平滑地转移过去。
* 知道了这个原理,我们可以很轻松地自定义Transition,只需要重写几个方法,控制我们需要的起点和终点的值,再定义我们想要的属性动画就好。具体可以见官方文档:[创建自定义过渡动画 | Android 开发者 | Android Developers (google.cn)]( )


#### 在RecyclerView中,让Item作为共享元素进行动画


在上面我们提到,想要执行属性动画的前提,是让两个Fragment的共享元素拥有相同的TransitionName,在RecycerView中,我们这样操作:


1. 在创建这些卡片流的时候,我们给**每个卡片的TransitionName赋值为"shared\_card${position}"**,position使它的位次,以此保证他们的TransitionName是独一无二的。
2. 接着,我们在卡片被点击后,**给DetailFragment传入当前被点击卡片的TransitionName**,并让DetailFragment修改自己的那个卡片组件的TransitionName为"shared\_card${position}"


如此,我们便实现了绑定。


接着,便是让每个Item的点击事件添加一条Navigation跳转!(当然也可以用FragmentManager):


a. 我们需要首先创建一个当前View到对应TransitionName的绑定(命名规则上面提过)



//首先创建一个绑定,形式是 view to TransitionName
val extras = FragmentNavigatorExtras(
viewHolder.itemView.findViewById(R.id.backGroundCardView) to “backGroundCard${position}”,
)


b. 然后,我们使用navigate()实现跳转,函数内部我们填入目标fragment ID与先前绑定的\_**extras**\_



view!!.findNavController().navigate(
R.id.action_to_article, null,
null,
extras
)


完成共享元素动画的最后一步,在\_**DetailFragment**\_(目标Fragment)内设置我们需要的Transition效果。 sharedElementEnterTransition对象接受一个Transition类,Transition则包含了我们需要实现的动画效果。这里我们使用的R.transiton.shared是自定义的Transition集合。



//in DetailFragment.kt
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.shared)
sharedElementReturnTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.shared)


#### 我们使用的共享元素动画Transition:R.transition.shared



<transitionSet android:transitionOrdering="together">
    <transition class="isense.com.ui.myTransition.MyCornerTransition">
    </transition>

</transitionSet>
<changeBounds android:interpolator="@anim/my_overshoot">
</changeBounds>
<changeTransform android:interpolator="@anim/my_overshoot">
</changeTransform>

在如上代码中,我们定义的Transition包括了三个内容,分别是:changeBounds, CornerTransiton(自己定义的)和changeTransform。我们借助他们来实现所需要的卡片展开效果。


#### 为什么使用OverShootInterpolator?


前面提到,AppStore原生的动画函数曲线是类弹簧的,这与OverShootInterpolator的函数曲线是类似的:  
 他们都会在到达目标值后,继续向前进一小步,然后再退回来,就像下方的函数曲线一样: ![image.png](https://img-blog.csdnimg.cn/img_convert/834999d036b9318bc0409ed9ecfedf2b.png) f(t)=t∗t∗((1.2+1)∗t+1.2)+1.0f(t) = t \* t \* ((1.2 + 1) \* t + 1.2) + 1.0f(t)=t∗t∗((1.2+1)∗t+1.2)+1.0


#### 怎么实现其他卡片的模糊?


这里,我借助了Github的开源库:[wasabeef/Blurry: Blurry is an easy blur library for Android (github.com)]( )  
 它可以实现将当前context的画面转为模糊,并重新映射回rootViewGroup。



viewHolder.itemView.visibility=View.INVISIBLE
Blurry.with(context).radius(25).sampling(1).animate(100).onto(NoiseConstraintLayout)
viewHolder.itemView.visibility=View.VISIBLE


#### 最后,为保证共享动画返回时的效果,请注意:


为了保证DetailFragment返回HomeFragment也能拥有共享动画的效果,请务必在HomeFragment的onCreate()内添加如下代码:



postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }


如果你觉得还挺有趣,可以点击下方链接浏览更多同类文章:  
 [“用Android复刻Apple产品UI”(3)—优雅的数据统计图表 - 掘金 (juejin.cn)]( )  
 [“用Android复刻Apple产品UI”(1)—丝滑的噪声监测音量条 - 掘金 (juejin.cn)]( )


## 写在最后


在技术领域内,没有任何一门课程可以让你学完后一劳永逸,再好的课程也只能是“师傅领进门,修行靠个人”。“学无止境”这句话,在任何技术领域,都不只是良好的习惯,更是程序员和工程师们不被时代淘汰、获得更好机会和发展的必要前提。


**如果你觉得自己学习效率低,缺乏正确的指导,可以评论区留言或私信,加入我们资源丰富,学习氛围浓厚的技术圈一起学习交流吧!**



> 
> 这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
> 
> 
> 


**【详细可整理扫描下方二维码免费领取】**  
 ![](https://img-blog.csdnimg.cn/d64145006ab34b5f811a68bed546ed01.png)


### 尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

**进阶学习视频**

![](https://img-blog.csdnimg.cn/img_convert/2df760151a3fad979d69eac9688d7226.webp?x-oss-process=image/format,png)

**附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题** (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

![](https://img-blog.csdnimg.cn/img_convert/f799f38f803f0ea5fcad54d264b1877b.webp?x-oss-process=image/format,png)



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

、常见算法题汇总。)

[外链图片转存中...(img-dMLqnaZx-1714525722930)]



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值