繼承View是實現自定義View的重要方式,通過自定義屬性以實現xml中的便捷使用,通過重寫onMeasure和onDraw方法自定義View的繪製過程,通過攔截事件響應完成特定的行爲,讓想法變爲現實。
上一篇文章Android自定義View之旅(一)自定義View的幾種方式
本文將通過實戰來講述如何通過繼承View實現自定義,先仔細看看需求,一個聲音波形控件,持續動畫效果,單靠上面的簡單實現是不可能的了,需要的效果如下:
1、繼承View實現自定義View
在上一篇文章中,我們詳細介紹了簡單的“自定義”如何實現,繼承了安卓系統的原生控件,再在此基礎上完成附加的功能樣式或者更改原有的功能樣式,可以迅速達到想要的效果,缺點是會受到父類的限制、無法完成複雜的需求。
對於這次需要實現的聲音波形控件,我們需要新建一個類VoiceLineView
,繼承自View
:
/**
* 自定義聲音振動曲線view
*/
public class VoiceLineView extends View {
public VoiceLineView(Context context) {
super(context);
}
public VoiceLineView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VoiceLineView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
2、提供一些自定義的屬性
通過自定義屬性以實現xml中的便捷使用,就像我們在使用TextView
的時候,設置android:text="Hello World!"
。
首先,需要在src/main/res/values文件夾下的attrs.xml文件(如不存在此文件新建一個即可)中新增如下代碼:
<?xml version="1.0" encoding="utf-8"?>
<resources>
······
<!--name爲獲取屬性時使用,不可與其他控件的自定義屬性重複-->
<declare-styleable name="voiceView">
<!--中間線的顏色,就是波形的時候,大家可以看到,中間有一條直線,就是那個-->
<attr name="middleLine" format="color" />
<!--中間線的高度,中間線的寬度是充滿的無需設定-->
<attr name="middleLineHeight" format="dimension" />
<!--波動的線的顏色-->
<attr name="voiceLine" format="color" />
<!--波動線的橫向移動速度,線的速度的反比,即這個值越小,線橫向移動越快,越大線移動越慢,默認90-->
<attr name="lineSpeed" format="integer" />
<!--所輸入音量的最大值,默認是100-->
<attr name="maxVolume" format="float" />
<!--靈敏度,默認值是4-->
<attr name="sensibility">
<enum name="one" value="1" />
<enum name="two" value="2" />
<enum name="three" value="3" />
<enum name="four" value="4" />
<enum name="five" value="5" />
</attr>
<!--精細度,繪製曲線的時候,每幾個像素繪製一次,默認是1,一般,這個值越小,曲線越順滑,但在一些舊手機上,會出現幀率過低的情況,可以把這個值調大一點,在圖片的順滑度與幀率之間做一個取捨-->
<attr name="fineness">
<enum name="one" value="1" />
<enum name="two" value="2" />
<enum name="three" value="3" />
</attr>
</declare-styleable>
······
</resources>
新增一個declare-styleable標籤聲明一個屬性集,根據需要,在declare-styleable標籤中增加多個attr標籤聲明屬性,attr標籤根據控件的可設置屬性進行配置。關於attr自定義屬性的類型可以看文章Android中attr屬性的類型。
隨後在VoiceLineView
類中修改代碼如下:
public class VoiceLineView extends View {
private int middleLineColor = Color.BLACK;
private int voiceLineColor = Color.BLACK;
private float middleLineHeight = 4;
private int sensibility = 4;
private float maxVolume = 100;
private int fineness = 1;
private int lineSpeed = 90;
public VoiceLineView(Context context) {
super(context);
}
public VoiceLineView(Context context, AttributeSet attrs) {
super(context, attrs);
initAtts(context, attrs);
}
public VoiceLineView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAtts(context, attrs);
}
private void initAtts(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.voiceView);
voiceLineColor = typedArray.getColor(R.styleable.voiceView_voiceLine, Color.BLACK);
maxVolume = typedArray.getFloat(R.styleable.voiceView_maxVolume, 100);
sensibility = typedArray.getInt(R.styleable.voiceView_sensibility, 4);
middleLineColor = typedArray.getColor(R.styleable.voiceView_middleLine, Color.BLACK);
middleLineHeight = typedArray.getDimension(R.styleable.voiceView_middleLineHeight, 4);
lineSpeed = typedArray.getInt(R.styleable.voiceView_lineSpeed, 90);
fineness = typedArray.getInt(R.styleable.voiceView_fineness, 1);
typedArray.recycle();
}
}
在佈局中使用如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<VoiceLineView
android:id="@+id/voice_line"
android:layout_width="match_parent"
android:layout_height="200dp"
app:middleLine="@color/colorPrimary"
app:middleLineHeight="1dp"
app:voiceLine="@color/colorPrimary"
app:lineSpeed="90"
app:maxVolume="100"
app:sensibility="four"
app:fineness="one" />
</LinearLayout>
3、重寫onDraw方法,持續繪製聲波
話不多說,直接上代碼:
public class VoiceLineView extends View {
······
private Paint paint;
private Paint paintVoicLine;
private float translateX = 0;
private boolean isSet = false;
private float amplitude = 1;
private float volume = 10;
private long lastTime = 0;
private int lineSpeed = 90;
List<Path> paths = null;
······
public void setVolume(int volume) {
if (volume > maxVolume * sensibility / 25) {
isSet = true;
this.targetVolume = getHeight() * volume / 2 / maxVolume;
}
}
@Override
protected void onDraw(Canvas canvas) {
drawMiddleLine(canvas);
drawVoiceLine(canvas);
run();
}
private void drawMiddleLine(Canvas canvas) {
if (paint == null) {
paint = new Paint();
paint.setColor(middleLineColor);
paint.setAntiAlias(true);
}
canvas.save();
canvas.drawRect(0, getHeight() / 2 - middleLineHeight / 2, getWidth(), getHeight() / 2 + middleLineHeight / 2, paint);
canvas.restore();
}
private void drawVoiceLine(Canvas canvas) {
lineChange();
if (paintVoicLine == null) {
paintVoicLine = new Paint();
paintVoicLine.setColor(voiceLineColor);
paintVoicLine.setAntiAlias(true);
paintVoicLine.setStyle(Paint.Style.STROKE);
paintVoicLine.setStrokeWidth(2);
}
canvas.save();
if (paths == null) {
paths = new ArrayList<>(20);
for (int i = 0; i < 20; i++) {
paths.add(new Path());
}
}
int moveY = getHeight() / 2;
for (int i = 0; i < paths.size(); i++) {
paths.get(i).reset();
paths.get(i).moveTo(getWidth(), getHeight() / 2);
}
for (float i = getWidth() - 1; i >= 0; i -= fineness) {
amplitude = 4 * volume * i / getWidth() - 4 * volume * i * i / getWidth() / getWidth();
for (int n = 1; n <= paths.size(); n++) {
float sin = amplitude * (float) Math.sin((i - Math.pow(1.22, n)) * Math.PI / 180 - translateX);
paths.get(n - 1).lineTo(i, (2 * n * sin / paths.size() - 15 * sin / paths.size() + moveY));
}
}
for (int n = 0; n < paths.size(); n++) {
if (n == paths.size() - 1) {
paintVoicLine.setAlpha(255);
} else {
paintVoicLine.setAlpha(n * 130 / paths.size());
}
if (paintVoicLine.getAlpha() > 0) {
canvas.drawPath(paths.get(n), paintVoicLine);
}
}
canvas.restore();
}
private void lineChange() {
if (lastTime == 0) {
lastTime = System.currentTimeMillis();
translateX += 1.5;
} else {
if (System.currentTimeMillis() - lastTime > lineSpeed) {
lastTime = System.currentTimeMillis();
translateX += 1.5;
} else {
return;
}
}
if (volume < targetVolume && isSet) {
volume += getHeight() / 30;
} else {
isSet = false;
if (volume <= 10) {
volume = 10;
} else {
if (volume < getHeight() / 30) {
volume -= getHeight() / 60;
} else {
volume -= getHeight() / 30;
}
}
}
}
public void run() {
invalidate();
}
}
到此,需求就已經完成啦,使用時按第2點加入佈局,代碼中使用:
VoiceLineView voiceLine = findViewById(R.id.voice_line);
// 循環設置音量即可繪製聲波圖
voiceLine.setVolume(volume);