时间:2022-05-08 10:04:45 | 栏目:Android代码 | 点击:次
今天我们实现一个直接继承于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的情况进行必要的考虑。好了,今天就说到这里吧。