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 页面共享元素过渡
定义定制动画

利用动画效果实现

  • 效果:
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共享元素转场动画兼容实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容