轉(zhuǎn)載請注明出處:http://www.itdecent.cn/p/e4e59ab22324
寫在開頭
一個似水波的球狀控件,WaterWaveBallView,其實際用途同 ProgressBar 進度顯示控件。
我們先構(gòu)思,它由哪些部分構(gòu)成,它需要定義些什么屬性。
- 水波:兩條正弦或余弦曲線。
- 球狀:圓形視圖。
- 顏色:兩條曲線與控件底部間的填充顏色。
- 高度:兩條曲線的水平線位置。
當然了,這只是我們前期的設想,在實際的實現(xiàn)過程中會遇到很多的未知問題。
發(fā)車...
實現(xiàn)過程
水波
public class GoiWaveView extends View {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mTotalWidth = w; // 控件的寬度
mTotalHeight = h; // 控件的高度
// 定義波形的周期為控件的寬度,即 ω:周期
FACTOR_W = (float) (2 * Math.PI / mTotalWidth);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for(int i = 0; i < mTotalHeight; i ++){
// 計算出橫坐標 i 對應的 縱坐標值 positionY
float positionY = (float) (numberA * Math.sin(FACTOR_W *i) + OFFSET_Y);
// 畫豎線,從起始位置(0, mTotalHeight - 0 - 100)畫到終點坐標(0, mTotalHeight)
canvas.drawLine(i, mTotalHeight - positionY - 100, i, mTotalHeight, sinPaint);
}
}}
解釋下 canvas.drawLine(i, mTotalHeight - positionY - 100, i, mTotalHeight, sinPaint);
假設控件的高度 mTotalHeight = 300,當 i = 0 時
drawLine的四個參數(shù):
startX,此時是 0;
startY,此時是 300 - 0 - 100;
stopX,此時是 0;
stopY,此時是 300。
左上角的坐標是(0, 0),所以是 mTotalHeight - positionY,-100是為了我們看到曲線的全貌。
可以看到,實際上畫正弦曲線的過程就是畫一根一根的豎線,它們是連續(xù)的,所以我們看到的是填充了的曲線圖。
再加一個余弦曲線...
然后,讓兩條曲線,都移動起來。
其思路是,將一個完整的曲線向左或右移動,如何移動呢?
將一個周期曲線的橫縱坐標點,記錄下來。
for(int i = 0; i < mTotalWidth; i ++){
// 計算出橫坐標 i 對應的 縱坐標值 positionY
yOnePosition[i] = (float) (Y_FUNCTION_NUMBER_A * Math.sin(FACTOR_W *i) + OFFSET_Y);
yTwoPosition[i] = (float) (Y_FUNCTION_NUMBER_A * Math.cos(FACTOR_W *i) + OFFSET_Y);
}
xOneOffset 是移動偏移量,通過 System.arraycopy 方法將在移動中超出屏幕部分的點,添加到新數(shù)組的后面。
即: 1 2 3 4 5 6 7 8 9 10 向左移動兩個數(shù)
變成:3 4 5 6 7 8 9 10
這時把 1 2 追加到 10 的后面,使其變成
3 4 5 6 7 8 9 10 1 2
在正弦和余弦曲線中,他們還是首尾相連的。
int xOnePoint = yOnePosition.length - xOneOffset;
System.arraycopy(yOnePosition, xOneOffset, yOneResetPosition, 0, xOnePoint);
System.arraycopy(yOnePosition, 0, yOneResetPosition, xOnePoint, xOneOffset);
核心代碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int xOnePoint = yOnePosition.length - xOneOffset;
System.arraycopy(yOnePosition, xOneOffset, yOneResetPosition, 0, xOnePoint);
System.arraycopy(yOnePosition, 0, yOneResetPosition, xOnePoint, xOneOffset);
int xTwoPoint = yTwoPosition.length - xTwoOffset;
System.arraycopy(yTwoPosition, xTwoOffset, yTwoResetPosition, 0, xTwoPoint);
System.arraycopy(yTwoPosition, 0, yTwoResetPosition, xOnePoint, xTwoOffset);
for(int i = 0; i < mTotalHeight; i ++){
// 畫豎線,從起始位置(0, mTotalHeight - 0 - 100)畫到終點坐標(0, mTotalHeight)
canvas.drawLine(i, mTotalHeight - yOneResetPosition[i] - 50, i, mTotalHeight, sinPaint);
canvas.drawLine(i, mTotalHeight - yTwoResetPosition[i] - 50, i, mTotalHeight, cosPaint);
}
// 每次移動多少個坐標
xOneOffset += xOneSpeed;
xTwoOffset += xTwoSpeed;
// 橫坐標如果超過控件的寬度,則從0開始循環(huán)
if(xOneOffset >= mTotalWidth){
xOneOffset = 0;
}
if(xTwoOffset >= mTotalWidth){
xTwoOffset = 0;
}
postInvalidate();
}
球狀
猜想:
- 利用 Xfermode 使一個圓形與波形圖混合;
- 利用 Xfermode 使一個圓形與一個與控件同等高寬的矩形混合;
- 找 UI 小姐姐給一個遮罩圖。
經(jīng)過測試:
- 圓形與波形圖混合,可能因為 Xfermode 混合圖層消耗也是很大的,再加上不斷地 draw 波形圖,每 draw 一次都會去混合一次,所以運行起來很不流暢,很卡頓的感覺。
- 第一個方法行不通,所以采用的這個方法。這時新建了一個圖層來混合,使其形成一個空心的圓,放在波形圖的上面。只會混合一次,所以消耗不大。
- 第三種方法,我是最先嘗試的,這個必然是可行的,在 layout 布局文件中處理。只是畢竟程序猿都喜歡用代碼來解決。
方法2 的核心代碼如下:
for (int i = 0; i < mTotalWidth; i++) {
canvas.drawLine(i, mTotalHeight - mResetOneYPositions[i] - getLevelNumber(),
i, mTotalHeight, mWavePaint);
canvas.drawLine(i, mTotalHeight - mResetTwoYPositions[i] - getLevelNumber(),
i, mTotalHeight, mWavePaintN);
}
// 將當前畫布保存起來,在此行代碼以后的繪制,都是在新的圖層中。
int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
mWavePaint.setColor(Color.WHITE);
// 畫一個和控件寬高相同的矩形
canvas.drawRect(0, 0, getWidth(), getHeight(), mWavePaint);
// 設置混合模式為 DST_OUT,即在矩形中掏一個圓出來。
mWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
// 畫一個半徑為控件寬高 1/2 的圓
canvas.drawCircle(getWidth()/2, getHeight()/2, circleRadius, mWavePaint);
mWavePaint.setXfermode(null);
// 還原保存的畫布
canvas.restoreToCount(sc);
mWavePaint.setColor(WAVE_COLOR);
本文主要講控件的實現(xiàn),關于更多的 Xfermode 學習煩請自行搜索,它還有很多神奇的用法,相信不會讓你失望。
顏色
為了使波形的顏色值可以動態(tài)控制,可通過布局文件中自定義屬性來實現(xiàn)。
- 創(chuàng)建資源文件
定義兩個顏色屬性名,和類型
2.在自定義控件的構(gòu)造函數(shù)中獲取屬性值
public GoiWaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.GoiWaveView);
SIN_WAVE_COLOR = typedArray.getColor(R.styleable.GoiWaveView_sin_color, Color.BLUE);
COS_WAVE_COLOR = typedArray.getColor(R.styleable.GoiWaveView_cos_color, Color.RED);
}
3.在 layout 布局文件中給值
<?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">
<com.goileo.cview.view.GoiWaveView
android:id="@+id/wave_view_goi"
android:layout_width="100dp"
android:layout_height="100dp"
app:cos_color="@color/colorPrimary"
app:sin_color="#abcdef"
android:layout_gravity="center" />
</LinearLayout>
運行就能看到效果了。
高度
高度則需通過公共方法來傳參。
public float getLevelNumber() {
return levelNumber;
}
public void setLevelNumber(double levelNumbers) {
levelNumber = (int)((double)(levelNumbers * (double)mTotalHeight / 1000));
this.invalidate();
}
這里設置成 1000 ,是為了讓波形上升時看上去更平緩。
寫在后頭
本文主要記錄了自定義球狀水波紋升級控件的實現(xiàn)過程,其中包括波形繪制、Xfermode混合技術應用、自定義屬性等。
如果有錯誤之處,煩請告知或指正。
演示:
推薦閱讀:Android 熱修復 - 各框架原理學習及對比
Android 熱修復 - Tinker 實現(xiàn)及踩過的坑
記錄在此,僅為學習!
感謝您的閱讀!歡迎指正!
歡迎加入 Android 技術交流群,群號:155495090。