<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
之前的繪製圓環,我們瞭解瞭如何繪製想要的形狀和進度的一些特點,那麼此篇文章我們更近一步,繪製一個稍微複雜一點的刻度與波浪。來一起復習一下Android的繪製。
相對應的這種型別的自定義View網上並不少見,但是如果我們要做一些個性化的效果,最好還是自己繪製一份,也相對的比較容易控制效果,如果想實現上面的效果,我們一般來說分為以下幾個步驟:
思路我們已經有了,下面一步一步的來實現吧。
話不多說,Let's go
之前的圓環進度,我們並沒有重寫 onMeasure
方法,而是在佈局中指定為固定的寬高,其實相容性和健壯性並不好,萬一寫錯了就會變形導致顯示異常。
最好的辦法是不管xml中設定為什麼值,這裡都能保證為一個正方形,要麼是取寬度為準,讓高度和寬度一致,要麼就是寬度高度取最大值,讓他們保持一致。由於我們是豎屏的應用,所以我就取寬度為準,讓高度和寬度一致。
前面我們只是講了 onDraw
並沒有講到 onMeasure
, 這裡簡單的說一下。
我們為什麼要重寫 onMeasure ?
一般來說我們重寫的 onMeasure
長這樣:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec,heightMeasureSpec) }
widthMeasureSpec ,heightMeasureSpec 並不是真正的寬高,看名字就知道,它只是寬高測量的規格,我們通過 MeasureSpec 的一些靜態方法,通過它們拿到一些資訊。
static int getMode(int measureSpec):根據提供的測量值(規格)提取模式(上述三個模式之一)
測量的 Model 一共有三種
我們常用的就是 EXACTLY 和 AT_MOST ,EXACTLY 對應的就是我們設定的match_parent或者300這樣的精確值,而 AT_MOST 對應的就是wrap_content。
static int getSize(int measureSpec):根據提供的測量值(規格)提取大小值(這個大小也就是我們通常所說的大小)
通過此方法就能獲取控制元件的寬度和高度值。
static int makeMeasureSpec(int size,int mode):根據提供的大小值和模式建立一個測量值(規格)
通過具體的寬高和model,建立對應的寬高測量規格,用於確定View的測量
而 onMeasure
的最終設定確定寬度的測量有兩種方式,
實戰:
比如我們的自定義溫度刻度View,我們整個View要確保一個正方形,那麼就拿到寬度,設定同樣的高度,然後確定測量,流程如下:
//重新測量 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //獲取控制元件的寬度,高度 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int newWidthMeasureSpec = widthMeasureSpec; //如果沒有指定寬度,預設給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); //最後設定生效-下面兩種方式都可以 // setMeasuredDimension(width, height); super.onMeasure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) ); }
這裡有詳細的註釋,大致實現的效果如下:
由於原本的 Canvas 內部沒有繪製刻度這麼一說,所以我們只能用繪製線條的方式,就是 drawLine
方法。
為了瞭解到座標系和方便實現,我們可以先繪製一個圓環,定位我們刻度需要繪製的位置。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //畫圓環 canvas.drawArc( mRectF.left + 2f, mRectF.top + 2f, mRectF.right - 2f, mRectF.bottom - 2f, mStartAngle, mSweepAngle, false, mDegreeCirPaint ); }
這個圓環是之前講到過了,就不過多贅述了,實現效果如下:
由於開始繪製的地方在左上角位置,我們要移動到圓的中心點開始繪製,也就是紅色點移動到藍色點。
我們就需要x軸和y軸做一下偏移 canvas.translate(radius, radius);
預設的 drawLine 都是橫向繪製,我們想要實現效果圖的效果,就需要旋轉一下畫筆,也就是用到 canvas.rotate(rotateAngle);
那麼旋轉多少了,如果說最底部是90度,我們的起始角度是120度開始的,我們就起始旋轉30度。後面每一次旋轉就按照百分比來,比如我們100度的溫度,那麼就相當於要畫100個刻度,我們就用需要繪製的角度除以100,就是每一個刻度的角度。
具體的刻度實現程式碼:
private float mStartAngle = 120f; // 圓弧的起始角度 private float mSweepAngle = 300f; //繪製的起始角度和滑過角度(繪製300度) private float mTargetAngle = 300f; //刻度的角度(根據此計算需要繪製有色的進度) private void drawDegreeLine(Canvas canvas) { //先儲存 canvas.save(); // 移動畫布 canvas.translate(radius, radius); // 旋轉座標系,需要確定旋轉角度 canvas.rotate(30); // 每次旋轉的角度 float rotateAngle = mSweepAngle / 100; // 累計疊加的角度 float currentAngle = 0; for (int i = 0; i <= 100; i++) { if (currentAngle <= mTargetAngle && mTargetAngle != 0) { // 計算累計劃過的刻度百分比 float percent = currentAngle / mSweepAngle; //動態的設定顏色 mDegreelinePaint.setColor(evaluateColor(percent, Color.GREEN, Color.RED)); canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint); // 畫過的角度進行疊加 currentAngle += rotateAngle; } else { mDegreelinePaint.setColor(Color.WHITE); canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint); } //畫完一個刻度就要旋轉移動位置 canvas.rotate(rotateAngle); } //再恢復 canvas.restore(); }
加上圓環與刻度的效果圖:
前面的一篇我們使用的是屬性動畫不停的繪製從而實現進度的效果,那麼這一次我們使用定時任務的方式也是可以實現動畫的效果。
由於我們之前的 drawDegreeLine 方法內部控制繪製進度的變數就是 targetAngle 來控制的,所以我們通過入口方法設定溫度的時候通過定時任務的方式來控制。
程式碼如下:
//動畫狀態 private boolean isAnimRunning; // 手動實現越來越慢的效果 private int[] slow = {10, 10, 10, 8, 8, 8, 6, 6, 6, 6, 4, 4, 4, 4, 2}; // 動畫的下標 private int goIndex = 0; //設定溫度,入口的開始 public void setupTemperature(float temperature) { mCurPercent = 0f; totalAngle = (temperature / 100) * mSweepAngle; targetAngle = 0f; mCurPercent = 0f; mCurTemperature = "0.0"; mWaveUpValue = 0; startTimerAnim(); } //使用定時任務做動畫 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(); } // 計算的溫度 mCurPercent = targetAngle / mSweepAngle; mCurTemperature = mDecimalFormat.format(mCurPercent * 100); // 水波紋的高度 mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2)); postInvalidate(); } }, 250, 30); }
那麼刻度動畫的效果如下:
我們再動畫中記錄動畫的百分比進度,和動畫當前的溫度。
... // 計算的溫度 mCurPercent = targetAngle / mSweepAngle; mCurTemperature = mDecimalFormat.format(mCurPercent * 100); postInvalidate(); ...
我們記錄一下小圓的半徑和文字的畫筆資源
private float mSmallRadius = 0f; private Paint mTextPaint; private Paint mSmallCirclePaint; private float mCurPercent = 0f; //進度 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("當前溫度", 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); }
由於進度和溫度都是動畫在 invalidate 之前賦值的,所以我們的文字和小圓天然就支援動畫的效果了。
效果如下:
水波紋的效果,我們不能直接用 Canvas 來繪製,我們可以用刻度的方法用 drawLine的方式來繪製,如何繪製呢?相信大家也有了解,就是正弦函數了。
由於我們的效果是兩個水波紋相互疊加起起伏伏的效果,所以我們定義兩個函數。
總體的思路是:我們定義兩個陣列來管理我們的Y軸的值,通過正弦函數給Y軸賦值,然後在drawLine的時候取出對應的x軸的y值就可以繪製出來。
x軸其實就是我們的控制元件寬度,我們先用一個陣列儲存起來
private float[] mFirstWaterLine; private float[] mSecondWaterLine; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //獲取控制元件的寬度,高度 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int newWidthMeasureSpec = widthMeasureSpec; //如果沒有指定寬度,預設給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值賦值,然後繪製的時候就取出對應的y值來 drawLine,具體的程式碼如下:
動畫的時候先對橫向運動和垂直運動的變數做一個賦值:
private int mWaveUpValue = 0; private float mWaveMoveValue = 0f; //使用定時任務做動畫 private void startTimerAnim() { if (isAnimRunning) { return; } mAnimTimer = new Timer(); mAnimTimer.schedule(new TimerTask() { @Override public void run() { ... // 計算的溫度 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); }
拿到了對應的變數值之後,然後開始繪製:
/** * 繪製水波 */ 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(); // 裁剪成圓形區域 Path path = new Path(); path.addCircle(len / 2f, len / 2f, mSmallRadius, Path.Direction.CCW); canvas.clipPath(path); path.reset(); // 將座標系移到底部 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(); }
一個是對Y軸賦值,一個是取出x軸對應的y軸進行繪製,這裡需要注意的是我們裁剪出了一個小圓的圖形,並且覆蓋在小圓上面實現出效果圖的樣子。
執行的效果如下:
要記得對定時器進行資源你的關閉哦。
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mWaveTimer != null) { mWaveTimer.cancel(); } if (mAnimTimer != null && isAnimRunning) { mAnimTimer.cancel(); } }
使用的時候我們只需要設定溫度即可開始動畫。
findViewById<View>(R.id.set_progress).click { val temperatureView = findViewById<TemperatureView>(R.id.temperature_view) temperatureView .setupTemperature(70f) }
由於是自用客製化的,本人也比較懶,所以並沒有對一些設定的屬性做自定義屬性的抽取,比如圓環的間距,大小,顏色,波紋的間距,動畫的快慢等等。
內部加了一點點測量的用法,但是主要還是繪製的流程,基本上把常用的幾種繪製方式都用到了。以後有類似的效果大家也可以按需修改即可。
到此這篇關於Android自定義View實現繪製水波浪溫度刻度表的文章就介紹到這了,更多相關Android自定義View繪製溫度刻度表內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45