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,整体结构与预览如下所示:
![]()) ![]()
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]()
### 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]() 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() }