生命不息,奋斗不止

前言

看了一下之前的文章记录,最近的文章是在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

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

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

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

getLocationOnScreen

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

//calculator pivot positionmPivotX = 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();        }    }

镇文之宝

demo地址
Github地址
博客地址

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

关注

更多相关文章

  1. Android(安卓)自定义View——自定义点击事件
  2. 百度地图开发Marker|Polyline隐藏或显示
  3. Android控件开发之四----ListView(4)
  4. android用户界面之ScrollView教程实例汇总
  5. Android(安卓)图像处理(类型转换,比例缩放,倒影,圆角)
  6. android图片的旋转和缩放
  7. Android(安卓)ViewPager与子控件点击事件冲突的解决方案
  8. 断网使用RycyclerView的jar包,运行时报错
  9. Android(安卓)Material design设计风格

随机推荐

  1. 【视频版】最新版PyCharm 2021.3.3 激活
  2. JS框架 -(一)fetch async await 模块 npm
  3. 实现鼠标悬停时自动停止播放离开时又自动
  4. vue常用术语、样式与事件绑定 和 列表渲
  5. 1. 实例演示fetch api, async,await的使
  6. 从淘宝进货卖到亚马逊,能做到利润可观吗?
  7. 【视频版】Goland 2022.1 最新正版激活方
  8. 【视频版】phpstorm 2021.3.3 最新正版激
  9. android有用的知识
  10. Android中使用httpclient等小结