自定義View是每個開發者繞不過去的一座山,高山仰止,不管看過多少的技術博客,都需要真正動手敲上一遍才能真正看到高處的風景,今天,趁着業務需求,就順手來寫一個基礎入門的自定義View。初步完成效果如下:
這樣的圖形基本上在頁面頂部會使用到,相對而言使用到的技術點較少,很適合用來做學習項目。現在,讓我們一步一步來拆分者個View吧。
1.分析
這個View我們可以把它分爲三層,第一層爲一個純色矩形,第二層爲從左到右依次排列的多個小矩形,帝三層爲裁切層,即上,左,右三條邊爲直線,下邊爲弧線的特殊圖形。
這麼一分析,我們發現,只需要在2個方法裏進行操作就可以實現我們的所有操作:onMeasure(),onDraw().
onMeasure()用於測量View的寬度和高度,方便後續的繪製。
onDraw()是這個View的重中之重。
2.正式開始編寫代碼
我們先定義自定義View的class文件,繼承View,重寫相關的構造函數(重點是2個參數的構造函數,必須)
2.1 自定義VIew 的屬性
經過第一步的分析,有幾個屬性是需要在佈局裏進行賦值的,這些屬性我們都定義在attr.xml中:
2.2在zidingView中獲取我們的自定義屬性的相關值
現在讓我們回到代碼中,獲取那些自定義的屬性值:
在構造函數中有個AttributeSet對象,這裏就包含了所有我們需要的東西。
以及在onMeasure獲取到當前View寬高(相對而言很簡單):
2.3開始繪製
準備工作已經做好,讓我們來到onDraw()裏進行繪製。
2.3.1 繪製背景矩形
/**
* 繪製一個背景 矩形
*/
private void drawMain() {
// 設置顏色
mPaint.setColor(mainColor);
// 設置填充樣式 爲填滿
mPaint.setStyle(Paint.Style.FILL);
// 規定矩形相對於當前View 上,下,左,右的距離
RectF rect = new RectF();
rect.left = 0;
rect.top = 0;
rect.right = width;
rect.bottom = height;
if (radios == 0f) {
// 無圓角矩形
mCanvas.drawRect(rect, mPaint);
} else {
// 帶圓角矩形
mCanvas.drawRoundRect(rect, radios, radios, mPaint);
}
}
2.3.2 繪製條紋(即多個小矩形):所用到的api跟上述一致,很好理解吧。
private void drwaLine() {
mPaint.setColor(lineColor);
mPaint.setStyle(Paint.Style.FILL);
boolean flag = true;
int i = 0;
while (flag) {
RectF rectF = new RectF();
rectF.left = lineWidth * (2 * i + 1);
rectF.right = lineWidth * (2 * i + 2);
rectF.top = 0;
rectF.bottom = height;
mCanvas.drawRect(rectF, mPaint);
if (lineWidth * (2 * i + 1) > width) {
break;
}
i++;
}
}
2.3.3 繪製不規則圖形,並對原有的圖形進行裁剪 。先看代碼:
這裏我們引入了一個新的對象Path,不規則圖形的使用就是基於它來實現。基於Path我們可以實現很多奇妙的效果,在這裏我們先練一下手。
然後我們在onDraw()裏依次調用這幾個方法:
現在,自定義View已經寫好了,我們去佈局裏使用:
運行結果:(這裏我們指定了圓角爲20dp)
是不是很簡單?
但是可能有人會說:啊,好麻煩,老子畫個圖還要考慮這麼多屬性,就不能直接用一個圖形去裁剪另一個圖形麼?難道Android沒有這樣的api麼?
不要着急啊,少年,繼續向下看:canva.ClipXXX()系列方法就完全可以這樣的效果:
/**
* 真裁剪
*/
private void drawArcRect() {
//從當前位置到目標點(x,y) 用直線連起 即左邊直線
path.lineTo(0, height * multiple);
// 從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1) 即底部曲線
path.quadTo(width / 2, height, width, height * multiple);
// 繪製右邊直線
path.lineTo(width, 0);
// 將圖形封閉,只有當style爲Fill時生效,等同於 path.lineTo(0,0);
path.close();
// 重要 裁剪
mCanvas.clipPath(path);
}
還沒完呢,裁剪的操作比較奇妙,需要我們在裁剪之前進行視圖的保存,在裁剪,繪製之後進行視圖的恢復,因此,我們改變了onDraw()裏圖層的繪製順序:
到了這一步,我們的View就算是大功告成了!
附完整的代碼:
package com.zyp.kotlin.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.zyp.kotlin.R;
/**
* 雙色 條紋 view/可只使用單個顏色
* Created by zhangyanpeng on 2020/5/28
*/
public class AnnieTwoLineView extends View {
private Context context;
/**
* 控件寬高
*/
private int height, width;
/**
* 背景顏色
*/
@SuppressLint("ResourceAsColor")
private int mainColor;
/**
* 條紋顏色
*/
@SuppressLint("ResourceAsColor")
private int lineColor;
/**
* 條紋寬度 px
*/
private float lineWidth;
/**
* 圓角
*/
private float radios;
private Canvas mCanvas;
private Paint mPaint = new Paint();
private Path path = new Path();
public AnnieTwoLineView(Context context) {
this(context, null);
}
public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
this.context = context;
}
public AnnieTwoLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AnnieTwoLineView);
lineWidth = typedArray.getDimension(R.styleable.AnnieTwoLineView_lineWidth, 0f);
mainColor = typedArray.getColor(R.styleable.AnnieTwoLineView_mainColor, getResources().getColor(R.color.colorWhite));
lineColor = typedArray.getColor(R.styleable.AnnieTwoLineView_lineColor, getResources().getColor(R.color.colorAccent));
radios = typedArray.getDimension(R.styleable.AnnieTwoLineView_rectRadios, 0);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 測量寬度和高度
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
// 方便生效
setMeasuredDimension(width, height);
}
/**
* 繪製一個背景 矩形
*/
private void drawMain() {
// 設置顏色
mPaint.setColor(mainColor);
// 設置填充樣式 爲填滿
mPaint.setStyle(Paint.Style.FILL);
// 規定矩形相對於當前View 上,下,左,右的距離
RectF rect = new RectF();
rect.left = 0;
rect.top = 0;
rect.right = width;
rect.bottom = height;
if (radios == 0f) {
// 無圓角矩形
mCanvas.drawRect(rect, mPaint);
} else {
// 帶圓角矩形
mCanvas.drawRoundRect(rect, radios, radios, mPaint);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
this.mCanvas = canvas;
// 圖像保存
canvas.save();
drawArcRect();
drawMain();
if (lineWidth > 0) {
drwaLine();
}
// 圖像恢復
canvas.restore();
}
private void drwaLine() {
mPaint.setColor(lineColor);
mPaint.setStyle(Paint.Style.FILL);
boolean flag = true;
int i = 0;
while (flag) {
RectF rectF = new RectF();
rectF.left = lineWidth * (2 * i + 1);
rectF.right = lineWidth * (2 * i + 2);
rectF.top = 0;
rectF.bottom = height;
mCanvas.drawRect(rectF, mPaint);
if (lineWidth * (2 * i + 1) > width) {
break;
}
i++;
}
}
/**
* 繪製下邊界爲弧線的矩形
*/
private float multiple = 0.7f;
/**
* 真裁剪
*/
private void drawArcRect() {
//從當前位置到目標點(x,y) 用直線連起 即左邊直線
path.lineTo(0, height * multiple);
// 從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1) 即底部曲線
path.quadTo(width / 2, height, width, height * multiple);
// 繪製右邊直線
path.lineTo(width, 0);
// 將圖形封閉,只有當style爲Fill時生效,等同於 path.lineTo(0,0);
path.close();
// 重要 裁剪
mCanvas.clipPath(path);
}
/**
* 第一種“裁剪方案”假裁剪
* @param dpValue
* @return
*/
// private void drawArcRect() {
//// 重要,此顏色可以作爲自定義View的屬性進行,方便與佈局的背景協調
// mPaint.setColor(Color.WHITE);
// mPaint.setStyle(Paint.Style.FILL);
// //從當前位置到目標點(x,y) 用直線連起 即左邊直線
// path.lineTo(0, height * multiple);
//// 從當前位置到目標點(x2,y2)做一條貝塞爾曲線,控制點爲(x1,y1) 即底部曲線
// path.quadTo(width / 2, height, width, height * multiple);
//// 繪製右邊直線
// path.lineTo(width, 0);
//// 將圖形封閉,只有當style爲Fill時生效,等同於 path.lineTo(0,0);
// path.close();
//// 重要,設置當前View與之前圖形的交疊之後的顯示方式
// path.setFillType(Path.FillType.INVERSE_WINDING);
//// 重要 繪製
// mCanvas.drawPath(path,mPaint);
// }
private float dp2px(float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (dpValue - 0.5f) * scale;
}
}