自定義View從實作到原理(七)
已經到這一步了啊,這一篇寫完基本上自定義View就不會寫了,以后有可能的話,也許會寫一下自定義ViewGroup或者是自定義View的仿真書籍翻頁效果,不過那也是以后的事情了,今天就來實作以下水波紋加載效果,先看一下效果圖:

類似這種的效果,其實也就是一個自定義的View,接下來我們來一步步實作一下:
定義屬性
首先還是一樣,根據效果圖,先定義這個View的屬性,這個效果我覺得需要圓形的背景顏色,圓形的半徑,顯示的進度,顯示文字的大小,顯示文字的顏色,定義屬性的代碼:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="WaterView">
<!-- 背景顏色,圓的半徑,進度,進度字號,進度顏色 -->
<attr name="backgroundColor" format="color" />
<attr name="radius" format="dimension" />
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
</declare-styleable>
</resources>
按照正常步驟,下一步在自定義的View中的建構式獲取屬性:
private int backgroundColor, textColor;
private float radius, textSize;
private String text;
private Paint backgroundPaint, textPaint;
public WaterView(Context context) {
this(context, null);
}
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray waterTypeArray = context.obtainStyledAttributes(attrs, R.styleable.WaterView);
backgroundColor = waterTypeArray.getColor(R.styleable.WaterView_backgroundColor,
Color.BLACK);
textColor = waterTypeArray.getColor(R.styleable.WaterView_textColor, Color.WHITE);
radius = waterTypeArray.getDimension(R.styleable.WaterView_radius, 260f);
textSize = waterTypeArray.getDimension(R.styleable.WaterView_textSize, 24f);
text = waterTypeArray.getString(R.styleable.WaterView_text);
//記得回收
waterTypeArray.recycle();
}
繪制進度文字
首先我們要初始化畫筆,在自定義View中我們通過畫筆可以繪制我們想要的所有效果:
/**
* 初始化畫筆
*/
private void initPaint() {
//初始化背景畫筆
backgroundPaint = new Paint();
backgroundPaint.setColor(backgroundColor);
//抗鋸齒
backgroundPaint.setAntiAlias(true);
//初始化顯示文字畫筆
textPaint = new Paint();
textPaint.setTextSize(textSize);
textPaint.setColor(textColor);
textPaint.setAntiAlias(true);
//字體為粗體
textPaint.setFakeBoldText(true);
}
定義完畫筆之后,我們首先繪制出底層的圓形,有圓形才能在圓形中心繪制文字:
canvas.drawCircle(getWidth()/2, getHeight()/2, radius, backgroundPaint);
別忘了在建構式中呼叫initPaint()方法,要不然會有空指標例外的錯誤拋出的,
寫到這里我們想看一下能不能畫出圓形,所以在xml中簡單參考一下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Activity.WaterActivity">
<com.example.day1.View.WaterView
android:layout_width="260dp"
android:layout_height="260dp"
android:layout_centerInParent="true"
/>
</RelativeLayout>
效果圖如下:

可真是樸實無華的一個圓形,既然能畫出來,那我們之前的作業就沒錯,在中心顯示一下文字:
canvas.drawText(text, getWidth() / 2, getHeight() / 2, textPaint);
我第一次是這么寫的,感覺很正常,在Width的中間與Height的中間繪制出顯示的文字,結果運行出來出了問題:

震驚,什么鬼?我仔細看了一下這個文字的位置,發現它的左下角,應該就是我們設定的中心位置,就離譜,遇到了問題就要解決,我就去搜了一下自定義View怎么將文字顯示在中心,這里參考了這篇文章:
Android自定義View之文字居中
首先在initPaint()方法中設定居中,不過注意這只能做到水平居中:
textPaint.setTextAlign(Paint.Align.CENTER);
這一行代碼就可以了,那么接下來我們處理豎直方向居中,按理來說上面這一行代碼就足以解決了,但是為什么會有一點偏上呢,這就是因為在文字顯示的時候,有一個基線,這個基線正是在我們設定的豎直居中位置,我們的文字在基線上部,因此就會比中心位置提高一點,對于這個問題有兩種解決方法:
getTextBounds()方法
先來看一下這個方法的使用:
getTextBounds(String text, int start, int end, Rect bounds)
text是要顯示的文本,start以及end是文本的開始顯示位置與結束位置,bounds是存盤文字顯示位置的物件,最后的結果會寫進bounds中,我們使用就是通過這個方法獲得文字的邊框bounds,由于基線在文字的下方,因此我們想要豎直居中的話就得向下平移文字高度的一半,就是 (bounds.top+bound.bottom)/2 這個值,但是要注意這個是一個負數,代表的是文字的高度一半的位置而不是真正意義的高度一半,getTextBounds()方法會有一個自己的坐標系:

可以看得出來在這個坐標系下,top以及bottom都是負值,因此我們向下平移需要減去之前算出的文字高度中心位置,那么具體的用法就是下面的代碼:
Rect bounds = new Rect();
textPaint.getTextBounds(text, 0, text.length(), bounds);
float offset = (bounds.bottom + bounds.top) / 2;
canvas.drawText(text, getWidth() / 2, (getHeight() / 2) - offset, textPaint);

可行,我們接下來看一下另外一種方法;
FontMetircs 方法

和之前的方法類似,我們看一下這個圖,有了之前的經驗,我們可以看出需要的兩個資料為ascent以及descent這兩個,不過要注意的是,這幾個值,是固定不會變的,也就是無論你繪制的內容怎么改變,這幾個值都不會變,類比上面的代碼,我們來看一下這個:
Paint.FontMetrics fontMetrics= new Paint.FontMetrics();
textPaint.getFontMetrics(fontMetrics);
float offset = (fontMetrics.descent+fontMetrics.ascent)/2;
canvas.drawText(text, getWidth() / 2, (getHeight() / 2) - offset , textPaint);
效果的話和上面是一樣的,那么這兩種方法,我們應該如何區別使用呢:
對于第一種,getTextBounds()方法,我們是獲取到了文字的中間高度,那么隨著內容的改變,我們的中間位置可能也會發生改變;
第二種,FontMetircs方法,這是固定的文字測量工具,不管內容如何改變,他的中間位置不會發生改變,
這樣我們就可以總結出:
1.當我們繪制的內容會有動態改變操作時,使用FontMetircs()方法;
2.當繪制內容固定的時候,我們用哪個都可以,第一種看起來更加直觀,
設定onMeasure
通常我們都會在xml布局中將控制元件設為wrapContent型別,按照我們之前寫過的博客,我們也應該要重寫onMeasure函式,這部分就不多說了,之前也介紹過:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
height = width = (int) radius * 2;
setMeasuredDimension(width, height);
} else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
簡簡單單的一串代碼,樸實無華,效果也沒什么變化,就這樣,下一步,
顯示文字模擬下載
下載自然是從0%到100%,我們開啟執行緒進行模擬,在規定時間內跑完,并動態更新文字內容的顯示:
private SingleTapThread singleTapThread;
private GestureDetector detector;
private int currentProgress = 0;
private int maxProgress = 100;
text = currentProgress + "%";
做好準備活動,接下來會開啟執行緒模擬下載:
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
startProgressAnimation();
}
return super.onTouchEvent(event);
}
private void startProgressAnimation() {
if (singleTapThread == null) {
singleTapThread = new SingleTapThread();
getHandler().postDelayed(singleTapThread, 100);
}
}
private class SingleTapThread implements Runnable {
@Override
public void run() {
int maxProgress = 100;
if (currentProgress < maxProgress) {
invalidate();
getHandler().postDelayed(singleTapThread, 100);
currentProgress++;
} else {
getHandler().removeCallbacks(singleTapThread);
}
}
}
我們從上到下看,首先設定了點擊事件,只要有觸碰抬起的事件發生,那么就啟動startProgressAnimation()方法,在這個方法中我們首先檢測了是否有執行緒在運行,如果沒有的話開啟執行緒,延遲100ms后啟動SingleTapThread()執行緒,在這個執行緒中定義了最大值為100,如果當前值小于100就會重繪View并且100ms后再次重啟,同時進度加一,如果已經完成則回收這個執行緒,效果圖如下:

文字效果已經實作,那么接下來才是主要的部分,實作水波紋波浪效果:
實作水波紋波浪效果
簡單來說,水波紋波浪效果,就是二階貝塞爾曲線的一個應用,來看一下二階貝塞爾曲線效果:

就是這樣,我們這個效果可以看成是曲線在固定范圍內的不斷變換,我們這里就簡要了解一下二階貝塞爾曲線的應用就好了,如果需要深入在后面我再研究研究,
在Android SDK中提供了關于繪制貝塞爾曲線的方法:
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
這四個引數都是相對值,我們來看一下上面的哪個動圖,曲線起點是P0,終點P2,控制點P1,而相對的我們這四個引數的值,為:
1.dx1:控制點X坐標,表示相對上一個終點X坐標的位移坐標,可為負值,正值表示相加,負值表示相減;
2.dy1:控制點Y坐標,相對上一個終點Y坐標的位移坐標,同樣可為負值,正值表示相加,負值表示相減;
3.dx2:終點X坐標,同樣是一個相對坐標,相對上一個終點X坐標的位移值,可為負值,正值表示相加,負值表示相減;
4.dy2:終點Y坐標,同樣是一個相對,相對上一個終點Y坐標的位移值,可為負值,正值表示相加,負值表示相減;
這四個引數都是傳遞的都是相對值,相對上一個終點的位移值,
接下來正式繪制波浪特效:
首先初始化波浪的畫筆:
//初始化波浪畫筆
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setColor(waterColor);
//兩層在一起我在上面
progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
最后這行設定如果兩層繪制在了同一個界面,這個繪制的會在上部顯示;
既然這個畫布是有遮蓋效果的,那么就應該設定在同一個bitmap上,我們定義一個bitmap,為了在這個bitmap上進行繪制,我們還要定義一個bitmapCanvas,來載入bitmap:
if (bitmap == null) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
這是初始化bitmap以及bitmapCanvas部分,定義部分自己在開始寫一下吧,既然已經有了bitmapCanvas,我們就來把之前的畫圓以及繪制文字都載入一下:
super.onDraw(canvas);
width = getWidth();
height = getHeight();
if (bitmap == null) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
bitmapCanvas.save();
//移動坐標系
bitmapCanvas.translate(0, height / 4);
//繪制圓
bitmapCanvas.drawCircle(radius, radius, radius, backgroundPaint);
//繪制PATH
//重置繪制路線
path.reset();
float percent = currentProgress * 1.0f / maxProgress;
float y = (1 - percent) * radius * 2;
//移動到右上邊
path.moveTo(radius * 2, y);
//移動到最右下方
path.lineTo(radius * 2, radius * 2);
//移動到最左下邊
path.lineTo(0, radius * 2);
//移動到左上邊
// path.lineTo(0, y);
//實作左右波動,根據progress來平移
path.lineTo(-(1 - percent) * radius * 2, y);
if (currentProgress != 0.0f) {
//根據直徑計算繪制貝賽爾曲線的次數
float count = radius * 4 / 60;
//控制-控制點y的坐標
float point = (1 - percent) * 15;
for (int i = 0; i < count; i++) {
path.rQuadTo(15, -point, 30, 0);
path.rQuadTo(15, point, 30, 0);
}
}
//閉合
path.close();
bitmapCanvas.drawPath(path, progressPaint);
//繪制文字
String text = currentProgress + "%";
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;
bitmapCanvas.drawText(text, width / 2, radius - offset, textPaint);
bitmapCanvas.restore();
canvas.drawBitmap(bitmap, 0, 0, null);
這里我們將onDraw()的代碼都寫了上去,那么我們來分析一下現在改過的代碼,首先我們存盤了這個bitmapCanvas,存盤對應的就是save(),對應的在后面也要有restore()方法進行對應,具體的貝塞爾曲線部分我現在還不是特別懂,等明天再研究一下會更新博客的,
效果圖如下:
視頻還發不了,,等明天錄gif吧,溜了
最后貼一個整體的自定義View代碼,以供參考:
package com.example.day1.View;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.example.day1.R;
public class WaterView extends View {
//定義背景顏色,字體顏色;
private int backgroundColor, textColor, waterColor;
private float radius, textSize;
private String text;
private Paint backgroundPaint, textPaint, progressPaint;
private SingleTapThread singleTapThread;
private int currentProgress = 0;
private int maxProgress = 100;
private Path path = new Path();
private Bitmap bitmap;
private Canvas bitmapCanvas;
private int width, height;
public WaterView(Context context) {
this(context, null);
}
public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray waterTypeArray = context.obtainStyledAttributes(attrs, R.styleable.WaterView);
backgroundColor = waterTypeArray.getColor(R.styleable.WaterView_backgroundColor, Color.WHITE);
textColor = waterTypeArray.getColor(R.styleable.WaterView_textColor, Color.BLACK);
radius = waterTypeArray.getDimension(R.styleable.WaterView_radius, 260f);
textSize = waterTypeArray.getDimension(R.styleable.WaterView_textSize, 24f);
text = waterTypeArray.getString(R.styleable.WaterView_text);
waterColor = waterTypeArray.getColor(R.styleable.WaterView_waterColor, Color.GREEN);
//記得回收
waterTypeArray.recycle();
initPaint();
}
/**
* 初始化畫筆
*/
private void initPaint() {
//初始化背景畫筆
backgroundPaint = new Paint();
backgroundPaint.setColor(backgroundColor);
//抗鋸齒
backgroundPaint.setAntiAlias(true);
//初始化顯示文字畫筆
textPaint = new Paint();
textPaint.setTextSize(textSize);
textPaint.setColor(textColor);
textPaint.setAntiAlias(true);
//字體為粗體
textPaint.setFakeBoldText(true);
textPaint.setTextAlign(Paint.Align.CENTER);
//初始化波浪畫筆
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setColor(waterColor);
//取兩層繪制交集,顯示上層
progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
height = width = (int) radius * 2;
setMeasuredDimension(width, height);
} else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
/**
* 繪制部份
*
* @param canvas 畫布
*/
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
width = getWidth();
height = getHeight();
if (bitmap == null) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
bitmapCanvas.save();
//移動坐標系
bitmapCanvas.translate(0, height / 4);
//繪制圓
bitmapCanvas.drawCircle(radius, radius, radius, backgroundPaint);
//繪制PATH
//重置繪制路線
path.reset();
float percent = currentProgress * 1.0f / maxProgress;
float y = (1 - percent) * radius * 2;
//移動到右上邊
path.moveTo(radius * 2, y);
//移動到最右下方
path.lineTo(radius * 2, radius * 2);
//移動到最左下邊
path.lineTo(0, radius * 2);
//移動到左上邊
// path.lineTo(0, y);
//實作左右波動,根據progress來平移
path.lineTo(-(1 - percent) * radius * 2, y);
if (currentProgress != 0.0f) {
//根據直徑計算繪制貝賽爾曲線的次數
float count = radius * 4 / 60;
//控制-控制點y的坐標
float point = (1 - percent) * 15;
for (int i = 0; i < count; i++) {
path.rQuadTo(15, -point, 30, 0);
path.rQuadTo(15, point, 30, 0);
}
}
//閉合
path.close();
bitmapCanvas.drawPath(path, progressPaint);
//繪制文字
String text = currentProgress + "%";
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float offset = (fontMetrics.ascent + fontMetrics.descent) / 2;
bitmapCanvas.drawText(text, width / 2, radius - offset, textPaint);
bitmapCanvas.restore();
canvas.drawBitmap(bitmap, 0, 0, null);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
startProgressAnimation();
}
return super.onTouchEvent(event);
}
private void startProgressAnimation() {
if (singleTapThread == null) {
singleTapThread = new SingleTapThread();
getHandler().postDelayed(singleTapThread, 100);
}
}
private class SingleTapThread implements Runnable {
@Override
public void run() {
if (currentProgress < maxProgress) {
invalidate();
getHandler().postDelayed(singleTapThread, 100);
currentProgress++;
} else {
getHandler().removeCallbacks(singleTapThread);
}
}
}
}
就這樣,撤退,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/241875.html
標籤:其他
上一篇:安卓學期總結和作業完成情況
