在自定義控件(一)中呢,大家已經對自定義控件有了一個基本的認識,今天就和大家分享一下如何自定義類繼承view來實現我們的功能。
需求
效果圖如上圖所示,要求如下:
- 背景顏色從上到下由深變淺。
- 小黃點的數量從上到下由多到少。
- 小黃點的顏色大小隨機。
- 該控件可分爲5個等級,最佳爲背景全白,沒有小黃點。最嚴重爲顏色最深,小黃點最多。
好,需求就是這個樣子。看到這裏呢,希望小夥伴們先不要着急往下看,可以設身處地的想一想,如果是自己做的話,要怎樣實現。
實現方案
接下來跟大家分享一下我的實現方案。看到這個需求我想到的方案有兩個。
- 把每一顆牙齒作爲一個單位,我們去控制單個牙齒的背景色和小黃點的數量。然後繪製出四顆牙齒,按順序擺放。優點是可以比較精確的控制每顆牙齒的顯示情況。缺點是每顆牙齒都要去繪製,需要自己去用畫筆滑出牙齒的輪廓,背景和小黃點。
- 把四顆牙齒作爲一個單位,分成上下兩層。上層是一張中間透明,兩邊是白色的圖片。下層是一個從上到下顏色由深到錢,小黃點由少到多的自定義view。通過繪製不同的背景層,顯示我們要的效果。優點是隻需要繪製背景即可實現我們要的功能,不需要去分別繪製每顆牙齒的輪廓。鑑於我們的需求,不需要精確到每顆牙齒,這裏我們選擇第二種實現方案。
具體實現
我們先梳理一下第二種實現方式需要用到的知識點
1.自定義類繼承view實現自定義控件的步驟。
2.如何去測量控件的大小。
3.畫筆的使用。
好,接下來我們來按步驟實現這個控件。
初始化
/**
* 定義兩種模式,平均和漸變
*/
public enum MODE {
AVERAGE, SHADE
}
public Chart(Context context) {
super(context);
init();
}
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Chart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
random = new Random();
isNull = false;
isBig = false;
deltaX = 20;
deltaY = 30;
bgAlpha = 50;
sizeY = 1;
floatY = 10;
de = 3;
mode = MODE.AVERAGE;
}
爲了讓小黃點從上到下由少到多的顯示,我是這樣實現的,隨機生成一個0-20的初始Y軸座標,然後繪製每列的小黃點。小黃點的間距是逐漸減小的,這樣小黃點就會越來越密集。而X的座標是一個初始值加上一個隨機值,這樣繪製出來的小黃點每兩列間距也是隨機的。顯示效果會比較好看。
在初始化的方法裏,我們初始化了一些x軸和y軸的初始數據。同時我定義了兩種模式,一種是小黃點均勻分佈,兩一種是逐漸增多。
測量
系統幫我們測量的高度和寬度都是MATCH_PARNET,當我們設置明確的寬度和高度時,系統幫我們測量的結果就是我們設置的結果,當我們設置爲WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。
所以,當設置了WRAP_CONTENT時,我們需要自己進行測量,即重寫onMesure方法”:
重寫之前先了解MeasureSpec的specMode,一共三種類型:
EXACTLY:一般是設置了明確的值或者是MATCH_PARENT
AT_MOST:表示子佈局限制在一個最大值內,一般爲WARP_CONTENT
UNSPECIFIED:表示子佈局想要多大就多大,很少使用
下面是測量的方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMeasuredLength(widthMeasureSpec, true, this), getMeasuredLength(heightMeasureSpec, false, this));
}
/**
* 根據佈局模式計算寬高
*
* @param measureSpec
* @param isWidth
* @return
*/
public static int getMeasuredLength(int measureSpec, boolean isWidth, View view) {
int result;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
if (specMode == View.MeasureSpec.EXACTLY) {
result = specSize;
} else {
if (isWidth) {
result = view.getPaddingLeft() + view.getPaddingRight();
} else {
result = view.getPaddingTop() + view.getPaddingBottom();
}
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
繪製
@Override
protected void onDraw(Canvas canvas) {
//isNull爲true,背景爲純白色,不顯示牙菌斑
if (isNull) {
//填充背景顏色
mPaint.setColor(0xffffffff);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
} else {
//填充背景顏色
mPaint.setColor(0xffFEF8ED);
//繪製點
// int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
LinearGradient lg = new LinearGradient(0, 0, 0, canvasHeight, 0x7ff1b351, 0xfff1b351, Shader.TileMode.MIRROR);
mPaint.setShader(lg);
mPaint.setAlpha(bgAlpha);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
int x = 0;
//繪製Cap爲ROUND的點
mPaint.setStrokeCap(Paint.Cap.ROUND);
switch (mode) {
case AVERAGE:
for (int i = 0; i < 80; i++) {
int startX = deltaX + random.nextInt(10);//x每次增加的距離
int startY = 0 + random.nextInt(floatY);
x = x + startX;
int y = startY;
for (int j = 0; j < 80; j++) {
int alpha = 70 + random.nextInt(100);//隨機生成透明度的點
int size = 5 + random.nextInt(5);//設置線寬,如果不設置線寬,無法繪製點
mPaint.setStrokeWidth(size);
mPaint.setAlpha(alpha);
mPaint.setShader(null);//清空漸變色
canvas.drawPoint(x, y + deltaY, mPaint);
y = y + deltaY;
}
}
break;
case SHADE:
for (int i = 0; i < 80; i++) {
int startX = deltaX + random.nextInt(10);//x每次增加的距離
int startY = 0 + random.nextInt(floatY);
x = x + startX;
int deltay = deltaY;
int y = startY;
boolean isFirst = true;
// Log.e(TAG, "" + startY);
for (int j = 0; j < 80; j++) {
int alpha = 70 + random.nextInt(100);//隨機生成透明度的點
int size;
if (isBig) {
size = (int) ((5 + random.nextInt(10)) * de / 3);
} else {
size = (int) ((3 + random.nextInt(7)) * de / 3);//設置線寬,如果不設置線寬,無法繪製點
}
mPaint.setStrokeWidth(size);
mPaint.setAlpha(alpha);
if (deltay < 15) {
deltay = 20;
}
if (isFirst) {
canvas.drawPoint(x, y, mPaint);
isFirst = false;
} else {
canvas.drawPoint(x, y + deltay, mPaint);
y = y + deltay;
}
deltay -= sizeY;
}
}
break;
}
}
}
在這裏,我們給先給畫筆設置一個線性隨機色,繪製背景。
然後設置設置畫筆粗細爲隨機,透明度隨機,來繪製小黃點。爲了使小黃點在不同分辨率上顯示出相同的效果,它的像素值我們來根據手機的分辨率去計算。
public int getDis(Activity activity) {
DisplayMetrics metric = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metric);
int width = metric.widthPixels; // 屏幕寬度(像素)
int height = metric.heightPixels; // 屏幕高度(像素)
float density = metric.density; // 屏幕密度(0.75 / 1.0 / 1.5 / 2.0)
int densityDpi = metric.densityDpi; // 屏幕密度DPI(120 / 160 / 240 / 320)
if (density < 1.0) {
return -1;
} else if (density == 1.0) {
return 0;
} else if (density == 1.5) {
return 1;
} else {
return 2;
}
}
引用
到這裏這個控件基本上就寫完了,然後我們需要給外部提供一個方法,去設置這個控件的不同狀態。
/**
* 根據不同的時間,顯示不同的背景(小圖)
*
* @param time
*/
public void setChartLittele(int time, float de) {
this.de = de;
if (time >= 400) {
setBg(true, 0, 0, 0, 0, 0, Chart.MODE.SHADE);
} else if (time >= 320 && time < 400) {
setBg(false, 26, 36, 1, 20, 10, Chart.MODE.SHADE);
} else if (time >= 240 && time < 320) {
setBg(false, 22, 32, 1, 20, 20, Chart.MODE.SHADE);
} else if (time >= 160 && time < 240) {
setBg(false, 18, 28, 1, 20, 30, Chart.MODE.SHADE);
} else if (time >= 80 && time < 160) {
setBg(false, 14, 24, 1, 20, 40, Chart.MODE.SHADE);
} else if (time >= 0 && time < 80) {
setBg(false, 10, 20, 1, 20, 50, Chart.MODE.SHADE);
}
}
/**
* 設置背景牙菌斑的密集程度
*
* @param deltaX x軸每次往後移動的距離(隨機)
* @param deltaY y軸每次往下移動的距離
* @param sizeY 漸變模式y每次減少的距離
* @param floatY Y軸開始的隨機座標
* @param bgAlpha 背景漸變色的透明度
* @param mode 模式
*/
public void setBg(boolean isNull, int deltaX, int deltaY, int sizeY, int floatY, int bgAlpha, MODE mode) {
this.isNull = isNull;
this.deltaX = deltaX;
this.deltaY = deltaY;
this.sizeY = sizeY;
this.floatY = floatY;
this.bgAlpha = bgAlpha;
this.mode = mode;
invalidate();
}
看一下佈局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:gravity="center" >
<FrameLayout
android:layout_width="115dp"
android:layout_height="115dp" >
<com.oracleen.view.Chart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/check_right_second" />
</FrameLayout>
</LinearLayout>
這裏我們用了一個framelayout,上層是一張鏤空的圖片,下層是我們繪製的背景。
然後在activity中設置背景的顯示級別。
chart = (Chart) findViewById(R.id.chart);
chart.setChartLittele(300, getDis(this));
OK,搞定。demo鏈接。
demo
下一章我會和大家分享一下組合view的實現,把今天實現的view和前景圖片做成一個控件,添加一些屬性,使用起來會更方便一些。