A simple Android arc refresh animation

I saw the refresh animation of the post bar when I brushed the post bar before, which is an animation of circular arc rotation. It feels very good, so I took the time to realize it.

The final result is as follows:

As can be seen from the above figure, the effect of animation is that three arcs rotate, and the radian is gradually increasing and decreasing. Here, three arcs are drawn in OnDraw.

// 绘制圆弧
mPaint.setColor(mTopColor);
canvas.drawArc(left,top,right,bottom,startAngle,sweepAngle,false,mPaint);
mPaint.setColor(mLeftColor);
canvas.drawArc(left,startAngle - 120,mPaint);
mPaint.setColor(mRightColor);
canvas.drawArc(left,startAngle + 120,mPaint);

Animation is based on drawing arcs of three different colors in OnDraw. The three arcs are often separated by 120 degrees, so that the whole circle can be evenly divided, which is more beautiful.

Note that the initial value of startangle here is - 90, which is just the top point of the circle. It should be noted here that in the drawarc method of canvas, the first four parameters are the coordinates of the rectangle that determines the position of the arc, startangle refers to the starting angle of the arc, 0 degree is the rightmost point of the circle, and clockwise is positive and counterclockwise is negative. So - 90 degrees is just the top point of the circle.

Sweepangle refers to the angle swept by the arc. Similarly, clockwise is positive and counterclockwise is negative. Here, the initial value of the size of the sweepangle is - 1, so that a dot (actually an arc with an angle of 1, an approximate dot) can be drawn before the animation starts. The latter parameter is usecenter, which refers to whether to use the center. When it is true, the two ends of the arc will be connected to the center to form a sector. When it is false, the center will not be connected.

In addition, note that the style of paint should be set to stroke. By default, it is fill mode, that is, it will be filled directly. For the arc here, the two ends of the arc are directly connected to form a closed figure, and then filled.

In this way, the initial state of the animation is drawn: three dots (actually an arc with an angle of 1).

It can also be seen from the above that to draw an arc, there must be four coordinates. The coordinates here are obtained in this way: take the shortest side of the length and width of view as the side length of the square constituting the circle, and then display it in the center.

int width = getMeasuredWidth();
int height = getMeasuredHeight();
int side = Math.min(width - getPaddingStart() - getPaddingEnd(),height - getPaddingTop() - getPaddingBottom()) - (int) (mstrokeWidth + 0.5F);

// 确定动画位置
float left = (width - side) / 2F;
float top = (height - side) / 2F;
float right = left + side;
float bottom = top + side;

The above code is the implementation of the square coordinates of the positioning arc. Here you can see that the padding and mstrokenwidth of the view are removed when calculating the side length. Mstrokenwidth is the width of the arc line of the arc. When the arc line is wide (equivalent to a ring at this time), it will extend evenly to the inside and outside, that is, the distance from the middle of the inner margin and outer margin to the center of the circle is the radius. Therefore, when determining the position of the arc, the line width should be removed to prevent the arc from being completely drawn at the junction.

In addition, when we customize the view, the default is wrap_ In content mode, it will match with_ The effect of parent is the same, so it needs to be processed in onmeasure. Here, simply set wrap_ 20dp in content mode.

@Override
  protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    // 对于wrap_content ,设置其为20dp。认情况下wrap_content和match_parent是一样的效果
    if (widthMode == MeasureSpec.AT_MOST) {
      width = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20,getContext().getResources().getDisplayMetrics()) + 0.5F);
    }
    if (heightMode == MeasureSpec.AT_MOST) {
      height = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,getContext().getResources().getDisplayMetrics()) + 0.5F);
    }
    setMeasuredDimension(width,height);
  }

The above operations are the whole basis of animation, and the operation to make the view move is to constantly modify the startangle and sweepangle of the arc, and then trigger the redrawing of the view. This process uses valueanimator to generate a series of numbers, and then calculates the starting angle and scanning angle of the arc according to this.

// 最小角度为1度,是为了显示小圆点
sweepAngle = -1;
startAngle = -90;
curStartAngle = startAngle;

// 扩展动画
mValueAnimator = ValueAnimator.ofFloat(0,1).setDuration(mDuration);
mValueAnimator.setRepeatMode(ValueAnimator.REVERSE);
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
mValueAnimator.addUpdateListener(animation -> {
float fraction = animation.getAnimatedFraction();
float value = (float) animation.getAnimatedValue();
      if (mReverse)
        fraction = 1 - fraction;
      startAngle = curStartAngle + fraction * 120;
      sweepAngle = -1 - mMaxSweepAngle * value;
      postInvalidate();
    });
    mValueAnimator.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationRepeat(Animator animation) {
        curStartAngle = startAngle;
        mReverse = !mReverse;
      }
    });

The above is the calculation process. The animation uses the value value from 0 to 1 and then to 0, corresponding to one of the arcs extending from the origin to the maximum and then shrinking back to the origin. The calculation of sweepangle is sweepangle = - 1 - maxsweepangle * value, that is, in the whole process, the angle of the arc gradually increases to maxsweepangle. Negative values are used here, that is, draw counterclockwise from startangle- 1 is the base value to prevent it from shrinking to the minimum, and a dot can be displayed.

The calculation of startangle is based on the fraction of the animation process, not the animation value, that is, from 0 to 1, gradually increasing by 120 degrees throughout the animation process. Because the whole view is formed by three identical arcs, that is, each arc can only occupy 120 degrees at most, otherwise it will overlap. In the process of 0 to 1, when the radian increases to 120 degrees, startangle must move 120 degrees to make room for the arc. This is the origin of 120 degrees. And monitor the reverse state, because in the reverse state, the fraction is from 1 to 0, and what we need is that the startangle has been gradually increasing. Therefore, in the reverse state, make it consistent with the original animation through 1-fraction. And record the startangle as the new current position and record the reverse status each time the reverse is monitored.

The above is the implementation details of the whole arc animation. The whole is relatively simple, that is, change the startangle and sweepangle of the radian, and then notify view to redraw. The following is the complete code of the implementation. Here, some basic variables are extracted and put into attributes to easily control the display of animation:

values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="RefreshView">
    <attr name="top_color" format="color"/>
    <attr name="left_color" format="color"/>
    <attr name="right_color" format="color"/>
    <!-- 圆弧的宽度 -->
    <attr name="border_width" format="dimension"/>
    <!-- 每个周期的时间,从点到最大弧为一个周期,ms -->
    <attr name="duration" format="integer"/>
    <!-- 圆弧扫过的最大角度 -->
    <attr name="max_sweep_angle" format="integer"/>
    <!-- 是否自动开启动画 -->
    <attr name="auto_start" format="boolean"/>
  </declare-styleable>

</resources>

RefreshView.java

package com.pgaofeng.mytest.other;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.pgaofeng.mytest.R;

/**
 * @author gaofengpeng
 * @date 2019/9/16
 * @description :
 */
public class RefreshView extends View {

  /**
   * 动画的三种颜色
   */
  private int mTopColor;
  private int mLeftColor;
  private int mRightColor;

  private Paint mPaint;

  /**
   * 扫描角度,用于控制圆弧的长度
   */
  private float sweepAngle;
  /**
   * 开始角度,用于控制圆弧的显示位置
   */
  private float startAngle;
  /**
   * 当前角度,记录圆弧旋转的角度
   */
  private float curStartAngle;

  /**
   * 用动画控制圆弧显示
   */
  private ValueAnimator mValueAnimator;

  /**
   * 每个周期的时长
   */
  private int mDuration;
  /**
   * 圆弧线宽
   */
  private float mstrokeWidth;

  /**
   * 动画过程中最大的圆弧角度
   */
  private int mMaxSweepAngle;

  /**
   * 是否自动开启动画
   */
  private boolean mAutoStart;

  /**
   * 用于判断当前动画是否处于Reverse状态
   */
  private boolean mReverse = false;

  public RefreshView(Context context) {
    this(context,null);
  }

  public RefreshView(Context context,@Nullable AttributeSet attrs) {
    this(context,attrs,0);
  }

  public RefreshView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) {
    super(context,defStyleAttr);
    initAttr(context,attrs);
    init();
  }

  private void initAttr(Context context,AttributeSet attrs) {
    TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.RefreshView);
    mTopColor = array.getColor(R.styleable.RefreshView_top_color,Color.BLUE);
    mLeftColor = array.getColor(R.styleable.RefreshView_left_color,Color.YELLOW);
    mRightColor = array.getColor(R.styleable.RefreshView_right_color,Color.RED);
    mDuration = array.getInt(R.styleable.RefreshView_duration,600);
    if (mDuration <= 0) {
      mDuration = 600;
    }
    mstrokeWidth = array.getDimension(R.styleable.RefreshView_border_width,8F);
    mMaxSweepAngle = array.getInt(R.styleable.RefreshView_max_sweep_angle,90);
    if (mMaxSweepAngle <= 0 || mMaxSweepAngle > 120) {
      // 对于不规范值直接采用认值
      mMaxSweepAngle = 90;
    }
    mAutoStart = array.getBoolean(R.styleable.RefreshView_auto_start,true);

    array.recycle();
  }

  private void init() {

    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.stroke);
    mPaint.setstrokeWidth(mstrokeWidth);
    mPaint.setstrokeCap(Paint.Cap.ROUND);

    // 最小角度为1度,是为了显示小圆点
    sweepAngle = -1;
    startAngle = -90;
    curStartAngle = startAngle;

    // 扩展动画
    mValueAnimator = ValueAnimator.ofFloat(0,1).setDuration(mDuration);
    mValueAnimator.setRepeatMode(ValueAnimator.REVERSE);
    mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
    mValueAnimator.addUpdateListener(animation -> {
      float fraction = animation.getAnimatedFraction();
      float value = (float) animation.getAnimatedValue();

      if (mReverse)
        fraction = 1 - fraction;

      startAngle = curStartAngle + fraction * 120;
      sweepAngle = -1 - mMaxSweepAngle * value;
      postInvalidate();
    });
    mValueAnimator.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationRepeat(Animator animation) {
        curStartAngle = startAngle;
        mReverse = !mReverse;
      }
    });

  }

  @Override
  protected void onMeasure(int widthMeasureSpec,height);
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();

    int side = Math.min(width - getPaddingStart() - getPaddingEnd(),height - getPaddingTop() - getPaddingBottom()) - (int) (mstrokeWidth + 0.5F);

    // 确定动画位置
    float left = (width - side) / 2F;
    float top = (height - side) / 2F;
    float right = left + side;
    float bottom = top + side;

    // 绘制圆弧
    mPaint.setColor(mTopColor);
    canvas.drawArc(left,mPaint);
    mPaint.setColor(mLeftColor);
    canvas.drawArc(left,mPaint);
    mPaint.setColor(mRightColor);
    canvas.drawArc(left,mPaint);
  }

  @Override
  protected void onDetachedFromWindow() {
    if (mAutoStart && mValueAnimator.isRunning()) {
      mValueAnimator.cancel();
    }
    super.onDetachedFromWindow();
  }

  @Override
  protected void onAttachedToWindow() {
    if (mAutoStart && !mValueAnimator.isRunning()) {
      mValueAnimator.start();
    }
    super.onAttachedToWindow();
  }

  /**
   * 开始动画
   */
  public void start() {
    if (!mValueAnimator.isStarted()) {
      mValueAnimator.start();
    }
  }

  /**
   * 暂停动画
   */
  public void pause() {
    if (mValueAnimator.isRunning()) {
      mValueAnimator.pause();
    }
  }

  /**
   * 继续动画
   */
  public void resume() {
    if (mValueAnimator.isPaused()) {
      mValueAnimator.resume();
    }
  }

  /**
   * 停止动画
   */
  public void stop() {
    if (mValueAnimator.isStarted()) {
      mReverse = false;
      mValueAnimator.end();
    }
  }

}

The above is the whole content of this article. I hope it will help you in your study, and I hope you will support us a lot.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>