欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 自定义view之圆盘进度条

程序员文章站 2022-03-31 10:29:21
很久没有用到自定义View了,手有点生疏了,这不同事刚扔给一个活,按照UI的要求,画一个进度条,带动画效果的。需求是这样的: 嗯,实现后效果如下: 嗯,算是基本满足...

很久没有用到自定义View了,手有点生疏了,这不同事刚扔给一个活,按照UI的要求,画一个进度条,带动画效果的。需求是这样的:Android 自定义view之圆盘进度条
嗯,实现后效果如下:

Android 自定义view之圆盘进度条

嗯,算是基本满足需求吧。
本文包含的知识点
1、自定义view的绘制
2、属性动画
3、图像的合成模式 PorterDuff.Mode

嗯,废话不多说,show me the code
1)WordView.java

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.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Xfermode;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Author: gongwq on 2017/12/20 002011.
 * Email: gwq_0111@163.com
 * Descriptions:
 */

public class WordView extends View {
    private Paint linePaint, circlePaint, circlePaintBg;
    private Paint textPaint1, textPaint2, textPaint3, textPaint4;
    private int screenWidth, screenHeight;
    private int mCenter, mRadius;
    private RectF mRectF, mRectFBg1, mRectFBg2;
    private int defaultValue;
    private float rate = 0f;
    private String hasStudyText = "";
    private String notStudyText = "";
    private int startAng = 0;
    private int defaultStartAng = 120;
    private long animDuration = 2000L;
    private int progressColor, wordTitleColor, wordViewBackground;
    private Xfermode xfermode;

    public WordView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray ta = context.obtainStyledAttributes(
                attrs, R.styleable.WordView);

        progressColor = ta.getColor(R.styleable.WordView_progressColor, 0xFFFFFFFF);
        wordViewBackground = ta.getColor(R.styleable.WordView_wordViewBackground, 0xFFFF0000);
        wordTitleColor = ta.getColor(R.styleable.WordView_wordTitleColor, 0xFFFF00FF);
        ta.recycle();
        initPaint(context);
    }

    private void initPaint(Context context) {
        screenWidth = MeasureUtil.getScreenWidth(context);
        screenHeight = MeasureUtil.getScreenHeight(context);
        linePaint = new Paint();
        linePaint.setColor(Color.WHITE);
        linePaint.setStyle(Paint.Style.FILL);
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(6.0f);

        circlePaint = new Paint();
        circlePaint.setColor(progressColor);
        circlePaint.setAntiAlias(true);
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeCap(Paint.Cap.ROUND);
        circlePaint.setStrokeWidth(6.0f);

        circlePaintBg = new Paint();
        circlePaintBg.setColor(wordViewBackground);
        circlePaintBg.setAntiAlias(true);
        circlePaintBg.setStyle(Paint.Style.FILL);

        textPaint1 = new Paint();//已学习生词
        textPaint1.setColor(wordTitleColor);
        textPaint1.setTextAlign(Paint.Align.CENTER);
        textPaint1.setAntiAlias(true);
        textPaint1.setTextSize(25);

        textPaint2 = new Paint();//xx个
        textPaint2.setColor(Color.WHITE);
        textPaint2.setTextAlign(Paint.Align.CENTER);
        textPaint2.setAntiAlias(true);
        textPaint2.setTextSize(70);

        textPaint4 = new Paint();//个
        textPaint4.setColor(Color.WHITE);
        textPaint4.setTextAlign(Paint.Align.CENTER);
        textPaint4.setAntiAlias(true);
        textPaint4.setTextSize(25);

        textPaint3 = new Paint();//未学习生词x个
        textPaint3.setColor(wordTitleColor);
        textPaint3.setTextAlign(Paint.Align.CENTER);
        textPaint3.setAntiAlias(true);
        textPaint3.setTextSize(25);

        mCenter = screenWidth / 2;
        mRadius = screenWidth / 4;

        mRectF = new RectF(10, 10, 2 * mRadius - 10, 2 * mRadius - 10);//外切圆弧

        mRectFBg1 = new RectF(20, 20, 2 * mRadius - 20, 2 * mRadius - 20);//中间的背景

        mRectFBg2 = new RectF(60, 2 * mRadius - 60, 2 * mRadius - 60, 2 * mRadius + 60);//通过这个去调下面“缺”的弧度
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);//DST_OUT在不相交的地方绘制目标图像,相交处根据源图像alpha进行过滤,完全不透明处则完全过滤,完全透明则不过滤
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawArc(mRectF, startAng, (360 - (startAng - 90) * 2) * rate, false, circlePaint);
        //将绘制操作保存到新的图层,将图像合成的处理放到离屏缓存中进行
        circlePaintBg.setColor(wordViewBackground);
        int saveCount = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
        canvas.drawArc(mRectFBg1, 0, 360, true, circlePaintBg);//绘制目标图
        circlePaintBg.setXfermode(xfermode);//设置混合模式

        circlePaintBg.setColor(Color.parseColor("#FF00FF00"));//这里的颜色只需前面的透明值为FF即完全不透明即可
        canvas.drawArc(mRectFBg2, 0, 360, true, circlePaintBg);//绘制源图
        circlePaintBg.setXfermode(null);//清除混合模式
        canvas.restoreToCount(saveCount);

        canvas.drawText("已学习生词", mRadius, mRectF.top + 100, textPaint1);
        float width = textPaint2.measureText(hasStudyText);
        float width2 = textPaint4.measureText("个");
        //   float total = mRectFBg1.right - mRectFBg1.left;
        //  Log.e("文字宽度---->", width + "  " + width2 + "  " + total);
        // canvas.drawText(hasStudyText, mRadius + 30, mRadius, textPaint2);
        float center1 = mRadius - (width + width2) / 2 + width / 2;
        float center2 = mRadius - (width + width2) / 2 + width + width2 / 2;
        canvas.drawText(hasStudyText, center1, mRadius + 20, textPaint2);

        canvas.drawText(notStudyText, mRadius, mRectF.bottom - 100, textPaint3);
        if (startAng != 0) {
            canvas.drawText("个", center2, mRadius + 20, textPaint4);
            canvas.rotate(180 + (startAng - 90) + (360 - (startAng - 90) * 2) * rate, mRadius, mRadius);
            if (Integer.parseInt(hasStudyText) > 0) {
                canvas.drawLine(mRadius, mRectF.top, mRadius, mRectF.top + 20, linePaint);
            }
        }
    }

    /**
     * 设置学习单词数
     *
     * @param hasStudyNum 已学习单词数
     * @param notStudyNum 未学习单词数
     */
    public void setWordsNum(final int hasStudyNum, final int notStudyNum) {
        rate = (float) hasStudyNum / (float) (hasStudyNum + notStudyNum);
        final float rate2 = rate;

        startAng = getStartAng();//startAng必须为大于等于90,小于180
        ValueAnimator anim = ValueAnimator.ofFloat(1, 100);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                rate = (float) animation.getAnimatedValue() * rate2 / 100f;
                hasStudyText = (int) ((float) animation.getAnimatedValue() * hasStudyNum / 100) + "";
                notStudyText = "未学习生词" + (int) ((float) animation.getAnimatedValue() * notStudyNum / 100) + "个";
                postInvalidate();
            }
        });
        //  hasStudyText = hasStudyNum + "";
        //  notStudyText = "未学习生词" + notStudyNum+"个";
        anim.setDuration(getAnimationDuration());
        anim.start();

    }

    /**
     * 设置圆弧的起始角度值  
注 1.值必须是[90,180] 2.必须在setWordNum()方法之前调用
     * @param ang
     */
    public void setStartAng(int ang) {
        this.startAng = ang;
    }

    public int getStartAng() {
        if (startAng == 0) {
            return defaultStartAng;
        }
        return startAng;
    }

    /**
     * 设置动画时间 ,注意 需要在setWordsNum前调用才会生效
     *
     * @param time
     */
    public void setAnimationDuration(long time) {
        this.animDuration = time;
    }

    public long getAnimationDuration(){
        return animDuration;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    private int measureWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        // 默认宽高;
        defaultValue = screenWidth;

        switch (mode) {
            case MeasureSpec.AT_MOST:
                // 最大值模式 当控件的layout_Width或layout_height属性指定为wrap_content时
               // Log.e("cmos---->", "size " + size + " screenWidth " + screenWidth);
                //  size = Math.min(defaultValue, size);
                size = screenWidth / 2;
                defaultValue = size;
                break;
            case MeasureSpec.EXACTLY:
                // 精确值模式
                // 当控件的android:layout_width=”100dp”或android:layout_height=”match_parent”时

                break;
            default:
                size = defaultValue;
                break;
        }

        return size;
    }

    private int measureHeight(int heightMeasureSpec) {
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        switch (mode) {
            case MeasureSpec.AT_MOST:
                // 最大值模式 当控件的layout_Width或layout_height属性指定为wrap_content时
                Log.e("cmos---->", "size " + size + " screenHeight " + screenHeight);
                //   size = Math.min(screenHeight / 2, size);
                size = defaultValue;
                break;
            case MeasureSpec.EXACTLY:
                // 精确值模式
                // 当控件的android:layout_width=”100dp”或android:layout_height=”match_parent”时

                break;
            default:
                size = defaultValue;
                break;
        }
        return size;
    }
}

下面是他的一些配置
attrs.xml



    
        
        
        
    

布局文件
activity_main.xml




    

    

    

使用
MainActivity.java

 ......
 @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bt:
                if (!TextUtils.isEmpty(et1.getText().toString()) && !TextUtils.isEmpty(et2.getText().toString())) {
                    int num1 = Integer.parseInt(et1.getText().toString());
                    int num2 = Integer.parseInt(et2.getText().toString());
                    wordView.setAnimationDuration(4000);
                    wordView.setStartAng(130);
                    wordView.setWordsNum(num1, num2);
                } else {
                    Toast.makeText(this, "数值不能为空", Toast.LENGTH_SHORT).show();
                }

                break;
        }
    }
    ......

代码里注释已经相对较清晰了,就不做解释了,有不懂的可以留言。
整个view的重点就在onDraw()方法里,怎么去放置文字,中间的“xxx个”怎么随着数字的长度变化而始终居中,这主要与initPaint()画笔方法有关,其中textPaint.setTextAlign(Paint.Align.CENTER);是重点,它表示画的文字,你后面给定他一个绘制的中心点,然后它的文字会自动居中。第二个要注意的地方是,中间的背景,我是画的,开始准备用UI给的背景的,但是发现不好适配,所以就自己画了,这里主要用到的是图像合成模式PorterDuff.Mode,图像的合成模式的枚举类一共有16种,通过这16种模式,我们可以自己根据给定的2个图片,合成我们想要的结果。这也给我们一个启示:当需求中的图片可以看成是多个图片组合成的结果的画,不妨可以试试 图像的合成模式PorterDuff.Mode。