盒子
盒子
文章目录
  1. 前言
  2. 实现方案
    1. Android 5.0 及其以上的实现
      1. 建立契约
      2. 调用ActivityOptionsCompat
      3. finishAfterTransition
    2. 兼容全版本实现
      1. 原理
      2. 获取控件的相关参数
        1. Matrix
        2. getLocationOnScreen
        3. 平移偏移量
      3. 进入动画
      4. 退出动画
      5. 界面过度动画
  3. 收割
    1. 引入依赖
    2. 执行界面
    3. 响应界面
  4. 镇文之宝

Android共享动画兼容实现

生命不息,奋斗不止

前言

看了一下之前的文章记录,最近的文章是在3月12日写的,今天的7月16日。不知不觉已经4个月没有坐在电脑前认真的思考与静下心来做些总结。趁着刚刚王者荣耀超神的兴奋热度,接下来说说我对Android共享动画方面的一些心得。

实现方案

这里我姑且都认为大家都对共享动画的效果有所了解,简单的说就是从一个界面平移缩放过度到另一个界面。在实现方面上针对不同Android系统版本,有不同的做法。对于Android 5.0(LOLLIPOP API 21)以上的系统,实现起来相对来说方便了许多,只需做一些契约与调用系统的API即可。但是市场上对于Android 5.0以下的机型还是存在的,我们并不能忽略它们,所以为了更好的兼容上下版本的机型,同时以为了让用户体验一致,我们必须自己动手实现共享动画的需求。

Android 5.0 及其以上的实现

为了满足部分只考虑Android 5.0以上实现的朋友,我这里也对系统的调用方法进行简单的示例说明。我这边总结了一下,主要分为三步。

建立契约

要想在第一个界面点击控件共享跳转到另一个界面的对于控件上,需要将这两个共享的控件进行绑定,即要让系统能够找到对应生效的控件。而为了达到这种效果, 系统给我们提供了一个方法

1
public final void setTransitionName(String transitionName)

这是View中的方法,就一个参数,该参数就是一个字符串类型的契约名称。即在两个界面上对需要进行共享的两个控件进行相同名称的设定。

1
2
public static final String TRANSITION_NAME_SHARE = "share";
imageView.setTransitionName(TRANSITION_NAME_SHARE);

以上是在代码中动态设置,在xml文件中也能设置

1
android:transitionName="share"

唯一要注意的就是名称必须相同

调用ActivityOptionsCompat

上面建立的契约,就可以直接进入主题–开启共享动画。在进行界面的跳转,给平常的用发一样,创建Intent,调用startActivity方法。只不过在调用startActivity时要在传个Bundle参数。该参数需要通过ActivityOptionsCompat获取。

1
ActivityOptionsCompat compat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, imageView, TRANSITION_NAME_SHARE);

说下参数,第一个Activity,第二个需要共享的View,第三个就是契约名称。最后开启跳转时传入。

1
startActivity(intent, compat.toBundle());

finishAfterTransition

使用上面的代码就能看到跳转的开启共享动画了,当然前提是在Andorid 5.0及其以上的手机上。上面只完成了开启,对于退出,实现也很简单,只需在退出的时候调用如下代码即可

1
finishAfterTransition();

这是Activity的方法。所以可以直接在退出界面中调用。建议可以重写onBackPressed方法,在其中进行调用。

再回顾一下上面的代码,也就10行代码以内。所以对于只支持高版本的系统的朋友来说,真是爽歪歪。无图无真相,客官请看图。

效果图


兼容全版本实现

我相信一直读到这里的客官心理都是很愉悦与轻松的,下面我需要提醒客官们,应该提起几分注意了来看下面的精彩内容。

原理

基于上面的实现,我们再来看下上面的效果图,所谓一图胜千言,我们一起来结合效果图来分析实现原理。首先,我们通过效果图能够看到两个明显的效果:

  • 界面背景是透明渐变的方式过度到另一个界面的。
  • 控件是从第一个界面原地放大平移到第二个界面的控件位置上。

从上面的要点来看,对控件的动画实现是重中之重。具体的实现过程是:将第二个界面透明启动,同时将第二个界面的控件缩放平移到第一个界面的控件位置上,然后再进行放大平移到第二个界面原始的位置上。这样就实现了高版本的共享动画的效果。要想达到放大平移动画的准确进行,自然要得到相应的控件参数信息。所以我们在实现控件的放大动画,这里必须要得到两个界面的控件的宽高与控件内图片的宽高。再计算出需要缩放的比例。

请注意,这里我是对图片控件进行共享动画,如果是简单的TextView之类的控件就只需获取控件的宽高,相信客官们看了下面的实现方案也能迅速应对其它控件的类型。

有的客官可能会有所疑问,为何要获取图片的宽高呢,图片的宽高不就等于控件的宽高吗?是的,对于绝大多数情况来说确实是如此,但有的时候控件的宽高并不一定等于图片的宽高,例如大图浏览模式下的图片。如果此时使用控件的宽高来计算缩放比例,自然得不到预想的效果,图片缩放的效果必然会不准确。其实本质是我们要脱离控件,关注本质---图片效果

说了这么多,客官们可能有点不耐烦了,开始show me the code

获取控件的相关参数

控件的宽高获取,这里就不多说了。我们主要来思考图片在控件中显示的真实宽高。看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void convertOriginalInfo(ImageView oriView) {
if (oriView == null || oriView.getDrawable() == null) {
throw new NullPointerException("original ImageView or ImageView drawable must not null");
}
//get original ImageView info
oriView.getImageMatrix().getValues(mOriginalValues);
Rect oriRect = oriView.getDrawable().getBounds();
mOriginalWidth = (int) (oriRect.width() * mOriginalValues[Matrix.MSCALE_X]);
mOriginalHeight = (int) (oriRect.height() * mOriginalValues[Matrix.MSCALE_Y]);
mOriginalViewWidth = oriView.getWidth();
mOriginalViewHeight = oriView.getHeight();
oriView.getLocationOnScreen(mOriginalLocation);
}

Matrix

这里有一个知识点,每一张图片都有对应的一个Matrix,它代表的是一个3*3的矩阵,其中包含了图片的相关信息,例如缩放,平移。

这里是ImageVIew中的ImageMatrix而不是View中的Matrix,具体的Matrix信息可以自行google

通过MatrixgetValues方法将3*3的矩形值转化成一个大小为9的float型数组mOriginalValues。这样我们使用Matrix.MSCALE_XMatrix.MSCALE_Y分别获取图片的xy方向的缩放比例。再通过ImageViewgetDrawable.getBounds方法获取图片原始相关信息。最后乘以比例系数,获取到我们所要的结果。

1
2
3
//calculator scale
mScaleX = (float) mOriginalWidth / mTargetWidth;
mScaleY = (float) mOriginalHeight / mTargetHeight;

既然说到Matrix,就再简单说下它的两个值,Matrix.MTRANS_XMatrix.MTRANS_Y分别代表图片平移的大小。类似与微信朋友圈中的大图浏览的下滑平移缩放退出效果,可以通过这两个值来获取图片在缩放过程中的平移量。

getLocationOnScreen

该方法能够直接获取到控件左上角在屏幕上的坐标位置。最终返回一个大小为2的数组。有个该方法我们就能方便的获取控件的中心坐标。

1
2
3
//calculator pivot position
mPivotX = mTargetLocation[0] + mTargetValues[Matrix.MTRANS_X] + mTargetWidth / 2;
mPivotY = mTargetLocation[1] + mTargetValues[Matrix.MTRANS_Y] + mTargetHeight / 2;

其中mTargetLocation[0]代表控件的在屏幕上的x坐标位置,mTargetLocation[1]代表控件在屏幕上的y坐标位置。

后续进行缩放平移动画需要确定中心位置,由于要达到对图片进行缩放平移的效果,所以要得到图片的确切中心位置,默认为控件的中心

平移偏移量

1
2
3
4
mCenterOffsetX = (int) (mOriginalLocation[0] + mOriginalValues[Matrix.MTRANS_X] + mOriginalViewWidth / 2
- mTargetLocation[0] - mTargetValues[Matrix.MTRANS_X] - mTargetViewWidth / 2);
mCenterOffsetY = (int) (mOriginalLocation[1] + mOriginalValues[Matrix.MTRANS_Y] + mOriginalViewHeight / 2
- CommonUtils.getStatusBarHeight(context) - mTargetLocation[1] - mTargetValues[Matrix.MTRANS_Y] - mTargetViewHeight / 2);

经过上面的解释说明,客官们对平移量的计算应该不难理解。核心是对中心位置进行偏移量计算。

进入动画

首先要确认控件动画的调用时机,必须要在控件绘制的时候进行调用,只有这样才能最早的获取控件的相关信息,为动画进行准备。我们可以采用注册addOnPreDrawListener进行监听控件的绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public FKJShareElement convert(final ImageView tarView) {
if (mInfo == null) {
throw new NullPointerException("ShareElementInfo must not null");
}
tarView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
tarView.getViewTreeObserver().removeOnPreDrawListener(this);
mInfo.convertTargetInfo(tarView, mContext);
//init
if (mEnter) {
tarView.setPivotX(mInfo.getPivotX());
tarView.setPivotY(mInfo.getPivotY());
tarView.setTranslationX(mInfo.getCenterOffsetX());
tarView.setTranslationY(mInfo.getCenterOffsetY());
tarView.setScaleX(mInfo.getScaleX());
tarView.setScaleY(mInfo.getScaleY());
mAnimator = tarView.animate();
start();
startBackgroundAlphaAnimation(mBgView, new ColorDrawable(ContextCompat.getColor(mContext, R.color.fkj_white)));
}
return true;
}
});
return this;
}

对于进入动画,在之前的原理分析中已经指出,要先将第二个界面的控件缩放到第一个界面的位置上。所以我们直接先对控件进行缩放平移,使用ViewsetTranslationX等方法。方法中的mInfo保存了上面获取的图片相关信息。真正的动画执行是在start中进行调用。目的是执行控件的还原动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void start() {
mAnimator.setDuration(mDuration)
.scaleX(1.0f)
.scaleY(1.0f)
.translationX(0)
.translationY(0);
if (mListener != null) {
mAnimator.setListener(mListener);
}
if (mInterpolator != null) {
mAnimator.setInterpolator(mInterpolator);
}
mAnimator.start();
}

退出动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void startExitAnimator() {
mEnter = false;
mAnimator.setDuration(mDuration)
.scaleX(mInfo.getScaleX())
.scaleY(mInfo.getScaleY())
.translationX(mInfo.getCenterOffsetX())
.translationY(mInfo.getCenterOffsetY());
if (mListener != null) {
mAnimator.setListener(mListener);
}
if (mInterpolator != null) {
mAnimator.setInterpolator(mInterpolator);
}
mAnimator.start();
startBackgroundAlphaAnimation(mBgView, new ColorDrawable(ContextCompat.getColor(mContext, R.color.fkj_white)), 255, 0);
}

退出动画就相对简单一点,只需将第二个界面的控件缩放平移到第一个界面控件的位置上即可。

界面过度动画

在进入与退出动画中都调用了startBackgroundAlphaAnimation方法,该方法的作用就是对界面进行透明渐变。原理也简单,我们只需对第二个界面的背景View进行背景渐变,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void startBackgroundAlphaAnimation(final View bgView, final ColorDrawable colorDrawable, int... value) {
if (bgView == null)
return;
if (value == null || value.length == 0) {
value = new int[]{0, 255};
}
ObjectAnimator animator = ObjectAnimator.ofInt(colorDrawable, "alpha", value);
animator.setDuration(mDuration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
bgView.setBackground(colorDrawable);
}
});
animator.start();
}

以上只是一个简单的透明动画的调用,不过直接这样调用你会发现效果不对,因为你还需要将Activity的theme设置为透明效果。只需将android:windowBackgroun设置为透明即可

收割

不知道坚持看到这里的客官有多少,先在这里谢谢客官们的支持。最后将两种实现方式结合一起灵活的调用,在Android 5.0以上调用系统方法,Android 5.0以下调用封装的方法。大概步骤如下:

引入依赖

1
2
3
dependencies {
compile 'com.idisfkj.share:sharelibrary:1.0.0'
}

执行界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, ShareElementActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
imageView.setTransitionName(TRANSITION_NAME_SHARE);
ActivityOptionsCompat compat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, imageView, TRANSITION_NAME_SHARE);
startActivity(intent, compat.toBundle());
} else {
ShareElementInfo info = new ShareElementInfo();
info.convertOriginalInfo(imageView);
intent.putExtra(EXTRA_SHARE_ELEMENT_INFO, info);
startActivity(intent);
overridePendingTransition(0, 0);
}
}
});

响应界面

1
2
3
4
5
6
7
8
9
10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mImageView.setTransitionName(MainActivity.TRANSITION_NAME_SHARE);
} else {
ShareElementInfo info = getIntent().getExtras().getParcelable(MainActivity.EXTRA_SHARE_ELEMENT_INFO);
mShareElement = new FKJShareElement(info, this, mImageView.getRootView());
mShareElement.convert(mImageView)
.setDuration(ANIMATOR_DURATION)
.setInterpolator(new LinearInterpolator())
.startEnterAnimator();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onBackPressed() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
finishAfterTransition();
} else {
mShareElement.convert(mImageView)
.setDuration(ANIMATOR_DURATION)
.setInterpolator(new LinearInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finish();
overridePendingTransition(0, 0);
}
})
.startExitAnimator();
}
}

镇文之宝

效果图
效果图

demo地址

后续还会继续持续更新,如果客官们对此还有兴趣的话可以关注我的博客或者Github,谢谢支持。

支持一下
赞赏是一门艺术