过渡效果在Material Design app中提供体觉上的连续。用户操作app的同时,app中views的状态改变。动画和过渡强化了界面是真实的,连接着普通的元素从一个view到另外一个view。
本文旨在提供一个安卓fragment之间的特殊的连续过渡的方案和实现。我们会展示如何实现一个RecyclerView中的图片到ViewPager中的图片的过渡并且返回,用“Shared Elements”来决定哪些view参与到过渡效果和如何过渡。我们也会处理比较棘手(蛋疼)的情况-切换到一个别的item后,返回过渡时根据当前的item关联RecyclerView。
效果如下:Demo地址
什么是“Shared Elements”?
一个共享元素过渡决定了在两个fragment中显示的view之间如何过渡。比如,Fragment A和Fragment B都有一张图片在ImageView中显示,当B显示时,图片从A过渡到B。
有当大量的之前发表过的文章解释共享元素的原理和怎样实现一个基本的Fragment过渡效果。so 本文将忽略大部分基础,并简化怎样创建一个可用的进出ViewPager的过渡的细节。然而,如果你想学习更多的过渡效果,我推荐你参考Android's developers website ,花点时间看看2016 Google I/O presentation。
难点
1,共享元素映射
我们希望支持一个无缝的前进后退过渡,包括从grid到pager的过渡,然后返回到相关的图片上,即使用户翻到其他图片上了。
为此,我们需要找到一个方法来动态映射共享元素来告知Android的过渡系统他需要什么来实现这个过渡。
2,延迟加载
共享元素过渡很强大,但是可能当处理让他们实现过渡之前需要加载出来的元素是变得很棘手。过渡效果在fragment中的view没有加载出来时可能不能达到期望的效果。
本项目中有两个地方存在加载时间影响共享元素过渡:
(1).viewpager需要几毫秒来加载其内部的fragment。另外,加载图片到pager fragment中也需要花费时间(甚至可能包含从asset中加载的时间)
(2).RecyclerView也需要花费时间来加载图片。
Demo app的设计
1.基本结构
在我们进入生动的过渡效果之前,先讲讲demo app的结构。
MainActivity 加载 GridFragment来展示一个RecyclerView实现的图片列表。RecyclerView adapter加载image项(一个在ImageData类中定义的静态数组),并且通过把GridFragment替换成ImagePagerFragment的来处理点击事件
当翻页时,ImagePagerFragment adapter加载嵌套的ImageFragments来展示独立的图片
注意:demo通过Glide来异步加载图片。demo中的图片是本地的。然而你可以轻松的把它转换成网络图片路径
2.整合一个选中的/显示的位置
为了实现选中的图片位置在fragment之间的通信,我们用MainActivity保存。
当一个item被点击或翻页后,MainActivity更新相关的item的位置。
保存的位置在后面的几个地方被用到:
(1).当确定哪一页要显示在viewPager中
(2).当返回到grid并且自动滚动到当前位置来保证他的显示。
(3).当然,实现过渡的回调在下一节中讲解。
设置过渡
上文中已经提到,我们需要找到一个方法来动态测量共享元素来告知Android的过渡系统他需要什么来实现这个过渡。
当我们处理同一个layout中包含任意数量的view的情况时,通过在xml中给imageView设置过渡的Name属性将不会生效
为了实现这个,我们将用过渡系统提供我们的一些方法。
1,通过调用setTransitionName来给imageView设置过渡name,这会给imageView添加一个过渡效果的唯一标识。
setTransitionName在RecyclerView的adapter绑定view和ImageFragment的onCreateView中被调用。两个地方都唯一的图片资源作为名字来标识view
2,设置SharedElementCallbacks来拦截onMapSharedElements,并且调整共享元素名对view的映射。这个将在从GridFragment进入ImagePagerFragment时被执行
设置FragmentManager的过渡效果
给fragment replacement初始化过渡效果首先要做的是FragmentManager过渡准备,我们要告知系统我们有一个共享元素过渡。
fragment.getFragmentManager()
.beginTransaction()
.setReorderingAllowed(true) // setAllowOptimization before 26.1.0
.addSharedElement(imageView, imageView.getTransitionName())
.replace(R.id.fragment_container,
new ImagePagerFragment(),
ImagePagerFragment.class.getSimpleName())
.addToBackStack(null)
.commit();
setReorderingAllowed被设为true,它会重置fragment的状态改变来适配更好的共享元素过渡。新添加的fragment会在被替换的fragment调用onDestory()之前调用onCreate(Bundle),从而使共享view在过渡动画开始前被创建和摆放。
图片过渡效果
为了定义图片移动到新位置时如何过渡,我们在xml中设置TransitionSet并且在ImagePagerFragment中加载。
<ImagePagerFragment.java>
Transition transition =
TransitionInflater.from(getContext())
.inflateTransition(R.transition.image_shared_element_transition);
setSharedElementEnterTransition(transition);
<image_shared_element_transition.xml>
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<changeClipBounds/>
<changeTransform/>
<changeBounds/>
</transitionSet>
调整共享元素的映射
当离开GridFragment时我们先调整共享元素的。为此,我们要调用setExitSharedElementCallback()并给他提供一个SharedElementCallback,这个回调会映射元素名到我们想要加入过渡的view。
要注意的一点是当fragment事务发生和fragment从后来重新进入时,这个callback在退出fragment时被调用。我们利用这个特点来重新映射共享View并调整过渡来处理view在翻页后被改变的情况。
对于这个情况,我们只需要关注一个ImageView从grid到viewpager中的fragment的过渡,所以映射只需要在onMapSharedElements回调接受的第一个元素来调整。
<GridFragment.java>
setExitSharedElementCallback(
new SharedElementCallback() {
@Override
public void onMapSharedElements(
List<String> names, Map<String, View> sharedElements) {
// Locate the ViewHolder for the clicked position.
RecyclerView.ViewHolder selectedViewHolder = recyclerView
.findViewHolderForAdapterPosition(MainActivity.currentPosition);
if (selectedViewHolder == null || selectedViewHolder.itemView == null) {
return;
}
// Map the first shared element name to the child ImageView.
sharedElements
.put(names.get(0),
selectedViewHolder.itemView.findViewById(R.id.card_image));
}
});
我们也需要在进入ImagePagerFragment时调整共享元素的映射。对此,我们要调用setEnterSharedElementCallback()
<ImagePagerFragment.java>
setEnterSharedElementCallback(
new SharedElementCallback() {
@Override
public void onMapSharedElements(
List<String> names, Map<String, View> sharedElements) {
// Locate the image view at the primary fragment (the ImageFragment
// that is currently visible). To locate the fragment, call
// instantiateItem with the selection position.
// At this stage, the method will simply return the fragment at the
// position and will not create a new one.
Fragment currentFragment = (Fragment) viewPager.getAdapter()
.instantiateItem(viewPager, MainActivity.currentPosition);
View view = currentFragment.getView();
if (view == null) {
return;
}
// Map the first shared element name to the child ImageView.
sharedElements.put(names.get(0), view.findViewById(R.id.image));
}
});
延迟过渡
我们要过渡的图片在grid和pager中被加载,并且加载需要耗费时间。为了让他正常执行,我们需要延迟过渡直到需要参与的view加载完成。
因此,我们在fragment的onCreateView()中调用postponeEnterTransition() ,一旦image被加载了,我们通过调用startPostponedEnterTransition()来开始过渡。
注意:延迟在grid和viewpager中的fragment都被调用,从而满足了app前进后退的跳转。
我们用Glide加载图片时设置监听,当图片加载完成后触发进入的过渡。需要做以下两个操作:
1,当一个ImageFragment 中的图片被加载了,在他的父类ImagePagerFragment 中调用他来开始过渡。
2,当返回到grid时,当前选中的image被加载后过渡被调用。
注意:postponeEnterTransition在ImagePagerFragment中实现,然而startPostponeEnterTransition在pager中创建的子类ImageFragment中被调用
<ImageFragment.java>
Glide.with(this)
.load(arguments.getInt(KEY_IMAGE_RES)) // Load the image resource
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource) {
getParentFragment().startPostponedEnterTransition();
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
getParentFragment().startPostponedEnterTransition();
return false;
}
})
.into((ImageView) view.findViewById(R.id.image));
你可能已经注意到了,在加载失败时我们也会调用开始延迟过渡,这可以避免UI一直停留在失败结果。
Final Touch
为了使过渡更加平滑,我们要在imge过渡到pager时让grid渐淡消失。为此,我们创建一个TransitionSet作为GridFragment的退出动画。
<GridFragment.java>
setExitTransition(TransitionInflater.from(getContext())
.inflateTransition(R.transition.grid_exit_transition));
<grid_exit_transition.xml>
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:startDelay="25">
<fade>
<targets android:targetId="@id/card_view"/>
</fade>
</transitionSet>
这就是设置了退出动画的过渡效果:
你可能已经注意到了,到此过渡效果依然不是非常完美。grid的card view包括持有过渡到pager的image都有渐淡效果。
为了解决这个问题,我们需要在向GridAdapter提交fragment过渡效果的时候排除点击的item。
// The 'view' is the card view that was clicked to initiate the transition.
((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true);
此时,动画看起来更好了(其他card渐淡时,被点击的card不会渐淡)
As a final touch,当我们在pager翻到某一个图片退出时,我们需要让GridFragment滚动到当前的图片的位置。(在onViewCreated中)
<GridFragment.java>
recyclerView.addOnLayoutChangeListener(
new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
recyclerView.removeOnLayoutChangeListener(this);
final RecyclerView.LayoutManager layoutManager =
recyclerView.getLayoutManager();
View viewAtPosition =
layoutManager.findViewByPosition(MainActivity.currentPosition);
// Scroll to position if the view for the current position is null (not
// currently part of layout manager children), or it's not completely
// visible.
if (viewAtPosition == null
|| layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){
recyclerView.post(()
-> layoutManager.scrollToPosition(MainActivity.currentPosition));
}
}
});
总结(Wrapping up)
本文中我们实现了一个从RecyclerView到ViewPager和返回的平滑的过渡。
我们讲解了怎样延迟过渡然后等view加载完成后开始过渡。我们也实现了共享元素动态改变时重映射来得到过渡效果。这给用户带来了更好的体验。
6
7
原文:https://chinagdg.org/2018/02/continuous-shared-element-transitions-recyclerview-to-viewpager/
https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+blogspot/hsDu+(Android+Developers+Blog)