您的位置:首页 > 移动开发 > Android开发

Android共享动画兼容实现

2017-07-24 20:53 218 查看
生命不息,奋斗不止

前言

看了一下之前的文章记录,最近的文章是在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
以下调用封装的方法。大概步骤如下:

引入依赖

dependencies {
compile 'com.idisfkj.share:sharelibrary:1.0.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();
}
}


镇文之宝





demo地址

后续还会继续持续更新,如果客官们对此还有兴趣的话可以关注我的博客或者Github,谢谢支持。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 动画 界面
相关文章推荐