Android 自定義view之圓盤進度條
阿新 • • 發佈:2019-02-20
很久沒有用到自定義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