Android雙波浪自定義控件(DoubleWaveView)

  發現淘寶個人頁頂部的自定義控件很炫酷啊有沒有(IOS端),它這裏是一個動態的雙波紋效果,由於IOS端的效果它有週期性地漸變振幅的功能,比較複雜。對於振幅的漸變效果,當時就想着是怎麼實現的,冥思苦想了老半天不得果(每次都重新計算設置正弦函數值,有點太耗費性能了)。
  後面又拿起安卓機看了一下安卓客戶端的效果,結果發現是個靜態的雙波紋,What ? 和IOS的差距咋那麼大? 想了想,淘寶應該是出於對於安卓機器性能參差不齊的考慮,所以他們的研發人員就沒有在安卓機實現動態效果。好了,發一下截圖對比:

Android端的效果:

這裏寫圖片描述

 IOS端的效果:

這裏寫圖片描述

  對於一個有追求的攻城獅來說,一知半解是最不能忍受的,所以說幹就幹,咱也弄一個出來。秉着IT界的真理:“不要重複造輪子”的思想,先在網上查資料看有沒有人造好了這個輪子。果然,有類似的博客,但是很多人評論說性能消耗很大,實際使用會比較卡,尤其是在低端機上面。類似效果的博客地址:http://blog.csdn.net/tianjian4592/article/details/44222565。鑑於這樣的情況,就自己進行優化吧~按照慣例,先發一下我最後實現出來的效果:
  
  doublewave Gif效果圖
  
如果你只是抱着直接拿來用的心態的話,這個自定義控件我上傳到JCenter上面去了,可以直接

1.引入依賴項目:

compile 'com.xiaosong520:doublewaveview:1.0.1' 

2.然後在佈局文件中:

 <com.doublewave.DoubleWaveView
        android:id="@+id/waveView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        DoubleWaveView:speedOne="8"
        DoubleWaveView:speedTwo="6"
        DoubleWaveView:peakValue="20dp"
        DoubleWaveView:waveHeight="200dp"
        DoubleWaveView:waveColor="@color/colorBlue"/>

由於是自定義控件,需要在根佈局中添加適配:
xmlns:DoubleWaveView="http://schemas.android.com/apk/res-auto"

3.在Activity中:

 waveView = (DoubleWaveView) findViewById(R.id.waveView);
 waveView.setAnim(false);//默認是開啓動畫效果的,選false可關閉
 waveView.setAnim(true);//如果已經關閉,重新設置True開啓動畫

簡單的三步,就可以使用了。

如果你是想理解透它的實現原理,那麼請繼續往下看~

在開始碼碼碼之前,得做好準備工作,俗話說得好,磨刀不誤砍柴工嘛,先搞清楚實現思路:

 1.確定水波紋的正弦函數方程;

 2.根據函數方程得出每一個波紋上點的座標(單位:px),並保存到一維數組中;

 3.根據正弦函數的座標不斷繪製豎直直線,形成一個靜態波紋圖;

 4.不斷調用onDraw方法,改變正弦函數Y值進行重繪,生成動態水波紋。

  
  Step 1:生成波紋曲線
  
  波紋曲線是利用正弦函數來實現的,正弦函數的方程式:y = A*sin(ωx+b)+h
一開始自己也懵逼了,這幾個參數分別都是什麼來着了啊?努力回憶中。。。 當年的理科學霸小正太現在已然成了老年癡呆社會青年,感覺自己應該是讀了個假高中。關於正弦函數的定義,如果你也忘記了的話,自行Google 百度溫習一下知識吧~查找資料後可以確定:w影響週期,A影響振幅,h影響y軸位置,b爲初相;週期T = 2π/ω。

   Step 2:自定義DoubleWaveView的屬性

  在步驟一中,我們已經瞭解了正弦函數的各個參數的含義以及計算方法。那麼接下來我們就開始自定義View:
  首先創建一個DoubleWaveView類 ,繼承自View。 由於波紋的顏色、振幅、移動速度等,可能會根據實際情況需要變動,所以我們接下來在項目的res-values目錄下創建一個attrs的xml 文件,用於創建自定義屬性。關於自定義View的步驟如果還不是蠻太懂的話,可以補習一下自定義View 的知識,推薦張鴻洋的這篇博客: Android 自定義View (一)
  
這裏我定義的attrs屬性如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="peakValue" format="dimension"/>
    <attr name="waveColor" format="color" />
    <attr name="speedOne" format="integer" />
    <attr name="speedTwo" format="integer" />
    <attr name="waveHeight" format="dimension" />

    <declare-styleable name="DoubleWaveView">
        <attr name="peakValue" />
        <attr name="waveColor" />
        <attr name="speedOne" />
        <attr name="speedTwo" />
        <attr name="waveHeight" />

    </declare-styleable>

</resources>

正弦函數方程式: y = A*sin(wx+b)+h
五個屬性代表的含義:

peakValue :振幅 ,即對應的是A
waveHeight:波浪距離控件底部的高度。描述起來可能有點歧義,看下面的圖就明白了

這裏寫圖片描述

speedOne:第一條波浪的移動速度
speedTwo:第二條波浪的移動速度
waveColor:水波的顏色

Step 3: 繪製雙波紋圖形

  1.先把之前在attr.xml中定義好的屬性值,通過構造函數獲取到,並設置好畫筆,代碼都有註釋,就不多說了,如果有疑問可以回覆討論:

    public DoubleWaveView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public DoubleWaveView(Context context)
    {
        this(context, null);
    }

    public DoubleWaveView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        //獲取自定義屬性值
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DoubleWaveView, defStyle, 0);
        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.DoubleWaveView_peakValue:
                    //振幅默認是30dp
                    STRETCH_FACTOR_A =  a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 30, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.DoubleWaveView_waveColor:
                    WAVE_PAINT_COLOR = a.getColor(attr, 0x881E90FF);//默認是藍色
                    break;
                case R.styleable.DoubleWaveView_speedOne:
                    TRANSLATE_X_SPEED_ONE = a.getInteger(attr,7);//默認是7
                    break;
                case R.styleable.DoubleWaveView_speedTwo:
                    TRANSLATE_X_SPEED_TWO = a.getInteger(attr,5);//默認是5
                    break;
                case R.styleable.DoubleWaveView_waveHeight:
                    WaveHeight = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics()));//默認100dp
                    break;
            }
        }
        a.recycle();

        // 將dp轉化爲px,用於控制不同分辨率上移動速度基本一致
        mXOffsetSpeedOne = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_ONE);
        mXOffsetSpeedTwo = DensityUtil.dip2px(context, TRANSLATE_X_SPEED_TWO);

        // 初始繪製波紋的畫筆
        mWavePaint = new Paint();
        // 去除畫筆鋸齒
        mWavePaint.setAntiAlias(true);
        // 設置風格爲實線
        mWavePaint.setStyle(Paint.Style.FILL);
        // 設置畫筆顏色
        mWavePaint.setColor(WAVE_PAINT_COLOR);
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    }

其中需要用到的密度轉換工具類:

/**
 * @TODO<分辨率轉換工具類>
 * @author 小嵩
 * @date 2016-8-3 09:20:46
 */
public class DensityUtil {
    /**
     * 根據手機的分辨率從 dp 的單位 轉成爲 px(像素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根據手機的分辨率從 px(像素) 的單位 轉成爲 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }
}

2.覆蓋onDraw方法和onSizeChanged方法

3.在onSizeChanged方法中,獲取控件的寬高,根據寬高計算正弦波紋週期以及對應的Y值:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 記錄下控件設置的寬高
        mTotalWidth = w;
        mTotalHeight = h;
        // 一維數組, 用於保存原始波紋的y值
        mYPositions = new float[mTotalWidth];

        // 將Sin函數週期定爲view總寬度, ω = 2π/T
        mCycleFactorW = (float) (2 * Math.PI / mTotalWidth);

        // 根據view總寬度得出所有對應的y值,即計算出正弦圖形對應位置
        for (int i = 0; i < mTotalWidth; i++) {
            mYPositions[i] = (float) (STRETCH_FACTOR_A * Math.sin(mCycleFactorW * i) + OFFSET_Y);
        }
    }

4.在onDraw方法中繪製雙波紋圖形,繪製方法是:
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint),幾個參數代表的含義應該都能看明白吧,分別是起點X、Y值,終點X、Y值,畫筆對象。

  其實就是在豎直方向一條一條直線地畫,起點是從onSizeChanged中得到的正弦函數Y值的數組中得到的,而終點,就是控件底部的座標。
畫筆繪製過程如下圖所示:

這裏寫圖片描述

  所以控件寬度的像素有多少,一個波紋圖形需要調用多少次drawLine方法。

Step 4: 不斷重繪雙波紋圖形,形成動態效果

在繪製完成整個靜態圖形的過程後,通過 postInvalidate()方法來通知系統更新UI ,其實就是相當於重新調用了onDraw方法,從而進行平移,實現動態的效果。相關代碼如下。

網上的那些例子,繪製方案大都是每次平移都拷貝4次數組,造成很大的內存開銷:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
        //中間省略
        ...
        resetPositonY();//拷貝兩個波浪正弦函數值的數組(每調用一次onDraw,都需要4次數組的拷貝)
for (int i = 0; i < mTotalWidth; i++) {

            // 減500只是爲了控制波紋繪製的y的在屏幕的位置(高度),大家可以改成一個變量,然後動態改變這個變量,從而形成波紋上升下降效果

            // 繪製第一條水波紋(豎直方向)
            canvas.drawLine(i, mTotalHeight - mResetOneYPositions[i] - 500, i, mTotalHeight, mWavePaint);

            // 繪製第二條水波紋(豎直方向)
            canvas.drawLine(i, mTotalHeight - mResetTwoYPositions[i] - 500, i, mTotalHeight, mWavePaint);
        }
   }
private void resetPositonY() {//Copy數組
        // mXOneOffset代表當前第一條水波紋要移動的距離
        int yOneInterval = mYPositions.length - mXOneOffset;
        // 使用System.arraycopy方式重新填充第一條波紋的數據
        System.arraycopy(mYPositions, mXOneOffset, mResetOneYPositions, 0, yOneInterval);
        System.arraycopy(mYPositions, 0, mResetOneYPositions, yOneInterval, mXOneOffset);

        int yTwoInterval = mYPositions.length - mXTwoOffset;
        System.arraycopy(mYPositions, mXTwoOffset, mResetTwoYPositions, 0,
                yTwoInterval);
        System.arraycopy(mYPositions, 0, mResetTwoYPositions, yTwoInterval, mXTwoOffset);
    }

以上方法不推薦使用,太耗費內存了,實際使用的效果會很卡,所以這裏我整理了一下,綜合別人的處理方案做了優化:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //從canvas層面去除繪製時的鋸齒
        canvas.setDrawFilter(mDrawFilter);
        for(int i=0,j=0,k=0;i<mTotalWidth;i++){

            if(i+mXOneOffset<mTotalWidth){//第一條波紋圖形繪製
                canvas.drawLine(i,mTotalHeight-mYPositions[mXOneOffset+i]-WaveHeight,i,mTotalHeight,mWavePaint);
            }else {//大於週期值,則設置爲j(與相位相關,已移動的X距離,最大值爲一個週期,即控件的寬度)
                canvas.drawLine(i,mTotalHeight-mYPositions[j]-WaveHeight,i,mTotalHeight,mWavePaint);
                j++;
            }

            if(i+mXTwoOffset<mTotalWidth){//第二條波紋圖形繪製
                canvas.drawLine(i,mTotalHeight-mYPositions[mXTwoOffset+i]-WaveHeight,i,mTotalHeight,mWavePaint);
            }else {//大於週期值,則設置爲k(與相位相關,已移動的X距離)
                canvas.drawLine(i,mTotalHeight-mYPositions[k]-WaveHeight,i,mTotalHeight,mWavePaint);
                k++;
            }

        }

        // 改變兩條波紋的移動點
        mXOneOffset += mXOffsetSpeedOne;
        mXTwoOffset += mXOffsetSpeedTwo;

        // 如果已經移動到結尾處,則重頭記錄
        if (mXOneOffset >= mTotalWidth) {
            mXOneOffset = 0;
        }
        if (mXTwoOffset > mTotalWidth) {
            mXTwoOffset = 0;
        }

        // 引發view重繪,可以考慮延遲10-30ms重繪,空出時間繪製
        if (isAnim){
            new Thread(mRunnable).start();
        }
    }

    private Runnable mRunnable = new Runnable() {

        @Override
        public void run() {
            {
                try {
                    //界面更新的頻率,每20ms更新一次界面
                    Thread.sleep(20);
                    //通知系統更新界面,相當於調用了onDraw函數
                    postInvalidate();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };

另外我還完善了動畫效果的啓動和暫停設置,有興趣可下載sample demo查看完整代碼 。

GitHub 地址 :DoubleWave 雙波紋自定義View

發佈了40 篇原創文章 · 獲贊 126 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章