• 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的情况进行必要的考虑。好了,今天就说到这里吧。

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

    这里写图片描述

  • 相关阅读:
    Task 和 Function
    FPGA中双向端口的设计原理及仿真
    EDK实用实例之LED
    分频电路设计(笔记)
    你了解Promise么
    配置vue多页
    Chrome控制台console的那些属性
    关于读书
    django常用命令
    django 简易博客开发 2 模板和数据查询
  • 原文地址:https://www.cnblogs.com/xieweikai/p/6837952.html
Copyright © 2020-2023  润新知