The Emergence of Android Animation Candle Blowing Animation

Keywords: Attribute

I. Introduction

Let's not say much. Let's look at these two little candles.



Small candles hold their breath and ignite the flame. They are blown out 0 ^ 0 by the buddies next to them. They still seem to be full of vigor. Looking at the picture, you should be able to think about how to achieve it, custom View! Yes, but how to do this process well? Follow your steps and have a look. There's a little bit more code, so watch it patiently.

II. Process Realization

Drawing and animation of candles
  • In the light of object-oriented thinking, it is obvious that there are two candles here. Since this is the case, we define a candle class as having the basic properties of candles.
public abstract class ICandle {

    //Left lower coordinates at the bottom of the candle
    protected int mCurX;
    protected int mCurY;
    //Candle width
    protected int mCandleWidth;
    protected int mCandleHeight;
    //Left eye coordinates of candles
    protected Point mEyeLPoint;
    //Right eye coordinates of candles
    protected Point mEyeRPoint;
    //Candle Eye Radius
    protected int mEyeRadius;
    //Eye spacing
    protected int mEyeDevide;
    //Body color
    protected int mCandleColor;
    //Stop animation or not
    protected boolean mIsAnimStoping = false;
    //Candle wick coordinates
    protected Point mCandlewickPoint;
    public void initAnim(){
    }
    public void stopAnim(){
    }
    public void drawSelf(Canvas canvas){
    }
}

There's code for the candle, so it's clear at a glance. Perhaps what needs to be explained is the following methods: public void initAnim(), stopAnim() initializes the data needed to start and end the animation. A small candle will implement this method, drawSelf(Canvas). canvas) Pass in the canvas and the candle draws itself.
Now it's time for us to take a look at the internal structure of the candle body, hiahiahiahia!
No, there's a flame that goes with the candle's life and death. Let's look at the flame first. Wait till the candle burns itself.

  • Let's first see what Fulham looks like.

There seems to be nothing wrong with it. First of all, the inner area is Flame. Outside, Mr. Flame burns his human light and ashes (wiping tears manually).
Let's take a look at the implementation of Flame. We analyze step by step.

    private static float CHANGE_FACTOR = 20;
    private Paint mPaint;
    private Path mPath;
    //Left lower coordinates
    private int mCurX;
    private int mCurY;
    //Flame width
    private int mWidth;
    //Flame height
    private int mHeight;
    //Record initial height
    private int mPreHeight;
    //Record initial width
    private int mPreWidth;
    //Variation parameters of control points of Bessel curve on top of flame
    private int mTopXFactor;
    private int mTopYFactor;
    //Used to achieve flame jitter
    private Random mRandom;
    //Aureole radius
    private int mHaloRadius;
    //Burning
    private boolean mIsFiring;
    //Whether to Start Stop Animation
    private boolean mIsStopAnim = false;
    private boolean mFlagStop = false;
    private LinearGradient mLinearGradient;
    private RadialGradient mRadialGradient;

    private ValueAnimator mFlameAnimator;
    private ValueAnimator mHaloAnimator;

These are the parameters, mainly our animation implementation process, that is, our attribute animation Value Animator here are two rendering classes do not know whether you have used, Linear Gradient and Radial Gradient do not understand the students can directly link. My csdn Read about it on my blog. Linear Gradient draws the flame, Radial Gradient draws in addition to the diffuse light.
I won't write the initialization process. Let's see the code. The main thing is how to draw the small flame.

    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setShader(mLinearGradient);
    mPath.reset();
    mPath.moveTo(mCurX, mCurY);
    mPath.quadTo(mCurX + mWidth / 2,
            mCurY + mHeight / 3,
            mCurX + mWidth, mCurY);
    mPath.quadTo(mCurX + mWidth / 2 + ((1 - mRandom.nextFloat()) * CHANGE_FACTOR) + mTopXFactor,
            mCurY - 2 * mHeight + mTopYFactor,
            mCurX, mCurY);
    canvas.drawPath(mPath, mPaint);

This is the drawing of flame flame. You can see that the drawing of quadratic Bessel curve is used here. Students who are not very clear about the Bessel curve can also point to this. Wave Loading Animation (Bessel Curve) There is a brief introduction, which was used in the view of a water wave. The drawing here refers to the rectangle in the previous one. Let's take a look at this one again (of course, the enhanced version of hiahia).


So why does the x coordinate above add mRandom. nextFloat ()?* What about CHANGE_FACTOR? You think, the flame will not sway left and right, using a random control to sway left and right.

    mFlameAnimator = ValueAnimator.ofFloat(0, 4).setDuration(4000);
    mFlameAnimator.setRepeatCount(ValueAnimator.INFINITE);
    mFlameAnimator.addUpdateListener(
                   new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float zeroToOne = (float) animation.getAnimatedValue();
            if (zeroToOne >= 1.0f && zeroToOne <= 1.2f) {
                //Flame ignite
                zeroToOne = 1.0f - 5 * (zeroToOne - 1.0f);//1-0
                mHeight = (int) (mPreHeight * (1 - zeroToOne));
                mIsFiring = true;
            } else if (zeroToOne >= 3.5f) {
                if (mFlagStop) {
                    mFlameAnimator.cancel();
                    return;
                }
                //The flame was blown out
                zeroToOne = 2 * (zeroToOne - 3.5f);//0-2
                mTopXFactor = (int) (-20 * zeroToOne);
                mTopYFactor = (int) (160 * zeroToOne);
//                    mWidth = (int) (mPreWidth * (1 -zeroToOne));
                mIsFiring = false;
            }
        }
    });

In 4 seconds, the flame has a series of activities. From below, as the wick moves up, the position of the flame is constantly changed. It is divided into two parts: flame ignition and flame extinction. As can be seen from the code, mHeight becomes larger slowly when the flame is ignited, and then the process of rising is hot. The other is when the flame is blown out, because the flame is blown out. The height must be kept at the previous value, so there is no need to change it. Instead, two factors, mTopXFactor and mTopYFactor, are used to control the position of the flame. Well, now that the flame is there, the wax torch turns to ash and tears begin to dry. It's time for the light of life to come out.
Diaphragm drawing and animation are relatively simple

            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(5);
            mPaint.setShader(mRadialGradient);
            canvas.drawCircle(mCurX + mWidth / 2,
                         mCurY - mHeight / 2, mHaloRadius, mPaint);
            canvas.drawCircle(mCurX + mWidth / 2,
                         mCurY - mHeight / 2, mHaloRadius + 5, mPaint);
            canvas.drawCircle(mCurX + mWidth / 2,
                         mCurY - mHeight / 2, mHaloRadius - 5, mPaint);
        mHaloAnimator = ValueAnimator.ofFloat(0, 1).setDuration(500);
        mHaloAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mHaloAnimator.addUpdateListener(
                         new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float zeroToOne = (float) animation.getAnimatedValue();
                if (mIsFiring) {
                    mHaloRadius = (int) (70 + zeroToOne % 1.0f * 20);
                }
            }
        });

There is only one parameter changed here, mHaloRadius, which is the radius of the aperture. But don't forget that other parameters are changing at the same time, just in mFlame Animator.
Okay, that's all for Flame.

  • FireCandle
    It's a strange name, Candle. It's awesome for Brother Word. I've already introduced ICandle. Now let's look at his implementation class, FireCandle, the two brothers of Candles.
    Initialization routine will not say, look at the variables that should be there.
      private Paint mPaint;
      //Centrox coordinates
      private int mCenterX;
      //Record Initial Width
      private int mPreWidth;
      //Record initial height
      private int mPreHeight;
      //Rotation angle of candle wick
      private int mCandlewickDegrees = 0;
      private Flame mFlame;
      private boolean mIsFire = false;
      private boolean mIsStateOnStart = false;
      private boolean mIsStateOnEnd = false;
      private boolean mFlagStop = false;
      private ValueAnimator mCandlesAnimator;
    Naming is still quite standard. We should know what it is at first glance.
    We're still going to focus on the combination of rendering and attribute animation, so we don't see the rendering (face-beating at the speed of light). Look at the animation.
      mCandlesAnimator = ValueAnimator.ofFloat(0, 4).setDuration(4000);
      mCandlesAnimator.addUpdateListener(
                       new ValueAnimator.AnimatorUpdateListener() {
          @Override
          public void onAnimationUpdate(ValueAnimator animation) {
              float zeroToOne = (float) animation.getAnimatedValue();
              if (zeroToOne <= 1.0f) {
                  //Candle wick pulls down
                  mIsFire = true;
                  mCandleWidth = mPreWidth + (int) (zeroToOne * 40);
                  mCandleHeight = mPreHeight - (int) (zeroToOne * 30);
                  mCandlewickDegrees = (int) (-60 + (180 + 60) * zeroToOne);
                  refreshEyePosition();
              } else if (zeroToOne <= 2.0f) {
                  zeroToOne = zeroToOne - 1.0f;
                  //Candle wick pendulum
                  if (zeroToOne <= 0.2f) {
                      zeroToOne = 1.0f - 5 * zeroToOne;
                      mIsFire = false;
                      mCandleWidth = mPreWidth + (int) (zeroToOne * 40);
                      mCandleHeight = mPreHeight - (int) (zeroToOne * 30);
                      mCandlewickDegrees = (int) (180 * zeroToOne);
                  } else {
                      if (mFlameStateListener != null && !mIsStateOnStart) {
                          mFlameStateListener.flameStart();
                          mIsStateOnStart = true;
                      }
                      mCandleWidth = mPreWidth;
                      mCandleHeight = mPreHeight;
                      mCandlewickDegrees = 0;
                      if (mFlagStop) {
                          mCandlesAnimator.cancel();
                      }
                  }
                  refreshEyePosition();
              } else if (zeroToOne >= 3.5f) {
                  //The candle wick was blown askew
                  zeroToOne = 2 * (zeroToOne - 3.5f);//0-1
                  mCandlewickDegrees = (int) (-60 * zeroToOne);
                  if (mFlameStateListener != null && !mIsStateOnEnd) {
                      mFlameStateListener.flameEnd();
                      mIsStateOnEnd = true;
                  }
              }
          }
      });
    This process is a little more, but in fact it is not complicated at all. First, we look at the small candle in the animation. At the beginning, he came to a squat with a fat red face. So mCandleWidth is bigger and mCandleHeight is smaller. The latter one rotates at a large angle with the squat. You can see how the wick rotates, change the coordinate system, and then you can see it. All right. Used.
    canvas.rotate(mCandlewickDegrees, mCenterX, MCurY - mCandleHeight; this method. The same is true of the pendulum process, let alone say. refreshEyePosition(); This method changes the position of the eyes, both of which are used, so it's slightly independent. Notice the mIsFire variable. When there is no fire, do other drawings, such as red eyes and so on. Okay, here we go, the little candle part is over.
  • SecCandle
    Big candles, handsome candletown buildings, the actual drawing and small candles are almost the same, here is no explanation.

Drawing View and Controller Together
  • AnimControler
    The function of this class is very simple. Drawing the floor part and assigning the height and width of the calculation to two candles, then controlling the two candles to start animation separately.
      mFirCandle = new FirCandle(mRelativeX + mWidth / 6, mRelativeY + mHeight);
      mFirCandle.initCandle(mFirCandleWidth, mFirCandleHeight);
      mFirCandle.initAnim();
      mSecCandle = new SecCandle(mRelativeX + mWidth / 2, mRelativeY + mHeight);
      mSecCandle.initCandle(mSecCandleWidth, mSecCandleHeight - 80);
      mSecCandle.initAnim();
    At the end of the day, it's our View.
  • CandlesAnimView

          //16ms refresh Canvas
          mInvalidateAnimator = ValueAnimator.ofInt(0, 1).setDuration(16);
          mInvalidateAnimator.setRepeatCount(ValueAnimator.INFINITE);
          mInvalidateAnimator.addListener(new AnimatorListenerAdapter() {
              @Override
              public void onAnimationRepeat(Animator animation) {
                  invalidate();
              }
          });
          mInvalidateAnimator.start();

    The task of this attribute animation is to refresh the interface quickly, so that Candle's animation can be displayed in time.

      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          int width = measureDimension(WIDTH_DEFAULT * mDensity, 
                                     widthMeasureSpec);
          int height = measureDimension(HEIGHT_DEFAULT *mDensity,
                                     heightMeasureSpec);
          setMeasuredDimension(width, height);
      }
    
      public int measureDimension(int defaultSize, int measureSpec) {
          int result;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
    
          if (specMode == MeasureSpec.EXACTLY) {
              result = specSize;
          } else {
              result = defaultSize;
              if (specMode == MeasureSpec.AT_MOST) {
                  result = Math.min(result, specSize);
              }
          }
          return result;
      }
    
      @Override
      protected void onDraw(Canvas canvas) {
          if (!mIsInit) {
              initConfig();
              mIsInit = true;
          }
          mAnimControler.drawMyView(canvas);
      }

You can see that we finally called our controller in view and passed cavas over.
Last tip: Have you found that the duration of each animation is the same?

Three, last

Well, so far, I've said so much about a simple view customization.

I hope to encourage you on the road of technology and go farther and farther. Thank you

Posted by linkin on Sun, 07 Apr 2019 12:39:31 -0700