Detailed explanation of Android application theme switching

640?wx_fmt=png&wxfrom=5&wx_lazy=1


Technology News Today


Yesterday, Wuhan University, the alma mater of Xiaomi Chairman and CEO Lei Jun, held a new product launch conference and officially launched the Xiaomi Mi 6X. The machine is priced from 1,599 yuan and will be sold online and offline at 10 am on April 27. Xiaomi Mi 6X adopts the current mainstream 5.99-inch 18:9 full-screen design. The all-metal body adopts an "invisible" antenna and a fully symmetrical design at the bottom. The thickness of the body is 7.3mm. Ishiguro and Sakura Pink are available in five colors.


About the Author


This article is from  Chen Xiaoyuan  's contribution, sharing how he completed the process of cool theme animation step by step, let's take a look! Hope you all like it.

Chen Xiaoyuan  's blog address:

https://blog.csdn.net/u011387817


foreword


Two days ago, I happened to see a very cool animation effect:

640?wx_fmt=gif&wxfrom=5&wx_lazy=1

So I want to know how it is implemented, because I have the experience of analyzing the animation effect last time, and judge whether it is using ValueAnimator. If so, we can set "Animation Duration Scaling" in Settings-Developer Options to change it. The animation duration, so this time we set this option to reduce the animation speed, and soon saw the mystery.


text


initial analysis

Let's slow it down first:

640?wx_fmt=jpeg

Let's adjust the animation duration scaling to 10x and see the effect:

640?wx_fmt=gif

Haha, have you noticed that when the animation is playing, the list cannot be swiped, there may be the following three reasons:

  1. Interrupted or consumed by the ViewGroup it is in;

  2. Consumed by other Views first;

  3. The list can slide according to whether the animation is playing or not;

Let's take a closer look at the animation again. . .

Will that animation be a View? If it is a View, the reason why the list cannot slide may be 2: it is consumed by this View first.

Well, let's assume that the animation is an independent View, so what exactly does this View do? 

The animation looks like the new color wipes out the old one. Huh? Wait, wipe it off, hahaha, wipe off the old one, doesn't it feel like a lighthearted feeling now? We continue to think in this way. 

idea code

Remember the PorterDuff.Mode.CLEAR mode? Use this to achieve the effect of an eraser, and we will definitely use it later. Since the old thing is to be wiped, then the old thing must be obtained first, haha, this can be obtained with getDrawingCache. We just assumed that it is a View, that is, a custom View, then our View must be added to the corresponding View before it can be displayed.

Prepare the above things, and the process of our animation is very clear: 

This time we don't plan to make it hard-coded in the layout, because it is not flexible enough, and it should be added and removed dynamically.

  1. Get a screenshot of the root layout;

  2. Add a custom View to the root layout;

  3. draw the screenshot;

  4. Set paint's Xfermode to PorterDuff.Mode.CLEAR;

  5. Taking the position where the button is clicked as the starting point, draw a circle, and continuously expand the radius of the circle;

  6. Until the circle is enough to cover the screen, stop the animation, remove from the root layout, and call back the interface of the playback completion;

For the beauty of the code, we can make the constructor private, and then expose a static create(View onClickView) method, haha, as long as a clicked View is passed in, we can meet the conditions we need to play the animation. 

So how do we calculate how much radius this circle needs to just cover the screen? Because the position of the button cannot be fixed, the required radius must be calculated dynamically. 

Let's take a look at the picture below: 

640?wx_fmt=png

This is the effect after turning on the pointer position in the developer options. You can see that after our finger is pressed, the screen is divided into 4 small rectangles by blue lines. We can calculate the diagonal lengths of these small rectangles respectively. Then take the longest one as the radius of the circle we draw. Then the problem of calculating the radius of the circle is solved. The following can basically write the code all the way smoothly.

code by hand

First is the construction method:

    private RippleAnimation(Context context, float startX, float startY, int radius) {
       super(context);
       //获取activity的根视图,用来添加本View
       mRootView = (ViewGroup) ((Activity) getContext()).getWindow().getDecorView();
       mStartX = startX;
       mStartY = startY;
       mStartRadius = radius;
       mPaint = new Paint();
       mPaint.setAntiAlias(true);
       //设置为擦除模式
       mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
       updateMaxRadius();
       initListener();
   }

Now that the constructor is private, we need to expose a static create method:

    public static RippleAnimation create(View onClickView) {
       Context context = onClickView.getContext();
       int newWidth = onClickView.getWidth() / 2;
       int newHeight = onClickView.getHeight() / 2;
       //计算起点位置
       float startX = getAbsoluteX(onClickView) + newWidth;
       float startY = getAbsoluteY(onClickView) + newHeight;
       //起始半径
       //因为我们要避免遮挡按钮
       int radius = Math.max(newWidth, newHeight);
       return new RippleAnimation(context, startX, startY, radius);
   }

Let's see how the code to get the radius of the circle is written:

    /**
    * 根据起始点将屏幕分成4个小矩形,mMaxRadius就是取它们中最大的矩形的对角线长度
    * 这样的话, 无论起始点在屏幕中的哪一个位置上, 我们绘制的圆形总是能覆盖屏幕
    */

   private void updateMaxRadius() {
       //将屏幕分成4个小矩形
       RectF leftTop = new RectF(0, 0, mStartX + mStartRadius, mStartY + mStartRadius);
       RectF rightTop = new RectF(leftTop.right, 0, mRootView.getRight(), leftTop.bottom);
       RectF leftBottom = new RectF(0, leftTop.bottom, leftTop.right, mRootView.getBottom());
       RectF rightBottom = new RectF(leftBottom.right, leftTop.bottom, mRootView.getRight(), leftBottom.bottom);
       //分别获取对角线长度
       double leftTopHypotenuse = Math.sqrt(Math.pow(leftTop.width(), 2) + Math.pow(leftTop.height(), 2));
       double rightTopHypotenuse = Math.sqrt(Math.pow(rightTop.width(), 2) + Math.pow(rightTop.height(), 2));
       double leftBottomHypotenuse = Math.sqrt(Math.pow(leftBottom.width(), 2) + Math.pow(leftBottom.height(), 2));
       double rightBottomHypotenuse = Math.sqrt(Math.pow(rightBottom.width(), 2) + Math.pow(rightBottom.height(), 2));
       //取最大值
       mMaxRadius = (int) Math.max(
               Math.max(leftTopHypotenuse, rightTopHypotenuse),
               Math.max(leftBottomHypotenuse, rightBottomHypotenuse));
   }

create方法里面有个getAbsoluteX和getAbsoluteY方法,这两个方法分别是获取view在屏幕中的x坐标和y坐标,为什么要有这两个方法呢,因为被点击的View所在的ViewGroup不一定top、left都是0的,所以如果我们直接获取这个View的xy坐标的话,是不够的,还要加上它父容器的xy坐标,我们要一直递归下去,这样就能真正获取到View在屏幕中的绝对坐标了:

   /**
    * 获取view在屏幕中的绝对x坐标
    */

   private static float getAbsoluteX(View view) {
       float x = view.getX();
       ViewParent parent = view.getParent();
       if (parent != null && parent instanceof View) {
           x += getAbsoluteX((View) parent);
       }
       return x;
   }

   /**
    * 获取view在屏幕中的绝对y坐标
    */

   private static float getAbsoluteY(View view) {
       float y = view.getY();
       ViewParent parent = view.getParent();
       if (parent != null && parent instanceof View) {
           y += getAbsoluteY((View) parent);
       }
       return y;
   }

我们还要定义一个start方法,用来启动动画:

    public void start() {
       if (!isStarted) {
           isStarted = true;
           updateBackground();
           attachToRootView();
           getAnimator().start();
       }
   }

updateBackground方法就是更新屏幕截图了,这个当然要从DecorView中获取了:

    /**
    * 更新屏幕截图
    */

   private void updateBackground() {
       if (mBackground != null && !mBackground.isRecycled()) {
           mBackground.recycle();
       }
       mRootView.setDrawingCacheEnabled(true);
       mBackground = mRootView.getDrawingCache();
       mBackground = Bitmap.createBitmap(mBackground);
       mRootView.setDrawingCacheEnabled(false);
   }

更新完截图后,我们就要添加自身到根布局中了:

   /**
    * 添加到根视图
    */

   private void attachToRootView() {
       setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
       mRootView.addView(this);
   }

我们调用addView方法之前还set了一个宽高都是MATCH_PARENT的LayoutParams,这样我们的View就能遮挡住屏幕,然后把刚刚获取到的截图draw上去,以假乱真,哈哈:

    @Override
   protected void onDraw(Canvas canvas) {
       //在新的图层上面绘制
       int layer = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
       canvas.drawBitmap(mBackground, 0, 0, null);
       canvas.drawCircle(mStartX, mStartY, mCurrentRadius, mPaint);
       canvas.restoreToCount(layer);
   }

我们的start方法中最后是调用了getAnimator().start(); 看看getAnimator方法里面做了什么:

     private ValueAnimator getAnimator() {
       ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mMaxRadius).setDuration(mDuration);
       valueAnimator.addUpdateListener(mAnimatorUpdateListener);
       valueAnimator.addListener(mAnimatorListener);
       return valueAnimator;
   }

就创建了一个ValueAnimator,动画的起始值是0,结束值是mMaxRadius,也就是圆的最大半径,我们的mCurrentRadius跟着动画的值更新,那么当我们的动画播放完之后,mCurrentRadius就刚好等于mMaxRadius,也就刚好覆盖屏幕了,这个时候我们也可以将自身从根布局中移除了:

    private void initListener() {
       mAnimatorListener = new AnimatorListenerAdapter() {
           @Override
           public void onAnimationEnd(Animator animation) {
               //动画播放完毕, 移除本View
               detachFromRootView();
               if (mOnAnimationEndListener != null) {
                   mOnAnimationEndListener.onAnimationEnd();
               }
               isStarted = false;
           }
       };
       mAnimatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator animation) {
               //更新圆的半径
               mCurrentRadius = (int) (float) animation.getAnimatedValue() + mStartRadius;
               postInvalidate();
           }
       };
   }

   /**
    * 从根视图中移除
    */

   private void detachFromRootView() {
       mRootView.removeView(this);
   }

对了,还应该有一个setDuration方法来设置动画的时长:

   public RippleAnimation setDuration(long duration) {
       mDuration = duration;
       return this;
   }

哈哈,到这里我们的RippleAnimation就基本完成了. 

说说酷安这个效果的实现原理:

  1. 切换主题前先获取当前屏幕截图;

  2. 开始播放动画;

  3. 切换主题;

哈哈, 不断扩大的圆形就会把旧的屏幕截图擦掉, 从而看到下面新的主题颜色, 这样我们的炫酷效果就出来了, 哈哈哈

因为只能从create方法中获取到RippleAnimation对象,所以我们的使用方法也是非常的简单,并且只需一行代码就能播放了:

    public void onClick(View view) {
       RippleAnimation.create(view).setDuration(200).start();
       //在这里切换主题
   }

我们写一个demo来测试一下我们的劳动成果: 

640?wx_fmt=gif

Hahaha, compare it with the effect of the Kuan client: 

640?wx_fmt=gif

Haha, this is basically the effect. In fact, our effect is not necessarily only used for theme switching, but also for the transition of other interface switching. Hahaha, you can use your imagination to make more dazzling Cool animation effects.


Epilogue


Well, the article ends here, the specific demo address is as follows:

https://github.com/wuyr/RippleAnimation


Welcome to long press the picture  ->  identify the QR code in the picture

Or  scan and  follow my official account

640.png?

640?wx_fmt=jpeg

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325744267&siteId=291194637