生命不息,奋斗不止
前言
看了一下之前的文章记录,最近的文章是在3月12日写的,今天的7月16日。不知不觉已经4个月没有坐在电脑前认真的思考与静下心来做些总结。趁着刚刚王者荣耀超神的兴奋热度,接下来说说我对Android
共享动画方面的一些心得。
实现方案
这里我姑且都认为大家都对共享动画的效果有所了解,简单的说就是从一个界面平移缩放过度到另一个界面。在实现方面上针对不同Android
系统版本,有不同的做法。对于Android 5.0(LOLLIPOP API 21)
以上的系统,实现起来相对来说方便了许多,只需做一些契约与调用系统的API
即可。但是市场上对于Android 5.0
以下的机型还是存在的,我们并不能忽略它们,所以为了更好的兼容上下版本的机型,同时以为了让用户体验一致,我们必须自己动手实现共享动画的需求。
Android 5.0 及其以上的实现
为了满足部分只考虑Android 5.0
以上实现的朋友,我这里也对系统的调用方法进行简单的示例说明。我这边总结了一下,主要分为三步。
建立契约
要想在第一个界面点击控件共享跳转到另一个界面的对于控件上,需要将这两个共享的控件进行绑定,即要让系统能够找到对应生效的控件。而为了达到这种效果, 系统给我们提供了一个方法
public final void setTransitionName(String transitionName)
这是View
中的方法,就一个参数,该参数就是一个字符串类型的契约名称。即在两个界面上对需要进行共享的两个控件进行相同名称的设定。
public static final String TRANSITION_NAME_SHARE = "share";
imageView.setTransitionName(TRANSITION_NAME_SHARE);
以上是在代码中动态设置,在xml
文件中也能设置
android:transitionName="share"
唯一要注意的就是名称必须相同
调用ActivityOptionsCompat
上面建立的契约,就可以直接进入主题--开启共享动画。在进行界面的跳转,给平常的用发一样,创建Intent
,调用startActivity
方法。只不过在调用startActivity
时要在传个Bundle
参数。该参数需要通过ActivityOptionsCompat
获取。
ActivityOptionsCompat compat = ActivityOptionsCompat.makeSceneTransitionAnimation(MainActivity.this, imageView, TRANSITION_NAME_SHARE);
说下参数,第一个Activity
,第二个需要共享的View
,第三个就是契约名称。最后开启跳转时传入。
startActivity(intent, compat.toBundle());
finishAfterTransition
使用上面的代码就能看到跳转的开启共享动画了,当然前提是在Andorid 5.0
及其以上的手机上。上面只完成了开启,对于退出,实现也很简单,只需在退出的时候调用如下代码即可
finishAfterTransition();
这是Activity
的方法。所以可以直接在退出界面中调用。建议可以重写onBackPressed
方法,在其中进行调用。
再回顾一下上面的代码,也就10行代码以内。所以对于只支持高版本的系统的朋友来说,真是爽歪歪。无图无真相,客官请看图。
兼容全版本实现
我相信一直读到这里的客官心理都是很愉悦与轻松的,下面我需要提醒客官们,应该提起几分注意了来看下面的精彩内容。
原理
基于上面的实现,我们再来看下上面的效果图,所谓一图胜千言,我们一起来结合效果图来分析实现原理。首先,我们通过效果图能够看到两个明显的效果:
- 界面背景是透明渐变的方式过度到另一个界面的。
- 控件是从第一个界面原地放大平移到第二个界面的控件位置上。
从上面的要点来看,对控件的动画实现是重中之重。具体的实现过程是:将第二个界面透明启动,同时将第二个界面的控件缩放平移到第一个界面的控件位置上,然后再进行放大平移到第二个界面原始的位置上。
这样就实现了高版本的共享动画的效果。要想达到放大平移动画的准确进行,自然要得到相应的控件参数信息。所以我们在实现控件的放大动画,这里必须要得到两个界面的控件的宽高与控件内图片的宽高。再计算出需要缩放的比例。
请注意,这里我是对图片控件进行共享动画,如果是简单的TextView之类的控件就只需获取控件的宽高,相信客官们看了下面的实现方案也能迅速应对其它控件的类型。
有的客官可能会有所疑问,为何要获取图片的宽高呢,图片的宽高不就等于控件的宽高吗?
是的,对于绝大多数情况来说确实是如此,但有的时候控件的宽高并不一定等于图片的宽高,例如大图浏览模式下的图片。如果此时使用控件的宽高来计算缩放比例,自然得不到预想的效果,图片缩放的效果必然会不准确。其实本质是我们要脱离控件,关注本质---图片效果
。
说了这么多,客官们可能有点不耐烦了,开始show me the code
。
获取控件的相关参数
控件的宽高获取,这里就不多说了。我们主要来思考图片在控件中显示的真实宽高。看下面代码:
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
通过Matrix
的getValues
方法将3*3
的矩形值转化成一个大小为9的float
型数组mOriginalValues
。这样我们使用Matrix.MSCALE_X
与Matrix.MSCALE_Y
分别获取图片的x
与y
方向的缩放比例。再通过ImageView
的getDrawable.getBounds
方法获取图片原始相关信息。最后乘以比例系数,获取到我们所要的结果。
//calculator scale
mScaleX = (float) mOriginalWidth / mTargetWidth;
mScaleY = (float) mOriginalHeight / mTargetHeight;
既然说到Matrix
,就再简单说下它的两个值,Matrix.MTRANS_X
与Matrix.MTRANS_Y
分别代表图片平移的大小。类似与微信朋友圈中的大图浏览的下滑平移缩放退出效果,可以通过这两个值来获取图片在缩放过程中的平移量。
getLocationOnScreen
该方法能够直接获取到控件左上角在屏幕上的坐标位置。最终返回一个大小为2的数组。有个该方法我们就能方便的获取控件的中心坐标。
//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
坐标位置。
后续进行缩放平移动画需要确定中心位置,由于要达到对图片进行缩放平移的效果,所以要得到图片的确切中心位置,默认为控件的中心
平移偏移量
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
进行监听控件的绘制。
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;
}
对于进入动画,在之前的原理分析中已经指出,要先将第二个界面的控件缩放到第一个界面的位置上。所以我们直接先对控件进行缩放平移,使用View
的setTranslationX
等方法。方法中的mInfo
保存了上面获取的图片相关信息。真正的动画执行是在start
中进行调用。目的是执行控件的还原动画。
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();
}
退出动画
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
进行背景渐变,具体实现如下:
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
以下调用封装的方法。大概步骤如下:
执行界面
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);
}
}
});
响应界面
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();
}
@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();
}
}
镇文之宝
后续还会继续持续更新,如果客官们对此还有兴趣的话可以关注我的博客或者Github,谢谢支持。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。