1

生命不息,奋斗不止

前言

看了一下之前的文章记录,最近的文章是在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行代码以内。所以对于只支持高版本的系统的朋友来说,真是爽歪歪。无图无真相,客官请看图。

clipboard.png


兼容全版本实现

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

原理

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

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

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

请注意,这里我是对图片控件进行共享动画,如果是简单的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

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

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

既然说到Matrix,就再简单说下它的两个值,Matrix.MTRANS_XMatrix.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;
    }

对于进入动画,在之前的原理分析中已经指出,要先将第二个界面的控件缩放到第一个界面的位置上。所以我们直接先对控件进行缩放平移,使用ViewsetTranslationX等方法。方法中的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();
        }
    }

镇文之宝

clipboard.png

clipboard.png

demo地址
Github地址
博客地址

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

关注

clipboard.png


午后一小憩
2.9k 声望838 粉丝