Android animation in action

foreword

Through the previous "Android Animation Summary" , I have an overall understanding of commonly used Android animations. However, the previous content is conceptual, and the demo listed has no practical significance. Here are two examples to learn how to use Android animation in actual development to achieve some good user experience.

Here are two common and easier to implement animation effects:

Imitation Alipay payment to complete the animation
shopping cart to add goods animation

Animation combat

Imitation Alipay payment complete animation

First look at the renderings.

be happy

The simulator intercepts animation is really drunk

Payment success animation

Regarding this successful payment animation, it can be achieved through the frame animation (Frame Animation) mentioned earlier, but the premise is that perfect picture resources are required. If the UI does not provide image resources, is it helpless? In fact, for this kind of animation with a relatively simple composition, it can still be achieved through attribute animation.

Observe this animation, first draw a circle , draw a "check mark" when the circle is completed, and then perform the effect of discoloration and zooming of the entire view when the animation is completed, and modify the text on the button at the same time .

Then our animation implementation is also in this order:

public void loadCircle(int mRadius) {
        mRadius = mRadius <= 0 ? DEFAULT_RADIUS : mRadius;
        this.mRadius = mRadius - PADDING;
        if (null != mAnimatorSet && mAnimatorSet.isRunning()) {
            return;
        }
        reset();
        reMeasure();
        Log.e("left", "R is -------->" + mRadius);
        mCircleAnim = ValueAnimator.ofInt(0, 360);
        mLineLeftAnimator = ValueAnimator.ofFloat(0, this.mRadius / 2f);
        mLineRightAnimator = ValueAnimator.ofFloat(0, this.mRadius / 2f);
        Log.i(TAG, "mRadius" + mRadius);
        mCircleAnim.setDuration(700);
        mLineLeftAnimator.setDuration(350);
        mLineRightAnimator.setDuration(350);
        mCircleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mDegree = (Integer) animation.getAnimatedValue();
                invalidate();
            }
        });
        mLineLeftAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mLeftValue = (Float) valueAnimator.getAnimatedValue();
                Log.e("left", "-------->" + mLeftValue);
                invalidate();
            }
        });
        mLineRightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mRightValue = (Float) animation.getAnimatedValue();
                invalidate();
            }
        });
        mAnimatorSet.play(mCircleAnim).before(mLineLeftAnimator);
        mAnimatorSet.play(mLineRightAnimator).after(mLineLeftAnimator);
        mAnimatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                stop();
                if (mEndListner != null) {
                    mEndListner.onCircleDone();
                    SuccessAnim();
                }

            }
        });
        mAnimatorSet.start();
    }

We define three attribute animations, mCircleAnim, mLineLeftAnimator and mLineRightAnimator, and play the three animations in sequence. At the same time, we obtain the current change value of the animation in the respective update methods, and call invalidate() at the same time, so that the onDraw method will be continuously executed and drawn continuously. New view, animated. When the animation execution ends, the method defined in the interface to monitor the end of the animation can be executed. This is done here to facilitate the execution of some operations after the animation ends in the Activity. At the same time , the animation SuccessAnim() of the current view size scaling is executed.

Here we focus on the onDraw method, which can be said to be the core content of the entire animation.

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mRectF.left = mCenterX - mRadius;
        mRectF.top = mCenterY - mRadius;
        mRectF.right = mCenterX + mRadius;
        mRectF.bottom = mCenterY + mRadius;
        canvas.drawArc(mRectF, 0, mDegree, false, mCirclePanit);
        canvas.drawLine(mCenterX - mRadius / 2, mCenterY,
                mCenterX - mRadius / 2 + mLeftValue, mCenterY + mLeftValue, mLinePaint);
        canvas.drawLine(mCenterX, mCenterY + mRadius / 2,
                mCenterX + mRightValue, mCenterY + mRadius / 2 - (3f / 2f) * mRightValue, mLinePaint);

    }

1. The implementation of canvas.drawArc in line 7 is easy to understand. In the previous property animation, we implemented a ValueAnimator with an initial value of 0 and an end value of 360. At the same time, during its execution, the intermediate value was continuously assigned to mDegree, so that the mDegree value changes from 0 to 360, thus realizing a circle drawing.

2. What is drawn in line 8 is the short line on the left side of the "check mark" . Line 10 draws the long line up from the right. The idea here is easy to understand with the following figure (this is just a schematic diagram, the slope of the long line on the right is determined by multiple values ​​​​of the circle center and radius during actual drawing).

Drawing principle

Two points determine a straight line, it's that simple.

As mentioned before, the operation mechanism of property animation is realized by continuously manipulating the value, and the animation transition between the initial value and the end value is calculated by the ValueAnimator class.

Here we use this principle to achieve this animation.

Understand this, the following animation of payment failure is also a similar principle, the content drawn in the middle is no longer a "check mark", but a huge X. This is easy to implement. Taking the center of the circle as the midpoint of the coordinate axis, draw four points in the 45-degree direction in the four quadrants, which can be used as the starting point and the end point respectively. It is easy to understand with the code.

        int mViewWidth = getWidth();
        int mViewHeight = getHeight();
        mCenterX = mViewWidth / 2;
        mCenterY = mViewHeight / 2;

        temp = mRadius / 2.0f * factor;
        Path path = new Path();
        path.moveTo(mCenterX - temp, mCenterY - temp);
        path.lineTo(mCenterX + temp, mCenterY + temp);
        pathLeftMeasure = new PathMeasure(path, false);

        path = new Path();
        path.moveTo(mCenterX + temp, mCenterY - temp);
        path.lineTo(mCenterX - temp, mCenterY + temp);
        pathRightMeasure = new PathMeasure(path, false);

Drawing method onDraw

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mRectF.left = mCenterX - mRadius;
        mRectF.top = mCenterY - mRadius;
        mRectF.right = mCenterX + mRadius;
        mRectF.bottom = mCenterY + mRadius;
        canvas.drawArc(mRectF, 0, mDegree, false, mCirclePanit);
        if (mLeftPos[1] > (mCenterY - temp) && mRightPos[1] > (mCenterY - temp)) {
            canvas.drawLine(mCenterX - temp, mCenterY - temp, mLeftPos[0], mLeftPos[1], mLinePaint);
            canvas.drawLine(mCenterX + temp, mCenterY - temp, mRightPos[0], mRightPos[1], mLinePaint);
        }
    }

The mLeftPos and mRightPos here are the positions corresponding to the intermediate change values ​​when the property animation transitions from the initial value to the end value. The specific can be understood in conjunction with the source code .

Finally, let me say that using frame animation to achieve this animation, in order to adapt to different models, it is necessary to need multiple pictures of different resolutions. The adaptation effect is unknown, and it will also increase the size of the application. However, using frame animation is different. It should be relatively easy to adapt to the size of the entire view. At the same time, the application size will not change, and the scalability is also higher.

Add product animation to shopping cart

Shopping add animation can be said to be the most classic example of attribute animation; it has been implemented a long time ago. Here we understand it from the perspective of learning attribute animation.

The drawing of the trajectory here is not completely completed by attribute animation, and a large part of the credit should be counted on the Bezier curve. For an understanding of Bezier curves, you can look here .

private void addToCarAnimation(ImageView goodsImg) {
        //获取需要进行动画的ImageView
        final ImageView animImg = new ImageView(mContext);
        animImg.setImageDrawable(goodsImg.getDrawable());
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
        shellLayout.addView(animImg, params);
        //
        final int shellLocation[] = new int[2];
        shellLayout.getLocationInWindow(shellLocation);
        int animImgLocation[] = new int[2];
        goodsImg.getLocationInWindow(animImgLocation);
        int carLocation[] = new int[2];
        carImage.getLocationInWindow(carLocation);
        //
        // 起始点:图片起始点-父布局起始点+该商品图片的一半-图片的marginTop || marginLeft 的值
        float startX = animImgLocation[0] - shellLocation[0] + goodsImg.getWidth() / 2 - DpConvert.dip2px(mContext, 10.0f);
        float startY = animImgLocation[1] - shellLocation[1] + goodsImg.getHeight() / 2 - DpConvert.dip2px(mContext, 10.0f);

        // 商品掉落后的终点坐标:购物车起始点-父布局起始点+购物车图片的1/5
        float endX = carLocation[0] - shellLocation[0] + carImage.getWidth() / 5;
        float endY = carLocation[1] - shellLocation[1];

        //控制点,控制贝塞尔曲线
        float ctrlX = (startX + endX) / 2;
        float ctrlY = startY - 100;

        Log.e("num", "-------->" + ctrlX + " " + startY + " " + ctrlY + " " + endY);

        Path path = new Path();
        path.moveTo(startX, startY);
        // 使用二阶贝塞尔曲线
        path.quadTo(ctrlX, ctrlY, endX, endY);
        mPathMeasure = new PathMeasure(path, false);

        ObjectAnimator scaleXanim = ObjectAnimator.ofFloat(animImg, "scaleX", 1, 0.5f, 0.2f);
        ObjectAnimator scaleYanim = ObjectAnimator.ofFloat(animImg, "scaleY", 1, 0.5f, 0.2f);

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到mCurrentPosition
                // mCurrentPosition此时就是中间距离点的坐标值
                mPathMeasure.getPosTan(value, mCurrentPosition, null);
                // 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
                animImg.setTranslationX(mCurrentPosition[0]);
                animImg.setTranslationY(mCurrentPosition[1]);
            }
        });

        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                goodsCount++;
                if (goodsCount < 100) {
                    carCount.setText(String.valueOf(goodsCount));
                } else {
                    carCount.setText("99+");
                }

                // 把执行动画的商品图片从父布局中移除
                shellLayout.removeView(animImg);
                shopCarAnim();

            }
        });

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(500);
        animatorSet.setInterpolator(new AccelerateInterpolator());
        animatorSet.playTogether(scaleXanim, scaleYanim, valueAnimator);
        animatorSet.start();

    }

We obtained the position of the entire layout on the phone screen: shellLocation The position
of the picture to be animated on the phone screen: animLocation The position of the
shopping cart on the entire phone screen: carLocation

And three points are determined by these three values ​​and the size and layout of the animated picture:

Start position (startX, startY), end position (endX, endY) and control point (CtrlX, CtrlY).
And a second-order Bezier curve path is determined by these three points.

At the same time use the PathMeasure class to measure this path, and use its length length as the end value in the property animation.

ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());

In the update callback method of the animation, we get the intermediate value of this length transition change, and then we use a very important method

mPathMeasure.getPosTan(value, mCurrentPosition, null);

You can take a look at the specific implementation of this method

/**
     * Pins distance to 0 <= distance <= getLength(), and then computes the
     * corresponding position and tangent. Returns false if there is no path,
     * or a zero-length path was specified, in which case position and tangent
     * are unchanged.
     *
     * @param distance The distance along the current contour to sample
     * @param pos If not null, eturns the sampled position (x==[0], y==[1])
     * @param tan If not null, returns the sampled tangent (x==[0], y==[1])
     * @return false if there was no path associated with this measure object
    */
    public boolean getPosTan(float distance, float pos[], float tan[]) {
        if (pos != null && pos.length < 2 ||
            tan != null && tan.length < 2) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return native_getPosTan(native_instance, distance, pos, tan);
    }

This method has three parameters

  1. The first parameter, the current value of the length of the path measured by MeasuePath, is the transition value in our animation change.

  2. The second parameter is an array, if not null, it will be assigned the coordinates of the position corresponding to the current value.

  3. The third parameter is also an array. If it is not null, it will be assigned the tangent coordinate corresponding to the current value. (I don't understand the meaning of the god horse)

If the path measured by this MeasurePath does not exist, it will return false.

In the end, this method will execute a native method, and we will not know the specific implementation.

Back to our code, here we pass in a two-dimensional int array as the second parameter, so as the total length of the path elapses, we sequentially obtain the coordinate point mCurrentPosition on the path line. Then the animation effect is achieved by setting the position of the animation picture animImg.

Here I focus on the overall implementation idea. In practice, there are many details worth considering, especially when switching to GridView mode, the animation starting point is different on the left and right sides. For details, you can refer to the source code to think for yourself.

Summarize

As you can see here, the ValueAnimator class is very simple, but very useful. He helps us realize a natural transition of property values ​​from start to end, and can obtain the intermediate value of the transition process, so that it is very convenient for us to combine this transition value to do various animations.

Finally , the github source code welcomes star & fork


Guess you like

Origin blog.csdn.net/TOYOTA11/article/details/53014663