Implementation of Custom Control for KPI Wheel Running and Digital Running

Keywords: Mobile Attribute Android github

In the process of project development, a lot of data are easily fixed (also directly set up data). Dynamic effects can only experience a little change occasionally in the process of switching or loading. In PC web, we can often see the data running and gradually changing process, so that users can use the data more dynamically and professionally. However, if these dynamics are implemented on the Android side, the experience of APP will be enhanced, and the user's cohesion and frequency will also be increased.

The following figure: KPI wheel pointer running, digital running effect, when loading data, you can see the data change process.

Analyzing the semi-circle and pointer running of the figure above and the numerical running effect, it is obvious that the control provided by the system can not be satisfied, so we can only customize the reality manually.
First of all, we will analyze the requirements:

1. Draw the color of the arc segment according to the number of KPI s and connect it to 180 degree semicircle. Draw the pointer at the same time. Then add the animation of gradual change to the pointer.
2. Text running can be realized by threads, but using threads to animate will affect performance overhead, so using animation will be more fluent.

After the requirement analysis is completed, since it is a custom control, some attributes should be defined to facilitate the use of layout and make the view operation easier:
The following attributes are needed to define:

<declare-styleable name="Statistics_View">
    <!--The radius of a circle-->
    <attr name="radian" format="integer" />
   <!--Width of arc-->
    <attr name="strokeWidth" format="integer" />
    <!--Width of pointer-->
    <attr name="pointerWidth" format="integer" />
    <!--Animation execution time-->
    <attr name="sv_duration" format="integer" />
    <!--Pointer color-->
    <attr name="pointerColor" format="color" />
</declare-styleable>

After the attribute definition is completed, the defined attribute is obtained by the third construction and the brush is initialized:
Membership variable part:

	private int angle;//Angle value
    private int mRadian;  //Radian value
    private Paint mPaint;
//            mPointerPaint; //Brush
    private int[] mColors = {0xffDF0D30, 0xffF69729, 0xffD4D03B, 0xff3AEC26, 0xff5AFFEF};//Default color of arc segment
    private int mStrokeWidth;// Semicircular Edge Width
    private int mPointerWidth;//Pointer width
    private int currentNum;//Running values in the execution of recorded value animation
    private int duration;//Animation duration
    private int mColor;

public StatisticsView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Statistics_View, defStyle, 0);
    int count = array.getIndexCount();
    for (int i = 0; i < count; i++) {
        int attr = array.getIndex(i);
        if (attr == R.styleable.Statistics_View_radian) {
            mRadian = array.getInt(attr, 180);
        } else if (attr == R.styleable.Statistics_View_strokeWidth) {
            mStrokeWidth = array.getInt(attr, 5);
        } else if (attr == R.styleable.Statistics_View_pointerWidth) {
            mPointerWidth = array.getInt(attr, 5);
        } else if (attr == R.styleable.Statistics_View_sv_duration) {
            duration = array.getInt(attr, 1000);
        } else if (attr == R.styleable.Statistics_View_pointerColor) {
            mColor = array.getColor(attr, getResources().getColor(R.color.color_red));
        }
    }
    array.recycle();
    initPaint();
}

    private void initPaint() {
//        mPointerPaint = new Paint();
//        mPointerPaint.setColor(mColor);
//        mPointerPaint.setAntiAlias(true);
//        mPointerPaint.setStyle(Paint.Style.FILL);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        //Set up a circular edge to fill smoothly
        mPaint.setStyle(Paint.Style.STROKE);
    }

Because we are not sure what form of view width is used, we need to test the usual ways to use view width and height: wrap_content type, match_parent type, fixed value (100dp), or margin and pading values.

Before rewriting, learn about the spec mode of MeasureSpec (test size). There are three types:
EXACTLY: Usually set a clear value or MATCH_PARENT
AT_MOST: Represents that the sublayout is limited to a maximum, usually WARP_CONTENT
UNSPECIFIED: Represents how big the sub-layout is and is rarely used

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int resultWidth;
    // Obtaining mode in width measurement specification
    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
    // Obtaining size in width measurement specifications
    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

    //If it's a clear value or match_parent
    if (modeWidth == MeasureSpec.EXACTLY) {
        //Direct assignment specifies, such as 300 DP
        resultWidth = sizeWidth;
    } else {//If the value is wrap
        // If padding or margin values are set
        resultWidth = getPaddingLeft() + getMeasuredWidth() + getPaddingRight();
        //wrap type
        if (modeWidth == MeasureSpec.AT_MOST) {
            resultWidth = Math.min(resultWidth, sizeWidth);
        }
    }

    int resultHeight;
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
    if (modeHeight == MeasureSpec.EXACTLY) {
        resultHeight = sizeHeight;
    } else {
        resultHeight = getPaddingTop() + sizeHeight + getPaddingBottom();
        if (modeHeight == MeasureSpec.AT_MOST) {
            resultHeight = Math.min(resultHeight, sizeHeight);
        }
    }
    // Measurement
    setMeasuredDimension(resultWidth, resultHeight);
}

After the measurement is completed, the color and semicircle of the arc segment are drawn in onDraw using the drawing board passed to the child class after the initialization of the parent class is completed.

@Override
protected void onDraw(Canvas canvas) {
    //Draw curved lines and connect them into semicircles
    drawSemicircle(canvas);
    //Draw pointer
    drawPointer(canvas);
}
 /**
     * Drawing pointer
     *
     * @param canvas
     */
    private void drawPointer(Canvas canvas) {
        int height = getHeight();
        int width = getWidth();
        //Starting Center Point of Pointer: Take half of the width of x axis as the vertex of the height of Y axis to get the center position of the half circle.
    	float startX = width / 2;
        float startY = height;

        //Set the width and color of the pointer
    	mPaint.setStrokeWidth(mPointerWidth);
        mPaint.setColor(mColor);

        //The X-axis has the width of the arc segment, so the pointer needs three times the width of the arc when it passes, so that the pointer can rotate inside the arc without exceeding the arc range.
//The height of the Y axis is the same as that of the whole circle.
    	float stopX = mStrokeWidth * 3;
        float stopY = height;

        // Set the variable value of the pointer's angle, and change it constantly through the value. rotate makes the pointer rotate along the set range of values
        canvas.rotate(currentNum, startX, startY);
        // Draw pointer
        canvas.drawLine(startX, startY, stopX, stopY, mPaint);

//        Drawing Pointer Points
//        canvas.drawCircle(startX, height, stopX, mPointerPaint);
    }

    /**
     * Draw a semicircle
     *
     * @param canvas
     */
    private void drawSemicircle(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();
        //To avoid the edges being blocked, set the padding of left, top and right to 5
        float padding = 5;
        float left = padding;
        float top = padding;
        float right = width - padding;
        float bottom = height * 2;

        //Set the width of the semicircle
     mPaint.setStrokeWidth(mStrokeWidth);
      //Construct a square with four sides and set the width and height of the square.
        RectF rectF = new RectF(left, top, right, bottom);
        //Set the rendering color of the semi-circular arc segment, dis_move=180/5=36
        int dis_move = mRadian / mColors.length;
      //Drawing and coloring different arc segments according to the number of colors
        for (int i = 0; i < mColors.length; i++) {
            mPaint.setColor(mColors[i]);
      //From the first line segment, rendering begins at the end of each next line and then at the end of the previous line.
      int startAngle = mRadian + (dis_move * i);
 //Draw a semicircle in a square
            canvas.drawArc(rectF, startAngle, dis_move, false, mPaint);
        }
    }

At this point, the semicircle and the pointer will end. Next, the pointer will rotate. Threads can also be used, but using value animation will flow a little.
Principle: Value Animator is a value animation, which can be executed according to the start and end values. It can complete the change between the set start and end values within a set time. It can update the pointer by listening and returning the variable values of these values in the change.

/**
 * Start animation
 *
 * @return
 */
public void startAnim() {
     ValueAnimator intAnimator = new ValueAnimator().ofInt(0, angle);
    intAnimator.setDuration(duration);//Complete the animation in one second
    intAnimator.addUpdateListener(this);
   	intAnimator.start();
}

@Override
public void onAnimationUpdate(ValueAnimator animation) {
//The returned value refreshes the ondraw method asynchronously after conversion
    currentNum = (int) animation.getAnimatedValue();
    StatisticsView.this.postInvalidate();
}

Complete code to achieve half circle and pointer running effect

/**
 * Create by bob on 2018/10/18
 */
public class StatisticsView extends View implements ValueAnimator.AnimatorUpdateListener {

    private int angle;//Angle value
    private int mRadian;  //Radian value
    private Paint mPaint;
//            mPointerPaint; //Brush
    private int[] mColors = {0xffDF0D30, 0xffF69729, 0xffD4D03B, 0xff3AEC26, 0xff5AFFEF};//Default color of arc segment
    private int mStrokeWidth;// Semicircular Edge Width
    private int mPointerWidth;//Pointer width
    private int currentNum;//Running values in the execution of recorded value animation
    private int duration;//Animation duration
    private int mColor;

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

    public StatisticsView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StatisticsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Statistics_View, defStyle, 0);
        int count = array.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = array.getIndex(i);
            if (attr == R.styleable.Statistics_View_radian) {
                mRadian = array.getInt(attr, 180);
            } else if (attr == R.styleable.Statistics_View_strokeWidth) {
                mStrokeWidth = array.getInt(attr, 5);
            } else if (attr == R.styleable.Statistics_View_pointerWidth) {
                mPointerWidth = array.getInt(attr, 5);
            } else if (attr == R.styleable.Statistics_View_sv_duration) {
                duration = array.getInt(attr, 1000);
            } else if (attr == R.styleable.Statistics_View_pointerColor) {
                mColor = array.getColor(attr, getResources().getColor(R.color.color_red));
            }
        }
        array.recycle();
        initPaint();
    }

    private void initPaint() {
//        mPointerPaint = new Paint();
//        mPointerPaint.setColor(mColor);
//        mPointerPaint.setAntiAlias(true);
//        mPointerPaint.setStyle(Paint.Style.FILL);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        //Set up a circular edge to fill smoothly
        mPaint.setStyle(Paint.Style.STROKE);
    }

    /**
     * Set pointer angle value
     *
     * @param value
     * @return
     */
    public StatisticsView setAngleValue(int value) {
        this.angle = value;
        return this;
    }

    /**
     * Color resource array
     *
     * @param colorArrayId
     * @return
     */
    public StatisticsView setColors(int colorArrayId) {
        this.mColors = getContext().getResources().getIntArray(colorArrayId);
        return this;
    }

    /**
     * Setting the animation duration
     *
     * @param duration Millisecond
     * @return
     */
    public StatisticsView setDuration(int duration) {
        this.duration = duration;
        return this;
    }

    /**
     * Set the radian value
     */
    public StatisticsView setRadian(int radian) {
        this.mRadian = radian;
        return this;
    }

//    /**
//     * Set the color of the pointer and the center dot of the pointer
//     *
//     * @param resColor
//     * @return
//     */
//    public StatisticsView setPointerAndArcColor(int resColor) {
//        int color = getResources().getColor(resColor);
//        mPointerPaint.setColor(color);
//        mPaint.setColor(color);
//        return this;
//    }
//
//    /**
//     * Setting the color of the pointer center dot
//     *
//     * @param resColor
//     * @return
//     */
//    public StatisticsView setPointerArcColor(int resColor) {
//        int color = getResources().getColor(resColor);
//        mPointerPaint.setColor(color);
//        return this;
//    }
    /**
     * Setting the pointer color
     *
     * @param resColor
     * @return
     */
    public StatisticsView setPointerColor(int resColor) {
        int color = getResources().getColor(resColor);
        mPaint.setColor(color);
        return this;
    }

    /**
     * Setting the thickness of semicircle
     *
     * @param strokeWidth
     * @return
     */
    public StatisticsView setStrokeWidth(int strokeWidth) {
        this.mStrokeWidth = strokeWidth;
        return this;
    }

    /**
     * Refresh View
     *
     * @return
     */
    public StatisticsView refresh() {
        invalidate();
        return this;
    }

    /**
     * Start animation
     *
     * @return
     */
    public void startAnim() {
        ValueAnimator intAnimator = new ValueAnimator().ofInt(0, angle);//Set start value to end value
        intAnimator.setDuration(duration);//Complete the animation in one second
        intAnimator.addUpdateListener(this);
        intAnimator.start();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        currentNum = (int) animation.getAnimatedValue();
        StatisticsView.this.postInvalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int resultWidth;

        // Obtaining mode in width measurement specification
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        // Obtaining size in width measurement specifications
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

        //If it is a definite value
        if (modeWidth == MeasureSpec.EXACTLY) {
            //Direct assignment specifies, such as 300 DP
            resultWidth = sizeWidth;
        } else {//If the value is wrap or match_parent
            // Setting padding value
            resultWidth = getPaddingLeft() + getMeasuredWidth() + getPaddingRight();
            //wrap type
            if (modeWidth == MeasureSpec.AT_MOST) {
                resultWidth = Math.min(resultWidth, sizeWidth);
            }
        }

        int resultHeight;
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        if (modeHeight == MeasureSpec.EXACTLY) {
            resultHeight = sizeHeight;
        } else {
            resultHeight = getPaddingTop() + sizeHeight + getPaddingBottom();
            if (modeHeight == MeasureSpec.AT_MOST) {
                resultHeight = Math.min(resultHeight, sizeHeight);
            }
        }
        // Measurement
        setMeasuredDimension(resultWidth, resultHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //Draw curved lines and connect them into semicircles
        drawSemicircle(canvas);
        //Draw pointer
        drawPointer(canvas);
    }

    /**
     * Drawing pointer
     *
     * @param canvas
     */
    private void drawPointer(Canvas canvas) {

        int height = getHeight();
        int width = getWidth();

        //Starting Center Point of Pointer: Take half of the width of x axis as the vertex of the height of Y axis to get the center position of the half circle.
        float startX = width / 2;
        float startY = height;

        //Set the width and color of the pointer line
        mPaint.setStrokeWidth(mPointerWidth);
        mPaint.setColor(mColor);

        //Start and End Pointer: Stay at the other vertex of x and Y axis: x axis - width of semicircle
        float stopX = mStrokeWidth * 3;
        float stopY = height;

        // Debugging pointer angle
        canvas.rotate(currentNum, startX, startY);
        // Draw pointer
        canvas.drawLine(startX, startY, stopX, stopY, mPaint);

        //Drawing Pointer Points
       // canvas.drawCircle(startX, height, stopX, mPointerPaint);
    }

    /**
     * Draw a semicircle
     *
     * @param canvas
     */
    private void drawSemicircle(Canvas canvas) {

        int width = getWidth();
        int height = getHeight();
        //To avoid the edges being blocked, set the padding of left, top and right to 5
        float padding = 5;

        float left = padding;
        float top = padding;
        float right = width - padding;
        float bottom = height * 2;

        //Set the width of the semicircle
        mPaint.setStrokeWidth(mStrokeWidth);
        RectF rectF = new RectF(left, top, right, bottom);
        //Set the rendering color of the semi-circular arc segment, dis_move=180/5=36
        int dis_move = mRadian / mColors.length;
        for (int i = 0; i < mColors.length; i++) {
            mPaint.setColor(mColors[i]);//red
            //From the first line segment, rendering begins at the end of each next line and then at the end of the previous line.
            int startAngle = mRadian + (dis_move * i);
            canvas.drawArc(rectF, startAngle, dis_move, false, mPaint);
        }
    }
}

Digital running is also inherited from the TextView to achieve combination, and then use Value Animator to do animation, its principle is simpler than the above, it will not be described one by one, if necessary: Click here to download the source code If it helps you, order a Star.

Posted by newbeee on Fri, 25 Jan 2019 21:27:17 -0800