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

  • 创建一个CardView:
    • 默认的elevation=2帮助我们实现了阴影
    • 设置cardCornerRadius=14dp,实现卡片的圆角效果
  • 在CardView内部创建一个LinearLayout
    • 在其内部创建三个TextView,用于分别承载主副标题与摘要
    • 将这个LinearLayout的backGround作为我们背景图的容器,我们先放一个默认的进去。

至此,我们拥有的xml结构及其预览效果如下:

  1. 创建_HomeFragment_,处理最重要的RecyclerView
  • 我们创建RecyclerView所需的Adapter及ViewHolder
    • 我们先创建一个数据类,ArticleCardData,用来存储每一个卡片的文字内容和背景图ID;当然,这里面也可以放任何你希望卡片能够方便被自定义的内容。
data class ArticleCardData(
    val backGroundImage: Int = R.drawable.testimg,
    val cardTitle: String = "Latest",
    val mainTitle: String = "Extraordinarily,\nundefeated.",
    val rootText: String = "i-Sense makes life better.",
    val contentText: String = "",
    val mainTitleColor: Int = Color.parseColor("#fafdfb")
) 

  • 我们需要ViewHolder每次在执行绑定(onBindViewHolder)的时候,把文章的主副标题、背景等都加载进去
  • 初始化RecyclerView,设置adpater,layoutManager等。

在这里,需要注意的是,RecyclerView中,如果希望能够实现每一个Item的上下左右间距,我们需要自己去创建一个ItemDecoration类来把item装进去,来实现间距效果。

class CardItemDecoration : RecyclerView.ItemDecoration() {

    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) 

  1. 在这个activity内的任意fragment内,用同样的方式,获取这个viewModel
//我们在DetailFragment.kt
viewModel = ViewModelProvider(activity!!).get(ArticleDetailViewModel::class.java) 

  1. 卡片点击事件:当前卡片向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,整体结构与预览如下所示:

)

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

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

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

image.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<CardView>(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 xmlns:android="http://schemas.android.com/apk/res/android"
               android:transitionOrdering="together"
               android:duration="400">

    <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>

</transitionSet> 

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

为什么使用OverShootInterpolator?

前面提到,AppStore原生的动画函数曲线是类弹簧的,这与OverShootInterpolator的函数曲线是类似的:
他们都会在到达目标值后,继续向前进一小步,然后再退回来,就像下方的函数曲线一样: image.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。

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

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

需要这份系统化学习资料的朋友,可以戳这里获取

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

程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

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

需要这份系统化学习资料的朋友,可以戳这里获取

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

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值