原文地址:http://mikescamell.com/shared-element-transitions-part-4-recyclerview/
在系列的第三节我们看到了如何在使用Picasso或Glide时使用共享元素过渡。
在第四节,我们准备通过使用应用程序中热门的共享元素过渡组件RecyclerView来实现这个效果。Google Play Music是我在第一节中提到过的例子,当然还有很多其他的例子。Pocket Casts便是另外一个不错的例子。
我们会做一个类似的效果,从动物相册进入到一个详细的介绍页面。这便是我们的两个页面:
我会举三个例子。其中两个使用Activity和Fragment来实现从RecyclerView进入简单的详情页。最后一个使用ViewPager中的RecyclerView来跳转。
这里有一个重点我想要先说明,以免你决定跳过本文的其他部分。这就是共享元素过渡需要一个唯一的过渡名称。当在RecyclerView中使用它们时,这一点很容易被遗忘。闲话少说,让我们进入正题。
公共代码
首先让我们看看一些例子中的公共代码。让我们从相册条目的布局开始。
动物相册条目布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/item_animal_square_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
没什么特别的,但是需要注意的关键是ImageView并没有设置transitionName。
动物相册Adapter
public class AnimalGalleryAdapter extends RecyclerView.Adapter<AnimalGalleryAdapter.ImageViewHolder> {
private final AnimalItemClickListener animalItemClickListener;
private ArrayList<AnimalItem> animalItems;
public AnimalGalleryAdapter(ArrayList<AnimalItem> animalItems, AnimalItemClickListener animalItemClickListener) {
this.animalItems = animalItems;
this.animalItemClickListener = animalItemClickListener;
}
....
@Override
public void onBindViewHolder(final ImageViewHolder holder, int position) {
final AnimalItem animalItem = animalItems.get(position);
Picasso.with(holder.itemView.getContext())
.load(animalItem.imageUrl)
.into(holder.animalImageView);
ViewCompat.setTransitionName(holder.animalImageView, animalItem.name);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animalItemClickListener.onAnimalItemClick(holder.getAdapterPosition(), animalItem, holder.animalImageView);
}
});
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animalItemClickListener.onAnimalItemClick(position, animalItem, holder.animalImageView);
}
});
}
}
这是我们的AnimalGalleryAdapter。在所有例子中,它会被用来显示相册图片。
在第六行的构造函数中,我们需要一个AnimalItemClickListener用来处理有关Activity或Fragment启动动物详情页面的回调。
在onBindViewHolder方法中我们会设置transitionName。正如之前提到的,在布局层级中它必须是唯一的,所以我们使用动物的名称对我们来说应该是唯一的。最好的方式是如果你可以使用一些源自你的类型中的唯一标识符。如果我们在XML中设置transitionName,那么所有相册中的ImageView都会有相同的名称,这意味着党我们回调相册时,框架就不知道需要移动那个图片了。这样的结果会是图片移动很怪异。第21行我们通过ViewCompat来给ImageView设置transitionName。
在26行当图片被点击时会触发onAnimalItemClick。我们给它了位置,被点击的AnimalItem,ImageView。正如之前的例子所做的,我们可以使用ViewCompat来调用ImageView的getTransitionName。
动物条目
public class AnimalItem implements Parcelable {
public String name;
public String detail;
public String imageUrl;
...
}
我们的AnimalItem非常简单。我们需要图片的URL来告诉Picasso(可以简单的替换为Glide)加载什么图片到我们的详情Activity或Fragment。因为是Parcelable,所以我们可以通过在Intent中添加额外的Bundle来传递它。
RecyclerView到Activity
这可能是最简单的实现。在我们的RecyclerViewActivity,我们只需要处理onAnimalItemClick并启动AnimalDetailActivity。
public class RecyclerViewActivity extends AppCompatActivity implements AnimalItemClickListener {
//Code to setup the RecyclerView and Adapter
@Override
public void onAnimalItemClick(int pos, AnimalItem animalItem, ImageView sharedImageView) {
Intent intent = new Intent(this, AnimalDetailActivity.class);
intent.putExtra(EXTRA_ANIMAL_ITEM, animalItem);
intent.putExtra(EXTRA_ANIMAL_IMAGE_TRANSITION_NAME, ViewCompat.getTransitionName(sharedImageView));
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
sharedImageView,
ViewCompat.getTransitionName(sharedImageView));
startActivity(intent, options.toBundle());
}
}
第8行和第9行,我们把我们的AnimalItem设置到Bundle中并将其传递到AnimalDetailActivity中。第11至14行,我们做了和第一节一样的事情,通过AnimalGalleryAdapter中传递过来的ImageView和transitionName来创建ActivityOptions。当我们在第16行调用startActivity时,我们添加ActivityOptions
现在我们需要计划AnimalDetailActivity了。
public class AnimalDetailActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_animal_detail);
supportPostponeEnterTransition();
Bundle extras = getIntent().getExtras();
AnimalItem animalItem = extras.getParcelable(RecyclerViewActivity.EXTRA_ANIMAL_ITEM);
ImageView imageView = (ImageView) findViewById(R.id.animal_detail_image_view);
TextView textView = (TextView) findViewById(R.id.animal_detail_text);
textView.setText(animalItem.detail);
String imageUrl = animalItem.imageUrl;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
String imageTransitionName = extras.getString(RecyclerViewActivity.EXTRA_ANIMAL_IMAGE_TRANSITION_NAME);
imageView.setTransitionName(imageTransitionName);
}
Picasso.with(this)
.load(imageUrl)
.noFade()
.into(imageView, new Callback() {
@Override
public void onSuccess() {
supportStartPostponedEnterTransition();
}
@Override
public void onError() {
supportStartPostponedEnterTransition();
}
});
}
}
我们需要在Picasso通过图片链接完成图片加载前停止AnimalDetailActivity的加载。所以第7行我们调用了supportPostponeEnterTransition来告诉它等待。第19行我们从extras中得到了transitionName并将其设置给AnimalDetailActivity的ImageView。这很重要,我们不在AnimalDetailActivity的布局XML中设置transitionName。尽管这不是必需的,如果你想要在布局中设置ImageView的transitionName也是可以的,但是你的确需要在创建RecyclerViewActivity的ActivityOptionsCompat时设置它。第23至36行我们使用Picasso来加载图片并在onSuccess和onError方法中设置回调supportStartPostponedEnterTransition来告诉AnimalDetailActivity它可以启动了。在第三节已经详细讲解过。一切都应该正常运行。
RecyclerView到Fragment
与上面Activity例子中的代码相比,使用Fragment并没有很大的区别。所以我们只需要了解关键的不同点。这里我们的RecyclerViewFragment通过Activity持有的Fragments加载而来。
public class RecyclerViewFragment extends Fragment implements AnimalItemClickListener {
//Code to setup RecyclerView and Adapter
@Override
public void onAnimalItemClick(int pos, AnimalItem animalItem, ImageView sharedImageView, String transitionName) {
Fragment animalDetailFragment = AnimalDetailFragment.newInstance(animalItem, transitionName);
getFragmentManager()
.beginTransaction()
.addSharedElement(sharedImageView, ViewCompat.getTransitionName(sharedImageView))
.addToBackStack(TAG)
.replace(R.id.content, animalDetailFragment)
.commit();
}
}
在onAnimalItemClick方法中我们创建了我们的FragmentTransaction。在第10行我们添加了从AnimalGalleryAdapter传递而来的ImageView。我们可以使用它来获得transitionName。就像Activity例子中的一样,我们需要将我们的AnimalItem和transitionName传递下去,正如我们在第7行创建我们的Fragment中所做的。
public class AnimalDetailFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
postponeEnterTransition();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setSharedElementEnterTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move));
}
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
AnimalItem animalItem = getArguments().getParcelable(EXTRA_ANIMAL_ITEM);
String transitionName = getArguments().getString(EXTRA_TRANSITION_NAME);
TextView detailTextView = (TextView) view.findViewById(R.id.animal_detail_text);
detailTextView.setText(animalItem.detail);
ImageView imageView = (ImageView) view.findViewById(R.id.animal_detail_image_view);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
imageView.setTransitionName(transitionName);
}
Picasso.with(getContext())
.load(animalItem.imageUrl)
.noFade()
.into(imageView, new Callback() {
@Override
public void onSuccess() {
startPostponedEnterTransition();
}
@Override
public void onError() {
startPostponedEnterTransition();
}
});
}
}
又一次和Activity的例子一样,我们需要告诉Fragment来等待加载,直到我们在第7行通过postponeEnterTransition()告诉它何时加载。第9行我们调用了setSharedElementTransition(),否则我们不能如愿,我们仅通过在Android资源文件中告诉它我们需要何种过渡效果来初始化过渡运动。
第25行,我们在ImageView上设置了transitionName。和Activity的例子一样,它不是必须的,我们可以通过XML来设置一个transitionName并在这使用它。我们通过Picasso来加载图片(28-41),并且确认我们在回调的onSuccess() 和OnError()方法中调用startPostponedEnterTransition(),这样确保Fragment实际加载。
真如你所见的,除了Fragment的特殊调用,Activity和Fragment的例子非常的相近。
RecyclerView到ViewPager
上周我被问到了这个场景,所以我决定在这将它抛出来。这仅仅是这个例子的预告。正如你所了解的那样ViewPager的特性是可以滑动改变页面。你可能滑动了ViewPager,不再持有第一次你进行共享元素过渡的View了。如果你这样做了你可能在毗邻的页面,所以当你按返回键时第一张图片过渡回来而不是你希望的。
使用ViewPager几乎和上面Fragment的例子没有什么不同,这是部分片段:
public class AnimalViewPagerFragment extends Fragment {
///Other setup code
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
postponeEnterTransition();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setSharedElementEnterTransition(TransitionInflater.from(getContext()).inflateTransition(android.R.transition.move));
}
setSharedElementReturnTransition(null);
}
}
在第12行,我们调用了setSharedElementReturnTransition()并设置它为空,所以它不会返回共享元素过渡。这样,当你点击返回键时,不会再有奇怪的过渡了。如果你知道解决办法我很乐意倾听,我有几个自己的想法,但我本周没有时间去尝试它们。注意我在将要创建的Fragment中调用它,而不是加载它的那个。
源代码可以在这里的recycler_view下找到。