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

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

    这里写图片描述

  • 相关阅读:
    命令拷屏之网络工具
    PHP 设计模式 笔记与总结(1)命名空间 与 类的自动载入
    Java实现 计蒜客 1251 仙岛求药
    Java实现 计蒜客 1251 仙岛求药
    Java实现 计蒜客 1251 仙岛求药
    Java实现 蓝桥杯 算法训练 字符串合并
    Java实现 蓝桥杯 算法训练 字符串合并
    Java实现 蓝桥杯 算法训练 字符串合并
    Java实现 LeetCode 143 重排链表
    Java实现 LeetCode 143 重排链表
  • 原文地址:https://www.cnblogs.com/guanmanman/p/6108265.html
Copyright © 2011-2022 走看看