Android自定義View實(shí)現(xiàn)繪制水波浪溫度刻度表
前言
之前的繪制圓環(huán),我們了解了如何繪制想要的形狀和進(jìn)度的一些特點(diǎn),那么此篇文章我們更近一步,繪制一個(gè)稍微復(fù)雜一點(diǎn)的刻度與波浪。來一起復(fù)習(xí)一下Android的繪制。
相對應(yīng)的這種類型的自定義View網(wǎng)上并不少見,但是如果我們要做一些個(gè)性化的效果,最好還是自己繪制一份,也相對的比較容易控制效果,如果想實(shí)現(xiàn)上面的效果,我們一般來說分為以下幾個(gè)步驟:
- 重寫測量方法,確保它是一個(gè)正方形
- 繪制刻度
- 繪制中心的圓與文字
- 水波紋的動(dòng)畫
- 設(shè)置進(jìn)度與動(dòng)畫,一起動(dòng)起來
思路我們已經(jīng)有了,下面一步一步的來實(shí)現(xiàn)吧。
話不多說,Let's go
1、onMeasure重新測量
之前的圓環(huán)進(jìn)度,我們并沒有重寫 onMeasure 方法,而是在布局中指定為固定的寬高,其實(shí)兼容性和健壯性并不好,萬一寫錯(cuò)了就會(huì)變形導(dǎo)致顯示異常。
最好的辦法是不管xml中設(shè)置為什么值,這里都能保證為一個(gè)正方形,要么是取寬度為準(zhǔn),讓高度和寬度一致,要么就是寬度高度取最大值,讓他們保持一致。由于我們是豎屏的應(yīng)用,所以我就取寬度為準(zhǔn),讓高度和寬度一致。
前面我們只是講了 onDraw 并沒有講到 onMeasure , 這里簡單的說一下。
我們?yōu)槭裁匆貙?onMeasure ?
- 為了自定義View尺寸的規(guī)則,如果你的自定義View的尺寸是根據(jù)父控件行為一致,就不需要重寫onMeasure()方法。
- 如果不重寫onMeasure方法,那么自定義view的尺寸默認(rèn)就和父控件一樣大小,當(dāng)然也可以在布局文件里面寫死寬高,而重寫該方法可以根據(jù)自己的需求設(shè)置自定義view大小。
一般來說我們重寫的 onMeasure 長這樣:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec)
}widthMeasureSpec ,heightMeasureSpec 并不是真正的寬高,看名字就知道,它只是寬高測量的規(guī)格,我們通過 MeasureSpec 的一些靜態(tài)方法,通過它們拿到一些信息。
static int getMode(int measureSpec):根據(jù)提供的測量值(規(guī)格)提取模式(上述三個(gè)模式之一)
測量的 Model 一共有三種
- UNSPECIFIED(未指定),父元素部隊(duì)自元素施加任何束縛,子元素可以得到任意想要的大小;
- EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大??;
- AT_MOST(至多),子元素至多達(dá)到指定大小的值。
我們常用的就是 EXACTLY 和 AT_MOST ,EXACTLY 對應(yīng)的就是我們設(shè)置的match_parent或者300這樣的精確值,而 AT_MOST 對應(yīng)的就是wrap_content。
static int getSize(int measureSpec):根據(jù)提供的測量值(規(guī)格)提取大小值(這個(gè)大小也就是我們通常所說的大小)
通過此方法就能獲取控件的寬度和高度值。
static int makeMeasureSpec(int size,int mode):根據(jù)提供的大小值和模式創(chuàng)建一個(gè)測量值(規(guī)格)
通過具體的寬高和model,創(chuàng)建對應(yīng)的寬高測量規(guī)格,用于確定View的測量
而 onMeasure 的最終設(shè)置確定寬度的測量有兩種方式,
- setMeasuredDimension(width, height)
- super.onMeasure(widthMeasureSpec,heightMeasureSpec)
實(shí)戰(zhàn):
比如我們的自定義溫度刻度View,我們整個(gè)View要確保一個(gè)正方形,那么就拿到寬度,設(shè)置同樣的高度,然后確定測量,流程如下:
//重新測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取控件的寬度,高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int newWidthMeasureSpec = widthMeasureSpec;
//如果沒有指定寬度,默認(rèn)給200寬度
if (widthMode != MeasureSpec.EXACTLY) {
newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
}
//獲取到最新的寬度
int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();
//我們要的是矩形,不管高度是多高,讓它總是和寬度一致
int height = width;
centerPosition.x = width / 2;
centerPosition.y = height / 2;
radius = width / 2f;
mRectF.set(0f, 0f, width, height);
//最后設(shè)置生效-下面兩種方式都可以
// setMeasuredDimension(width, height);
super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
}這里有詳細(xì)的注釋,大致實(shí)現(xiàn)的效果如下:

2、繪制刻度
由于原本的 Canvas 內(nèi)部沒有繪制刻度這么一說,所以我們只能用繪制線條的方式,就是 drawLine 方法。
為了了解到坐標(biāo)系和方便實(shí)現(xiàn),我們可以先繪制一個(gè)圓環(huán),定位我們刻度需要繪制的位置。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//畫圓環(huán)
canvas.drawArc(
mRectF.left + 2f, mRectF.top + 2f, mRectF.right - 2f, mRectF.bottom - 2f,
mStartAngle, mSweepAngle, false, mDegreeCirPaint
);
}這個(gè)圓環(huán)是之前講到過了,就不過多贅述了,實(shí)現(xiàn)效果如下:

由于開始繪制的地方在左上角位置,我們要移動(dòng)到圓的中心點(diǎn)開始繪制,也就是紅色點(diǎn)移動(dòng)到藍(lán)色點(diǎn)。
我們就需要x軸和y軸做一下偏移 canvas.translate(radius, radius);
默認(rèn)的 drawLine 都是橫向繪制,我們想要實(shí)現(xiàn)效果圖的效果,就需要旋轉(zhuǎn)一下畫筆,也就是用到 canvas.rotate(rotateAngle);
那么旋轉(zhuǎn)多少了,如果說最底部是90度,我們的起始角度是120度開始的,我們就起始旋轉(zhuǎn)30度。后面每一次旋轉(zhuǎn)就按照百分比來,比如我們100度的溫度,那么就相當(dāng)于要畫100個(gè)刻度,我們就用需要繪制的角度除以100,就是每一個(gè)刻度的角度。
具體的刻度實(shí)現(xiàn)代碼:
private float mStartAngle = 120f; // 圓弧的起始角度
private float mSweepAngle = 300f; //繪制的起始角度和滑過角度(繪制300度)
private float mTargetAngle = 300f; //刻度的角度(根據(jù)此計(jì)算需要繪制有色的進(jìn)度)
private void drawDegreeLine(Canvas canvas) {
//先保存
canvas.save();
// 移動(dòng)畫布
canvas.translate(radius, radius);
// 旋轉(zhuǎn)坐標(biāo)系,需要確定旋轉(zhuǎn)角度
canvas.rotate(30);
// 每次旋轉(zhuǎn)的角度
float rotateAngle = mSweepAngle / 100;
// 累計(jì)疊加的角度
float currentAngle = 0;
for (int i = 0; i <= 100; i++) {
if (currentAngle <= mTargetAngle && mTargetAngle != 0) {
// 計(jì)算累計(jì)劃過的刻度百分比
float percent = currentAngle / mSweepAngle;
//動(dòng)態(tài)的設(shè)置顏色
mDegreelinePaint.setColor(evaluateColor(percent, Color.GREEN, Color.RED));
canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);
// 畫過的角度進(jìn)行疊加
currentAngle += rotateAngle;
} else {
mDegreelinePaint.setColor(Color.WHITE);
canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);
}
//畫完一個(gè)刻度就要旋轉(zhuǎn)移動(dòng)位置
canvas.rotate(rotateAngle);
}
//再恢復(fù)
canvas.restore();
}加上圓環(huán)與刻度的效果圖:

3. 設(shè)置刻度動(dòng)畫
前面的一篇我們使用的是屬性動(dòng)畫不停的繪制從而實(shí)現(xiàn)進(jìn)度的效果,那么這一次我們使用定時(shí)任務(wù)的方式也是可以實(shí)現(xiàn)動(dòng)畫的效果。
由于我們之前的 drawDegreeLine 方法內(nèi)部控制繪制進(jìn)度的變量就是 targetAngle 來控制的,所以我們通過入口方法設(shè)置溫度的時(shí)候通過定時(shí)任務(wù)的方式來控制。
代碼如下:
//動(dòng)畫狀態(tài)
private boolean isAnimRunning;
// 手動(dòng)實(shí)現(xiàn)越來越慢的效果
private int[] slow = {10, 10, 10, 8, 8, 8, 6, 6, 6, 6, 4, 4, 4, 4, 2};
// 動(dòng)畫的下標(biāo)
private int goIndex = 0;
//設(shè)置溫度,入口的開始
public void setupTemperature(float temperature) {
mCurPercent = 0f;
totalAngle = (temperature / 100) * mSweepAngle;
targetAngle = 0f;
mCurPercent = 0f;
mCurTemperature = "0.0";
mWaveUpValue = 0;
startTimerAnim();
}
//使用定時(shí)任務(wù)做動(dòng)畫
private void startTimerAnim() {
if (isAnimRunning) {
return;
}
mAnimTimer = new Timer();
mAnimTimer.schedule(new TimerTask() {
@Override
public void run() {
isAnimRunning = true;
targetAngle += slow[goIndex];
goIndex++;
if (goIndex == slow.length) {
goIndex--;
}
if (targetAngle >= totalAngle) {
targetAngle = totalAngle;
isAnimRunning = false;
mAnimTimer.cancel();
}
// 計(jì)算的溫度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);
// 水波紋的高度
mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));
postInvalidate();
}
}, 250, 30);
}那么刻度動(dòng)畫的效果如下:

4. 繪制中心的圓與文字
我們再動(dòng)畫中記錄動(dòng)畫的百分比進(jìn)度,和動(dòng)畫當(dāng)前的溫度。
...
// 計(jì)算的溫度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);
postInvalidate();
...我們記錄一下小圓的半徑和文本的畫筆資源
private float mSmallRadius = 0f;
private Paint mTextPaint;
private Paint mSmallCirclePaint;
private float mCurPercent = 0f; //進(jìn)度
private String mCurTemperature = "0.0";
private DecimalFormat mDecimalFormat;
private void init() {
...
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setColor(Color.WHITE);
mSmallCirclePaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
//畫小圓
drawSmallCircle(canvas, evaluateColor(mCurPercent, Color.GREEN, Color.RED));
//畫中心的圓與文本
drawTemperatureText(canvas);
}具體的文本與小圓的繪制
private void drawSmallCircle(Canvas canvas, int evaluateColor) {
mSmallCirclePaint.setColor(evaluateColor);
mSmallCirclePaint.setAlpha(65);
canvas.drawCircle(centerPosition.x, centerPosition.y, mSmallRadius, mSmallCirclePaint);
}
private void drawTemperatureText(Canvas canvas) {
//提示文字
mTextPaint.setTextSize(mSmallRadius / 6f);
canvas.drawText("當(dāng)前溫度", centerPosition.x, centerPosition.y - mSmallRadius / 2f, mTextPaint);
//溫度文字
mTextPaint.setTextSize(mSmallRadius / 2f);
canvas.drawText(mCurTemperature, centerPosition.x, centerPosition.y + mSmallRadius / 4f, mTextPaint);
//繪制單位
mTextPaint.setTextSize(mSmallRadius / 6f);
canvas.drawText("°C", centerPosition.x + (mSmallRadius / 1.5f), centerPosition.y, mTextPaint);
}由于進(jìn)度和溫度都是動(dòng)畫在 invalidate 之前賦值的,所以我們的文本和小圓天然就支持動(dòng)畫的效果了。
效果如下:

5. 水波紋動(dòng)畫
水波紋的效果,我們不能直接用 Canvas 來繪制,我們可以用刻度的方法用 drawLine的方式來繪制,如何繪制呢?相信大家也有了解,就是正弦函數(shù)了。
由于我們的效果是兩個(gè)水波紋相互疊加起起伏伏的效果,所以我們定義兩個(gè)函數(shù)。
總體的思路是:我們定義兩個(gè)數(shù)組來管理我們的Y軸的值,通過正弦函數(shù)給Y軸賦值,然后在drawLine的時(shí)候取出對應(yīng)的x軸的y值就可以繪制出來。
x軸其實(shí)就是我們的控件寬度,我們先用一個(gè)數(shù)組保存起來
private float[] mFirstWaterLine;
private float[] mSecondWaterLine;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取控件的寬度,高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int newWidthMeasureSpec = widthMeasureSpec;
//如果沒有指定寬度,默認(rèn)給200寬度
if (widthMode != MeasureSpec.EXACTLY) {
newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
}
//獲取到最新的寬度
int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();
//我們要的是矩形,不管高度是多高,讓它總是和寬度一致
int height = width;
mFirstWaterLine = new float[width];
mSecondWaterLine = new float[width];
super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
}
然后我們再繪制之前就先對x軸對應(yīng)的y值賦值,然后繪制的時(shí)候就取出對應(yīng)的y值來 drawLine,具體的代碼如下:
動(dòng)畫的時(shí)候先對橫向運(yùn)動(dòng)和垂直運(yùn)動(dòng)的變量做一個(gè)賦值:
private int mWaveUpValue = 0;
private float mWaveMoveValue = 0f;
//使用定時(shí)任務(wù)做動(dòng)畫
private void startTimerAnim() {
if (isAnimRunning) {
return;
}
mAnimTimer = new Timer();
mAnimTimer.schedule(new TimerTask() {
@Override
public void run() {
...
// 計(jì)算的溫度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);
// 水波紋的高度
mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));
postInvalidate();
}
}, 250, 30);
}
public void moveWaterLine() {
mWaveTimer = new Timer();
mWaveTimer.schedule(new TimerTask() {
@Override
public void run() {
mWaveMoveValue += 1;
if (mWaveMoveValue == 100) {
mWaveMoveValue = 1;
}
postInvalidate();
}
}, 500, 200);
}拿到了對應(yīng)的變量值之后,然后開始繪制:
/**
* 繪制水波
*/
private void drawWaterWave(Canvas canvas, int color) {
int len = (int) mRectF.right;
// 將周期定為view總寬度
float mCycleFactorW = (float) (2 * Math.PI / len);
// 得到第一條波的峰值
for (int i = 0; i < len; i++) {
mFirstWaterLine[i] = (float) (10 * Math.sin(mCycleFactorW * i + mWaveMoveValue) - mWaveUpValue);
}
// 得到第一條波的峰值
for (int i = 0; i < len; i++) {
mSecondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + mWaveMoveValue + 10) - mWaveUpValue);
}
canvas.save();
// 裁剪成圓形區(qū)域
Path path = new Path();
path.addCircle(len / 2f, len / 2f, mSmallRadius, Path.Direction.CCW);
canvas.clipPath(path);
path.reset();
// 將坐標(biāo)系移到底部
canvas.translate(0, centerPosition.y + mSmallRadius);
mSmallCirclePaint.setColor(color);
for (int i = 0; i < len; i++) {
canvas.drawLine(i, mFirstWaterLine[i], i, len, mSmallCirclePaint);
}
for (int i = 0; i < len; i++) {
canvas.drawLine(i, mSecondWaterLine[i], i, len, mSmallCirclePaint);
}
canvas.restore();
}一個(gè)是對Y軸賦值,一個(gè)是取出x軸對應(yīng)的y軸進(jìn)行繪制,這里需要注意的是我們裁剪出了一個(gè)小圓的圖形,并且覆蓋在小圓上面實(shí)現(xiàn)出效果圖的樣子。
運(yùn)行的效果如下:

要記得對定時(shí)器進(jìn)行資源你的關(guān)閉哦。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mWaveTimer != null) {
mWaveTimer.cancel();
}
if (mAnimTimer != null && isAnimRunning) {
mAnimTimer.cancel();
}
}使用的時(shí)候我們只需要設(shè)置溫度即可開始動(dòng)畫。
findViewById<View>(R.id.set_progress).click {
val temperatureView = findViewById<TemperatureView>(R.id.temperature_view)
temperatureView .setupTemperature(70f)
}后記
由于是自用定制的,本人也比較懶,所以并沒有對一些配置的屬性做自定義屬性的抽取,比如圓環(huán)的間距,大小,顏色,波紋的間距,動(dòng)畫的快慢等等。
內(nèi)部加了一點(diǎn)點(diǎn)測量的用法,但是主要還是繪制的流程,基本上把常用的幾種繪制方式都用到了。以后有類似的效果大家也可以按需修改即可。
到此這篇關(guān)于Android自定義View實(shí)現(xiàn)繪制水波浪溫度刻度表的文章就介紹到這了,更多相關(guān)Android自定義View繪制溫度刻度表內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android TextWatcher三個(gè)回調(diào)以及監(jiān)聽EditText的輸入案例詳解
這篇文章主要介紹了Android TextWatcher三個(gè)回調(diào)以及監(jiān)聽EditText的輸入案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Kotlin?LinearLayout與RelativeLayout布局使用詳解
Kotlin?的基本特性就先寫到這里,我們這個(gè)系列的定位是基礎(chǔ),也就是能用就好,夠用就好,我們不會(huì)舉太多的例子,但是這些都是最經(jīng)常用到的特性。從這節(jié)開始就是Kotlin和android?進(jìn)行結(jié)合,使用Kotlin進(jìn)行安卓應(yīng)用的開發(fā)了2022-12-12
Android自定義webView頭部進(jìn)度加載效果
這篇文章主要介紹了Android自定義webView頭部進(jìn)度加載效果,小編畫一條進(jìn)度線,然后加載webview上面,具體實(shí)現(xiàn)代碼大家參考下本文2017-11-11
Android UI體驗(yàn)之全屏沉浸式透明狀態(tài)欄樣式
這篇文章主要介紹了Android UI體驗(yàn)之全屏沉浸式透明狀態(tài)欄效果的相關(guān)資料,需要的朋友可以參考下2017-01-01
Android的EditText字?jǐn)?shù)檢測和限制解決辦法
這篇文章主要介紹了Android的EditText字?jǐn)?shù)檢測和限制解決辦法的相關(guān)資料,需要的朋友可以參考下2017-03-03

