Android 共享元素效果
- Android 5.0 以上使用 Transition 实现的方法
- Android 5.0 以下的实现方法
Transition
Transition 框架是 Android 4.4 KitKat 中加入的,但在 5.0 才开始被人应用起来,
而且这一部分也涉及了 22.0 的 API,虽然有对应的 support.v4 包,但也还是有点问题。
所以这一部分可以说是 5.0 以上适用的方法。
效果(录制出来的效果有点卡顿):
共享元素效果图- 设置 Activity 引用的 theme
设置windowContentTransitions
为 true,即开启窗口内容过渡
<style name="AppTheme.custom">
<item name="colorControlHighlight">@color/ControlHighlight</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentTransitions">true</item>
</style>
这里遇到一点小问题,即上述 Activity 引用的 style 中不仅设置了 android:windowIsTranslucent
,也设置了 android:windowIsTranslucent
: 让 Activity 的背景为透明,在我测试的时候发现使用共享元素的时候出现了返回时闪屏的现象,解决方法是设置 Activity 背景颜色为透明。
在 onCreate
中:
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActivity().getWindow().setBackgroundDrawableResource(R.color.transparent);
}
或者在上述的 style 中的 theme 添加:
<item name="android:windowBackground">@android:color/transparent</item>
- 设置共享元素
其实实现这样的效果就是把第一个界面的 ImageView 移动、放大到第二个界面的 ImageView 的位置,借助 API 实现效果可以省去自己写动画的逻辑,但就需要让系统关联两个 View。
而关联两个 View 通过设置android:transitionName
属性。
首先在第一个界面的activity_main.xml
:
<LinearLayout>
<ImageView
android:id="@+id/img"
android:transitionName="testImg"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="160dp" />
...
</LinearLayout>
在打开的 Activity 的 xml 中
<LinearLayout>
<ImageView
android:id="@+id/item_img"
android:transitionName="testImg"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="380dp" />
...
</LinearLayout>
对应的 ImageView 中的android:transitionName
属性值必须相同,而对两个控件的大小、id 等属性并无要求。
- 第一个 Activity 中,使用共享元素启动新界面方法
MainActivity:
Intent intent = new Intent(getActivity(),SecondActivity.class);
ActivityOptionsCompat options = ActivityOptionsCompat
.makeSceneTransitionAnimation(getActivity(),
mImgView,"testImg");
startActivity(intent,options.toBundle());
在 makeSceneTransitionAnimation
传入的参数中,mImgView 是第一个界面中 ImageView 的实例,第三个参数对应 xml 中的 android:transitionName
的值。
- 在被打开的 Activity 中
首先加载图片还是要自己写的,其余的需要注意:
返回不再调用finishActivity()
而是supportFinishAfterTransition()
。
@Override
public void onBackPressed() {
supportFinishAfterTransition();
}
因为打开新的 Activity 的时候,可能要去加载新的图片,这时候我们需要延迟过渡动画的开始,直到图片加载完成之后再开始动画。否则会出现各种 bug。
所以要在第二个 Activity 中的 onCreate()
中阻止动画的执行:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 延迟共享动画的执行
postponeEnterTransition();
}
然后在图片加载完成后开始动画:
Glide.with(this)
.load(data.getImage())
.priority(Priority.HIGH)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new GlideDrawableImageViewTarget(mImageView){
@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
super.onResourceReady(resource, animation);
//图片加载完成的回调中,启动过渡动画
supportStartPostponedEnterTransition();
}
});
当然,启动动画不一定要等待图片加载完成再进行,因为还存在着图片加载失败、加载时间过长等问题,这里只是提出一种方法,实际还是自己看情况决定。
以上只是简单的实现了一种效果,关于 Transition 的使用、共享元素在
Fragment 中的使用、多个共享元素的使用等,在这里暂时不打算细讲,可以参考:
使用 Transition FrameWork 实现有意义的转场动画(译)
(译)Android 5.0 页面共享元素过渡
(需科学上网)定义定制动画
利用动画效果实现
- 效果:
- 设置 Activity 背景透明
<style name="AppTheme.custom">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
- 创建一个实体类以存储 ImageView 的位置数据并实现 Parcelable 接口,
位置数据包括控件距离父控件( 屏幕 )左边、顶部的距离和控件的宽高。
public static class ImageBean implements Parcelable {
private String FilePath;
private String FileName;
private Boolean IsSelect;
private int viewLeft;
private int viewTop;
private int viewHeight;
private int viewWidth;
public int getViewLeft() {
return viewLeft;
}
public void setViewLeft(int viewLeft) {
this.viewLeft = viewLeft;
}
public int getViewTop() {
return viewTop;
}
public void setViewTop(int viewTop) {
this.viewTop = viewTop;
}
public int getViewHeight() {
return viewHeight;
}
public void setViewHeight(int viewHeight) {
this.viewHeight = viewHeight;
}
public int getViewWidth() {
return viewWidth;
}
public void setViewWidth(int viewWidth) {
this.viewWidth = viewWidth;
}
public ImageBean(int left,int top,int height,int width){
viewLeft = left;
viewTop = top;
viewHeight = height;
viewWidth = width;
}
public ImageBean(String p, String f){
FilePath = p;
FileName = f;
IsSelect = false;
}
public String getFilePath() {
return FilePath;
}
public void setFilePath(String filePath) {
FilePath = filePath;
}
public String getFileName() {
return FileName;
}
public void setFileName(String fileName) {
FileName = fileName;
}
public Boolean getSelect() {
return IsSelect;
}
public void setSelect(Boolean select) {
IsSelect = select;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.FilePath);
dest.writeString(this.FileName);
dest.writeValue(this.IsSelect);
dest.writeInt(this.viewLeft);
dest.writeInt(this.viewTop);
dest.writeInt(this.viewHeight);
dest.writeInt(this.viewWidth);
}
protected ImageBean(Parcel in) {
this.FilePath = in.readString();
this.FileName = in.readString();
this.IsSelect = (Boolean) in.readValue(Boolean.class.getClassLoader());
this.viewLeft = in.readInt();
this.viewTop = in.readInt();
this.viewHeight = in.readInt();
this.viewWidth = in.readInt();
}
public static final Parcelable.Creator<ImageBean> CREATOR = new Parcelable.Creator<ImageBean>() {
@Override
public ImageBean createFromParcel(Parcel source) {
return new ImageBean(source);
}
@Override
public ImageBean[] newArray(int size) {
return new ImageBean[size];
}
};
}
- 在打开新的 Activity 前,获取点击的 ImageView 的位置数据
private ImageBean img2Location(ImageView imageView,String path){
int[] location = new int[2];
imageView.getLocationOnScreen(location);
ImageBean bean = new ImageBean(
location[0],location[1],
imageView.getHeight(),imageView.getWidth());
bean.setFilePath(path);
return bean;
}
- 通过 Intent 将存有位置数据的 ImageBean 实例传到新的 Activity
String path = "..";
ImageView imageView = ...;
Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("location",img2Location(imageView,path));
startActivity(intent);
- 在打开的 Activity 中,通过传过来的 path 数据加载图片,
同时获取传过来的位置数据。
public void initView(@Nullable final View view) {
getActivity().setTheme(R.style.translucent);
Glide.with(getActivity()).load(bean.getFilePath())
.placeholder(R.drawable.black_place_holder).into(mPhotoView);
//获取位置数据
Intent intent = getIntent();
ImageBean bean = intent.getParcelableExtra("image");
int mOriginLeft = bean.getViewLeft();
int mOriginTop = bean.getViewTop();
int mOriginHeight = bean.getViewHeight();
int mOriginWidth = bean.getViewWidth();
}
- 监听返回按钮,点击时开始移动、缩放的动画,
动画用 ValueAnimator 和 ScaleAnimation 实现。
private void finishPhotoViewWithTap(int left,int top){
//动画的执行时间
int ANIMATOR_DURATION = 500;
//控制 x 轴上的移动,将 ImageView 移动到对应的位置
ValueAnimator translateXAnimator = ValueAnimator.ofFloat(0, left);
translateXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//用 setX 移动控件
mPhotoView.setX((Float) valueAnimator.getAnimatedValue());
}
});
translateXAnimator.setDuration(ANIMATOR_DURATION);
translateXAnimator.start();
///控制 y 轴上的移动,并监听动画的结束,动画结束时关闭该 Activity
ValueAnimator translateYAnimator = ValueAnimator.ofFloat(0, top);
translateYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPhotoView.setY((Float) valueAnimator.getAnimatedValue());
}
});
translateYAnimator.setDuration(ANIMATOR_DURATION);
translateYAnimator.start();
translateXAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//动画结束的回调
animation.removeAllListeners();
finishActivity();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
//控制缩放的 Animation
Animation scaleAnimation = new ScaleAnimation(1.0f,0.2f,1.0f,0.2f
,left,top);
//控制透明度的 Animation
Animation alphaAnimation = new AlphaAnimation(1.0f,0f);
//用 AnimationSet 将以上两个 Animation 结合到一起
AnimationSet set = new AnimationSet(true);
set.setDuration(ANIMATOR_DURATION);
set.setFillAfter(true);
set.addAnimation(scaleAnimation);
set.addAnimation(alphaAnimation);
mPhotoView.startAnimation(set);
}
原理大概就是如此,实现动画的方法有很多种。
可以参考:
Activity 共享元素转场动画实践
Android共享元素转场动画兼容实践