1. 程式人生 > >Android 自定義view之圓盤進度條

Android 自定義view之圓盤進度條

很久沒有用到自定義View了,手有點生疏了,這不同事剛扔給一個活,按照UI的要求,畫一個進度條,帶動畫效果的。需求是這樣的:這裡寫圖片描述
嗯,實現後效果如下:

這裡寫圖片描述

嗯,算是基本滿足需求吧。
本文包含的知識點
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(); } /** * 設定圓弧的起始角度值 <br>注 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

<?xml version="1.0" encoding="utf-8"?>
<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

<?xml version="1.0" encoding="utf-8"?>
<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