Andriod 自定義控件之音頻條

今天咱們實現一個直接繼承於View的全新控件。你們都知道音樂播放器吧,在點擊一首歌進行播放時,一般會有一塊區域用於顯示音頻條,咱們今天就來學習下,播放器音頻條的實現。python

首先咱們仍是先定義一個類,直接繼承於View,並重寫它的構造方法,並初始化一個畫筆,這和上一節是一樣的道理。直接貼出代碼:android

public class AudioBar extends View{

    private Paint mTextPaint;
    
    public AudioBar(Context context) {
        this(context,null);
    }
    public AudioBar(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public AudioBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        mTextPaint = new TextPaint();
        mTextPaint.setColor(Color.RED);
    }
 }

而後一樣的道理,想要定義咱們本身的View控件,咱們須要重寫View的onDraw()方法。canvas

@Override
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
}

有聽過或是播放音樂的夥伴大都知道音頻條是什麼樣子的,無非就是來回跳動的不一樣豎形圖,在這裏咱們稍微轉換下思想就知道,在咱們android中能夠以豎形矩形來實現,各個矩形之間以固定的間距分割開來就能模仿實現咱們的目標控件-音頻條。先貼出代碼,稍候看代碼解釋:微信

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        width = getMeasuredWidth() ;
        height = getMeasuredHeight();
        int mRectCount = 0;
        for(int count = 5 ; count < width ;count += mRectWidth){
            mRectCount ++ ;
        }
        for(int i = 0 ; i < mRectCount ; i ++){
            double mRandom = Math.random();
            mRectHeight = (float) (height * mRandom);
            canvas.drawRect(offset + mRectWidth * i,
                    mRectHeight,
                    mRectWidth * (i+1),
                    height,
                    mTextPaint);
        }
    }

好,來看下這段代碼,首先是咱們先獲取手機頻幕的尺寸大小,而後我會根據手機頻幕尺寸和預先定義出的矩形寬度(這裏使用mRectWidth變量)來計算出當前手機頻幕能夠容納多少個矩形(使用mRectCount 來計數)。而後經過循環建立矩形的方式,讓系統給咱們畫出咱們所定義的視圖。固然這裏我還隨機產生了一個隨機數,用於控制矩形的高度。dom

ok,把它加入到咱們的佈局文件中,並在Activity中顯示出來看看是什麼效果吧:ide

activity_main.xml文件佈局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.sanhuimusic.mycustomview.MainActivity">
    <com.sanhuimusic.mycustomview.view.AudioBar
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
   
</LinearLayout>

而後MainActivity類post

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

如今運行程序看看效果吧。
這裏寫圖片描述學習

是否是很酷呢?可是有夥伴該有疑問了,音頻條不都是動態的嗎?如今咱們實現的只是靜態的矩形條呀,別急,咱們如今讓它動起來,可是該怎麼實現呢?ui

有經驗的夥伴都知道,咱們所使用或定義的UI視圖都是在onDraw()繪製完成以後在Activity中顯示出來的,那麼咱們要實現動態的視圖是否是能夠不停的調用該方法呢?又有什麼方法能夠不停的調用它使它不停的繪製呢?答案顯而易見,使用invalidate();方法,它能夠不停的從新繪製View。由於使用invalidate();間隔過短,速度太快,因此根據咱們的需求,咱們可使用延遲的方法重繪View,在這裏咱們使用postInvalidateDelayed(500);讓它500毫秒重畫一次,這樣就能夠體現了動態的音頻條。你們能夠試下,動態圖不太會搞,我就不貼圖了,你能夠跑下程序了。

ok,如今已基本符合咱們的要求了,是否是送了一口氣呢,尚未,你有沒有試試在layout文件中爲咱們自定義的控件添加padding屬性呢,試試吧。哈哈,是否是也木有任何改變呢?

那是由於咱們在onDraw()方法中沒有考慮到這一狀況的發生。在自定義控件中,直接繼承View時,必需要考慮到padding屬性對控件的影響,因此接下來,讓咱們的控件貼近原生控件吧。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int leftPadding = getPaddingLeft();
        int topPadding = getPaddingTop();
        int rightPadding = getPaddingRight();
        int bottomPadding = getPaddingBottom();
        width = getMeasuredWidth() - leftPadding - rightPadding;
        height = getMeasuredHeight()- topPadding - bottomPadding;
        int mRectCount = 0;
        for(int count = 5 ; count < width ;count += mRectWidth){
            mRectCount ++ ;
        }
        for(int i = 0 ; i < mRectCount ; i ++){
            double mRandom = Math.random();
            mRectHeight = (float) (height * mRandom);
            canvas.drawRect(offset + mRectWidth * i,
                    mRectHeight,
                    mRectWidth * (i+1),
                    height,
                    mTextPaint);
        }
        postInvalidateDelayed(500);
    }

也至關的好理解,根據當前情景對padding屬性進行控制一下就ok了,小夥伴們如今趕忙在運行試試吧。

到這裏整個自定義控件已差很少完成,可是細心的夥伴可能會發現:咱們製做的音頻條不可能佔據整個頻幕呀,嘿嘿,這個比較簡單,咱們一般的作法是修改一下佈局文件不就行嘍,好,修改以下:
activity_main.xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.sanhuimusic.mycustomview.MainActivity">
    <com.sanhuimusic.mycustomview.view.AudioBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
</LinearLayout>

ok,大功告成,在運行下,試試。

我倒,怎麼徹底沒有木有變化啊,檢查檢查,仍是木有問題,究竟是哪一個出問題了呢,我想你該蒙了。

這時候你該經過搜索或是書籍查詢了,(10秒鐘之後,哈哈)經過了解你大概明白了問題所在,View的工做流程是在onDraw繪製以前,是須要先測量佈局的,這裏引入了兩個名詞,測量,和佈局。後面我想針對View的工做流程專門作一節學習,因此,咱們如今只須要先了解下View測量的工做是在哪進行的。

好,通過查詢資料,咱們瞭解到,View的測量工做是在onMeasure()方法中進行的。接下來讓咱們看看它究竟是怎麼測量的,而咱們在當前場景下使用wrap_content爲何沒有效果?帶着問題,咱們先重寫View的onMeasure()方法,以下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

而後跟到super.onMeasure(widthMeasureSpec, heightMeasureSpec);源碼中,咱們所看到的源碼很簡單,以下,

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

在方法體中只是調用了setMeasuredDimension();方法來決定View尺寸的,再看它裏面的參數是經過getDefaultSize()方法獲取大小,再次跟進getDefaultSize()方法中。

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

ok,很簡單的源碼,主要經過MeasureSpec類(後面會詳細講解)獲取測量的模式和測量的大小,而後經過測量的模式來決定測量的大小,可是有一點是否是很奇怪呢,當測量模式爲AT_MOST(最大值模式,對應的是layout寬高屬性是wrap_content)時它的測量大小和模式爲EXACTLY(精確值模式,對應的是layout寬高屬性是match_parent)的測量大小同樣呢,由於咱們恍然大悟,系統默認的測量大小不論是layout寬高屬性是wrap_content仍是match_parent它的取值都是match_parent是的默認值。

由此能夠明白,咱們在修改了layout寬高屬性值時,並無達到咱們預期的但願。那該怎麼解決呢?其實也很簡單,由於,View測量大小的取值取決於setMeasuredDimension()這個方法,所以只要咱們重寫了setMeasuredDimension()方法,就能夠完成咱們的需求。所以,咱們能夠進行以下操做:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

            if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(getMeasuredWidth()/2 , getMeasuredHeight()/2);
            } else if(widthSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(getMeasuredWidth()/2 ,heightSpecSize);
            } else if(heightSpecMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSpecSize ,getMeasuredHeight()/2);
        }
    }

代碼解釋:首先咱們分別先獲得控件測量的模式和大小,而後根據狀況分別識別當前View屬性屬於哪一種情景,再根據具體的情景進行重寫了setMeasuredDimension()方法。這裏我是讓它各顯示屏幕的一半。好,來看看如今是否符合了咱們的需求。
這裏寫圖片描述

好了,徹底符合需求,能夠開心下了。

總結下:當咱們直接繼承View實現自定義控件時,主要困難點就在於座標系的計算,計算出正確的座標,自定義的控件也就完成大半了,另外還有須要針對padding屬性和layout_width 和 layout_height屬性值爲wrap_content的狀況進行必要的考慮。好了,今天就說到這裏吧。

更多資訊請關注微信平臺,有博客更新會及時通知。愛學習愛技術。

這裏寫圖片描述

相關文章
相關標籤/搜索