自定義View
讓我們先從一個簡單的例子入手:
一個簡單的環形進度條,在進度條前端位置加一個白色小圓點,然後還有一個進度加載動畫。
首先我們分析一下這個自定義View包含了以下幾個部分:
- 圓環背景
- 圓環進度
- 白色小圓點
- 加載動畫
大概就只有這幾個部分組成。
第一步:首先我們要新建一個自定義的類繼承系統的View類:
public class CircleProgress extends View {
private static final String TAG = "CircleProgress";
public CircleProgress(Context context) {
super(context);
init();
}
public CircleProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
//初始化操作
}
}
需要注意的是:
1、其中還有一個構造方法
public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
因爲這個構造方法需要至少API21
才能支持,所以我們不使用。
2、在每個構造方法中我們都需要調用init()
方法來初始化我們的view。
3、除了上面代碼中所展示的構造方法的寫法,還有另一種方式:
public class CircleProgress extends View {
public CircleProgress(Context context) {
this(context,null);
}
public CircleProgress(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
}
按照此種方式最終會調用到第三個構造方法,所以我們只需要在第三個構造方法裏調用init()
方法就可以了。
那麼問題來了,現在有兩種實現構造方法的方式,我們應該使用哪種?答案是:使用第一種方式更加穩妥。
使用第二種方式如果我們的自定義View(繼承TextView、ListView
等自定義View)擁有默認的defStyleAttr
時候,就會被我們0所覆蓋,這時候就會導致一系列問題。所以只有當我們能夠完全確認自定義View沒有默認的defStyleAttr
的時候纔可以使用第二種方式。
第二步:初始化自定義View的數據
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
對於我們這個需求來說,我們沒有自定義參數,也沒有其他需要初始化的東西。
如果我們需要自定義的參數:
- 創建自定義View之後,values文件夾下創建
attrs.xml
文件,在該文件中編寫styleable
和item
等標籤元素完成自定義屬性的定義; - 在佈局文件中使用自定義屬性;
- 在自定義View的構造方法中通過
TypedArray
獲取。
如果想要深入的瞭解自定義參數,請參考鴻洋的這篇博客。
1.在attrs
文件中創建自定義屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--CircleProgress的自定義屬性-->
<declare-styleable name="CircleProgress">
<attr name="progress" format="float" />
</declare-styleable>
</resources>
2.在佈局文件中使用自定義屬性
<com.kanlulu.customview.widget.CircleProgress
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
app:progress="75" />
3.在構造方法中獲取屬性值
private void init(Context context, @Nullable AttributeSet attrs) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
creditProgress = typedArray.getFloat(R.styleable.CircleProgress_progress, 0);
typedArray.recycle();
}
}
第三步:重寫onDraw()
方法繪製不同部分
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//TODO getHeight()方法只有在onDraw的時候和之後的方法纔有作用
radius = (getHeight() - backCircleWith) / 2;//半徑
//繪製背景圓環
paintBackCircle(canvas);
//繪製進度
paintProgress(canvas);
//繪製圓點
paintProgressPoint(canvas);
}
這裏有一個需要注意的地方getHeight()
方法獲取的是我們控件的高度,它只有在onDraw()
和之後的onMeasure()
或onlayout()
纔有作用,如果我們在init()
方法中獲取getHeight()
的結果是0。
如上代碼所示,我們分別新建三個方法完成這三部分的繪製:
1.繪製背景圓環
這是一個純粹的圓環沒有其他東西,我們只需要知道圓點和半徑就可以了。
/**
* 背景圓環
*
* @param canvas
*/
private void paintBackCircle(Canvas canvas) {
mPaint.setColor(getResources().getColor(R.color.circleBackColor));
mPaint.setStrokeWidth(backCircleWith);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);
}
2.繪製進度圓度圓環
其實就是繪製一個扇形圓環,對於繪製扇形而言,我們需要一個RectF
對象來確定繪製的範圍,然後我們需要一個扇形的起始位置的角度和扇形的角度,這樣我們就可以以繪製出一個扇形了。
/**
* 畫進度扇形圓環
*
* @param canvas
*/
private void paintProgress(Canvas canvas) {
mPaint.setColor(getResources().getColor(R.color.circleProgressColor));
mPaint.setStrokeWidth(backCircleWith);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
RectF rectF = new RectF(backCircleWith / 2, backCircleWith / 2, 2 * radius + backCircleWith / 2, 2 * radius + backCircleWith / 2);
currentAngle += 5;
if (currentAngle >= sweepAngle) currentAngle = sweepAngle;
canvas.drawArc(rectF, 180, currentAngle, false, mPaint);
}
RectF
的構造方法有四個參數,分別是left、top、right、bottom
,這四個位置限定了我們扇形繪製的範圍,它在Android座標體系中的位置如圖所示:
Android座標體系中關於角度位置的定義如圖所示:
3.繪製進度前端的圓點
這個就是繪製一個點,關鍵要準確的確定點的座標位置,這時候我們需要稍微用到一些簡單的三角函數的知識,我們在園環中現在已知半徑、角度和起始位置,我們就可以確定要繪製的點在圓環上的位置。
/**
* 畫進度前端的圓點
*
* @param canvas
*/
private void paintProgressPoint(Canvas canvas) {
mPaint.setColor(getResources().getColor(R.color.circlePoint));
mPaint.setStrokeWidth(backCircleWith - 8);
mPaint.setStyle(Paint.Style.STROKE);
if (sweepAngle > 359 || sweepAngle <= 0) return;
//每次繪製遞增5個數
currentPointAngle += 5;
if (currentPointAngle >= sweepAngle) currentPointAngle = sweepAngle;
//繪製點的x座標
double x = getWidth() / 2 - radius * Math.cos(currentPointAngle * Math.PI / 180);
//繪製點的y座標
double y = getHeight() / 2 - radius * Math.sin(currentPointAngle * Math.PI / 180);
canvas.drawPoint((float) x, (float) y, mPaint);
}
這些步驟完成後我們就可以看到靜態的進度環了;現在我們需要加上漸進的動畫。
在自定義View中沒有直接的動畫,要想實現動畫效果我們只有通過重新繪製的方法來達到動畫效果invalidate()
;
我們可以每輪比上一次多繪製一點,
//每次繪製遞增5個數
currentAngle += 5;
if (currentAngle >= sweepAngle) currentAngle = sweepAngle;
canvas.drawArc(rectF, 180, currentAngle, false, mPaint);
//在onDraw()裏重新繪製
if (currentAngle != sweepAngle || currentPointAngle != sweepAngle) invalidate();//重新繪製
我們可以不延遲發起重新繪製,因爲繪製一次本身就會耗費數十毫秒不等的時間。我們可以根據實際情況判斷是否要設置延時的時間。
項目地址