Andriod 自定义控件之音频条

今天我们实现一个直接继承于View的全新控件。大家都知道音乐播放器吧,在点击一首歌进行播放时,通常会有一块区域用于显示音频条,我们今天就来学习下,播放器音频条的实现。

首先我们还是先定义一个类,直接继承于View,并重写它的构造方法,并初始化一个画笔,这和上一节是同样的道理。直接贴出代码:

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()方法。

@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 来计数)。然后通过循环创建矩形的方式,让系统给我们画出我们所定义的视图。当然这里我还随机产生了一个随机数,用于控制矩形的高度。

ok,把它加入到我们的布局文件中,并在Activity中显示出来看看是什么效果吧:

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类

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

现在运行程序看看效果吧。
这里写图片描述

是不是很酷呢?但是有伙伴该有疑问了,音频条不都是动态的吗?现在我们实现的只是静态的矩形条呀,别急,我们现在让它动起来,但是该怎么实现呢?

有经验的伙伴都知道,我们所使用或定义的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的情况进行必要的考虑。好了,今天就说到这里吧。

更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。

这里写图片描述

posted @ 2016-11-28 09:41  管满满  阅读(629)  评论(0编辑  收藏  举报