Android 自定义view之圆盘进度条

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



嗯,算是基本满足需求吧。
本文包含的知识点
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: [email protected]
 * 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


<resources>
    <declare-styleable name="WordView">
        <attr name="progressColor" format="color" />
        <attr name="wordTitleColor" format="color" />
        <attr name="wordViewBackground" format="color" />
    declare-styleable>

resources>

布局文件
activity_main.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/details_top_bg"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <EditText
        android:id="@+id/et1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="已学习生词数"
        android:inputType="number" />

    <EditText
        android:id="@+id/et2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="未学习生词数"
        android:inputType="number" />

    <Button
        android:id="@+id/bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="绘制" />

    <com.example.myapplication.WordView
        android:id="@+id/customView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:progressColor="#ffffff"
        app:wordTitleColor="#ffffff"
        app:wordViewBackground="#61000000" />

LinearLayout>

屏幕测量工具
MeasureUtil.java

public class MeasureUtil {
    public static int getScreenWidth(Context mContext) {
        int width = mContext.getResources().getDisplayMetrics().widthPixels;
        return width;
    }

    public static int getScreenHeight(Context mContext) {
        int height = mContext.getResources().getDisplayMetrics().heightPixels;
        return height;
    }
}

使用
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。关于PorterDuff.Mode,可以查看我的下一篇文章。 Android图像合成模式之PorterDuff.Mode


你可能感兴趣的:(Android)