自定義動態View:Android 自定義View實例

前言

Android 開發中自定義View的重要性不言而喻,這里就結合github上一個自定義view的代碼來進行分析,代碼中提供了動態效果,使用兩種方式來生成了自定義View。
效果圖:


image

以下是介紹。

自定義View

在安卓開發藝術探索中,自定義View被分為4類

  1. 繼承View重寫onDraw方法
  2. 繼承ViewGroup派生特殊的Layout
  3. 繼承特定的View
  4. 繼承特定的ViewGroup

這里就使用上述1和4來實現效果。

繼承View重寫onDraw方法

這種方法主要用于實現一些不規則的效果,需要自己重寫onDraw方法,同時需要自己支持wrap_content,并且padding也需要自己處理,這里因為實現的兩個View會添加到一個重寫的ViewGroup里,所以沒有實現。
首先是簡單的Solid:

class Solid extends View {
    // This is a Rect to cover the main view
    private Paint aboveWavePaint;
    private Paint blowWavePaint;

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

    public Solid(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
       // 這里為1則在LinearLayout中會填滿剩余空間
        params.weight = 1;
        setLayoutParams(params);
    }

    public void setAboveWavePaint(Paint aboveWavePaint) {
        this.aboveWavePaint = aboveWavePaint;
    }

    public void setBlowWavePaint(Paint blowWavePaint) {
        this.blowWavePaint = blowWavePaint;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(getLeft(), 0, getRight(), getBottom(), blowWavePaint);
        canvas.drawRect(getLeft(), 0, getRight(), getBottom(), aboveWavePaint);
    }
}

Solid實現的效果很簡單,從onDraw中可以看到,Solid畫了兩個正方形,效果是實現了WaveView的下面部分,也就是波浪線的下方。

接下來是Wave,波浪線,不重要的代碼被去除了,只關注最重要的部分:

// y=Asin(ωx+φ)+k
class Wave extends View {
    ......
    // ω
    private double omega;

    public Wave(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.waveViewStyle);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mBlowWavePath, mBlowWavePaint);
        canvas.drawPath(mAboveWavePath, mAboveWavePaint);
    }

    ......

    /**
     * calculate wave track
     * this is why is can wave
     * y=Asin(ωx+φ)+k
     */
    private void calculatePath() {
        mAboveWavePath.reset();
        mBlowWavePath.reset();

        getWaveOffset();

        float y;
        mAboveWavePath.moveTo(left, bottom);
        // calculate the path by sin
        for (float x = 0; x <= mMaxRight; x += X_SPACE) {
            y = (float) (mWaveHeight * Math.sin(omega * x + mAboveOffset) + mWaveHeight);
            mAboveWavePath.lineTo(x, y);
        }
        mAboveWavePath.lineTo(right, bottom);

        mBlowWavePath.moveTo(left, bottom);
        for (float x = 0; x <= mMaxRight; x += X_SPACE) {
            y = (float) (mWaveHeight * Math.sin(omega * x + mBlowOffset) + mWaveHeight);
            mBlowWavePath.lineTo(x, y);
        }
        mBlowWavePath.lineTo(right, bottom);
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        if (View.GONE == visibility) {
            removeCallbacks(mRefreshProgressRunnable);
        } else {
            removeCallbacks(mRefreshProgressRunnable);
            mRefreshProgressRunnable = new RefreshProgressRunnable();
            post(mRefreshProgressRunnable);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus) {
            if (mWaveLength == 0) {
                startWave();
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mWaveLength==0){
            startWave();
        }
    }

    private void startWave() {
        if (getWidth() != 0) {
            int width = getWidth();
            mWaveLength = width * mWaveMultiple;
            left = getLeft();
            right = getRight();
            bottom = getBottom() + 2;
            mMaxRight = right + X_SPACE;
            omega = PI2 / mWaveLength;
        }
    }

    private void getWaveOffset() {
        if (mBlowOffset > Float.MAX_VALUE - 100) {
            mBlowOffset = 0;
        } else {
            mBlowOffset += mWaveHz;
        }

        if (mAboveOffset > Float.MAX_VALUE - 100) {
            mAboveOffset = 0;
        } else {
            mAboveOffset += mWaveHz;
        }
    }

    private class RefreshProgressRunnable implements Runnable {
        public void run() {
            synchronized (Wave.this) {
                long start = System.currentTimeMillis();

                calculatePath();

                invalidate();
                // update every 16ms
                long gap = 16 - (System.currentTimeMillis() - start);
                postDelayed(this, gap < 0 ? 0 : gap);
            }
        }
    }
}

Wave的邏輯是這樣的:

當窗口可視時,如果View不是GONE則調用post方法后臺運行一個RefreshProgressRunnable對象
RefreshProgressRunnable對象則調用calculatePath方法計算要畫的路徑,然后調用postDelayed方法,在16ms內再次計算路徑
calculatePath方法中則根據y=Asin(ωx+φ)+k,x以一定的間隔增大,從左到右的計算函數值并調用path的lineTo方法勾畫路徑,k則為設置的高度值,是水波的最高值
調用invalidate方法更新視圖
onDraw方法中根據路徑重畫

繼承特定的ViewGroup實現自定義View

這里是WaveView,它繼承了LinearLayout,里面添加了Wave和Solid兩個View,Wave負責水波紋效果,Solid則是水面下。

public class WaveView extends LinearLayout {
  ......

    public WaveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOrientation(VERTICAL);
        ......
        addView(mWave);
        addView(mSolid);
        setProgress(mProgress);
    }

    // The height of Wave
    public void setProgress(int progress) {
        this.mProgress = progress > 100 ? 100 : progress;
        computeWaveToTop();
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus) {
            computeWaveToTop();
        }
    }

    private void computeWaveToTop() {
        mWaveToTop = (int) (getHeight() * (1f - mProgress / 100f));
        ViewGroup.LayoutParams params = mWave.getLayoutParams();
        if (params != null) {
            ((LayoutParams) params).topMargin = mWaveToTop;
        }
        mWave.setLayoutParams(params);
    }
    ......
}

這個就比較簡單了,computeWaveToTop計算wave視圖到頂部的距離,以此實現WaveView progress的變化。

總結

上面這個例子實現了自定義View,繼承View 重寫了onDraw方法,同時繼承LinearLayout,通過組合加上多線程的更新,達到了一個良好的效果。這也同時說明了自定義View對我們開發者有多重要,它通過特殊的排列組合能夠完成令人眼前一亮的效果,大大拓展了安卓開發界面的效果。

參考資料

《Android開發藝術探索》
WaveView的github鏈接

個人思考

有時候會覺得遇到了瓶頸,有時候會不知道做什么,這時候就學點新東西吧,看看書吧,這種時候就是我們能夠大進步的時候,有瓶頸說明我們有不足,即使無法認清我們的不足之處,我們也可以學點新的東西,把舊知識弄的深入。會有進步的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容