3.6 自定义View
Android给我们提供了丰富的组件库来创建丰富的UI效果,同时也提供了非常方便的拓展方法。通过继承Android的系统组件,我们可以非常方便地拓展现有功能,在系统组件的基础上创建新的功能,甚至可以直接自定义一个控件,实现Android系统控件所没有的功能。自定义控件作为Android中一个非常重要的功能,一直以来都被初学者认为是代表高手的象征。其实,自定义View并没有想象中的那么难,与其说是在自定义一个View,不如说是在设计一个图形,只有站在一个设计者的角度上,才可以更好地创建自定义View。我们不能机械地记忆所有绘图的API,而是要让这些API为你所用,结合现实中的绘图方法,甚至是Photoshop的技巧,才能设计出更好的自定义View。
适当地使用自定义View,可以丰富应用程序的体验效果,但滥用自定义View则会带来适得其反的效果。一个让用户觉得熟悉的控件,才是一个好的控件。如果一味追求酷炫的效果而创建自定义View,则会让用户觉得华而不实。而且,在系统原生控件可以实现功能的基础上,系统也提供了主题、图片资源、各种风格来创建丰富的UI。这些控件都是经过了Android一代代版本迭代后的产物。即使这样,在如今的版本中,依然还存在不少Bug,更不要提我们自定义的View了。特别是现在Android ROM的多样性,导致Android的适配变得越来越复杂,很难保证自定义View在其他手机上也能达到你想要的效果。
当然,了解Android系统自定义View的过程,可以帮助我们了解系统的绘图机制。同时,在适当的情况下也可以通过自定义View来帮我们创建更加灵活的布局。
在自定义View时,我们通常会去重写onDraw()方法来绘制View的显示内容。如果该View还需要使用wrap_content属性,那么还必须重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。
在View中通常有以下一些比较重要的回调方法。
- onFinishInflate():从XML加载组件后回调。
- onSizeChanged():组件大小改变时回调。
- onMeasure():回调该方法来进行测量。
- onLayout():回调该方法来确定显示的位置。
- onTouchEvent():监听到触摸事件时回调。
当然,创建自定义View的时候,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。这也是Android控件架构灵活性的体现。
通常情况下,有以下三种方法来实现自定义的控件。
- 对现有控件进行拓展
- 通过组合来实现新的控件
- 重写View来实现全新的控件
3.6.1 对现有控件进行拓展
这是一个非常重要的自定义View方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等。一般来说,我们可以在onDraw()方法中对原生控件行为进行拓展。
下面以一个TextView为例,来看看如何使用拓展原生控件的方法创建新的控件。比如想让一个TextView的背景更加丰富,给其多绘制几层背景,如图3.6所示。
图3.6 自定义修改TextView
我们先来分析一下如何实现这个效果,原生的TextView使用onDraw()方法绘制要显示的文字。当继承了系统的TextView之后,如果不重写其onDraw()方法,则不会修改TextView的任何效果。可以认为在自定义的TextView中调用TextView类的onDraw()方法来绘制了显示的文字,代码如下所示。
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
程序调用super.onDraw(canvas)方法来实现原生控件的功能,但是在调用super.onDraw()方法之前和之后,我们都可以实现自己的逻辑,分别在系统绘制文字前后,完成自己的操作,即如下所示。
- @Override
- protected void onDraw(Canvas canvas) {
- //在回调父类方法前,实现自己的逻辑,对TextView来说即是在绘制文本内容前
- super.onDraw(canvas);
- //在回调父类方法后,实现自己的逻辑,对TextView来说即是在绘制文本内容后
- }
以上就是通过改变控件的绘制行为创建自定义View的思路。有了上面的分析,我们就可以很轻松地实现图3.6所示的自定义TextView了。我们在构造方法中完成必要对象的初始化工作,如初始化画笔等,代码如下所示。
- mPaint1 = new Paint();
- mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
- mPaint1.setStyle(Paint.Style.FILL);
- mPaint2 = new Paint();
- mPaint2.setColor(Color.YELLOW);
- mPaint2.setStyle(Paint.Style.FILL);
而代码中最重要的部分则是在onDraw()方法中,为了改变原生的绘制行为,在系统调用super.onDraw(canvas)方法前,也就是在绘制文字之下,绘制两个不同大小的矩形,形成一个重叠效果,再让系统调用super.onDraw(canvas)方法,执行绘制文字的工作。这样,我们就通过改变控件绘制行为,创建了一个新的控件,代码如下所示。
- //绘制外层矩形
- canvas.drawRect(
- 0,
- 0,
- getMeasuredWidth(),
- getMeasuredHeight(),
- mPaint1);
- //绘制内层矩形
- canvas.drawRect(
- 10,
- 10,
- getMeasuredWidth() - 10,
- getMeasuredHeight() - 10,
- mPaint2);
- canvas.save();
- //绘制文字前平移10像素
- canvas.translate(10, 0);
- //父类完成的方法,即绘制文本
- super.onDraw(canvas);
- canvas.restore();
下面再来看一个稍微复杂一点的TextView。在前面一个实例中,我们直接使用了Canvas对象来进行图像的绘制,然后利用Android的绘图机制,可以绘制出更加复杂丰富的图像。比如可以利用LinearGradient Shader和Matrix来实现一个动态的文字闪动效果,程序运行效果如图3.7所示。
图3.7 闪动的文字效果</h4>
要想实现这一个效果,可以充分利用Android中Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制要显示的文字。首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽带设置一个LinearGradient渐变渲染器,代码如下所示。
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mViewWidth == 0) {
- mViewWidth = getMeasuredWidth();
- if (mViewWidth > 0) {
- mPaint = getPaint();
- mLinearGradient = new LinearGradient(
- 0,
- 0,
- mViewWidth,
- 0,
- new int[]{
- Color.BLUE, 0xffffffff,
- Color.BLUE},
- null,
- Shader.TileMode.CLAMP);
- mPaint.setShader(mLinearGradient);
- mGradientMatrix = new Matrix();
- }
- }
- }
其中最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后,在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的闪动效果,代码如下所示。
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (mGradientMatrix != null) {
- mTranslate += mViewWidth / 5;
- if (mTranslate > 2 * mViewWidth) {
- mTranslate = -mViewWidth;
- }
- mGradientMatrix.setTranslate(mTranslate, 0);
- mLinearGradient.setLocalMatrix(mGradientMatrix);
- postInvalidateDelayed(100);
- }
- }
3.6.2 创建复合控件
创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的拓展性。下面就以一个TopBar为示例,讲解如何创建复合控件。
我们知道为了应用程序风格的统一,很多应用程序都有一些共通的UI界面,比如图3.8中所示的TopBar这样一个标题栏。
图3.8 界面上的TopBar
通常情况下,这些界面都会被抽象出来,形成一个共通的UI组件。所有需要添加标题栏的界面都会引用这样一个TopBar,而不是每个界面都在布局文件中写这样一个TopBar。同时,设计者还可以给TopBar增加相应的接口,让调用者能够更加灵活地控制TopBar,这样不仅可以提高界面的复用率,更能在需要修改UI时,做到快速修改,而不需要对每个页面的标题栏都进行修改。
下面我们就来看看该如何创建一个这样的UI模板。首先,模板应该具有通用性与可定制性。也就是说,我们需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息,而不是所有的模板都一样,那样就失去了模板的意义。
3.6.2.1 定义属性
为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <declare-styleable name="TopBar">
- <attr name="title" format="string" />
- <attr name="titleTextSize" format="dimension" />
- <attr name="titleTextColor" format="color" />
- <attr name="leftTextColor" format="color" />
- <attr name="leftBackground" format="reference|color" />
- <attr name="leftText" format="string" />
- <attr name="rightTextColor" format="color" />
- <attr name="rightBackground" format="reference|color" />
- <attr name="rightText" format="string" />
- </declare-styleable>
- </resources>
我们在代码中通过<declare-styleable>标签声明了使用自定义属性,并通过name属性来确定引用的名称。最后,通过<attr>标签来声明具体的自定义属性,比如在这里定义了标题文字的字体、大小、颜色,左边按钮的文字颜色、背景、字体,右边按钮的文字颜色、背景、字体等属性,并通过format属性来指定属性的类型。这里需要注意的就是,有些属性可以是颜色属性,也可以是引用属性。比如按钮的背景,可以把它指定为具体的颜色,也可以把它指定为一张图片,所以使用“|”来分隔不同的属性——“reference|color”。
在确定好属性后,就可以创建一个自定义控件——TopBar,并让它继承自ViewGroup,从而组合一些需要的控件。这里为了简单,我们继承RelativeLayout。在构造方法中,通过如下所示代码来获取在XML布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样。
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleable的TopBar,就是我们在XML中通过<declare-styleable name="TopBar">所指定的name名。接下来,通过TypedArray对象的getString()、getColor()等方法,就可以获取这些定义的属性值,代码如下所示。
- //通过这个方法,将你在atts.xml中定义的declare-styleable
- //的所有属性的值存储到TypedArray中
- TypedArray ta = context.obtainStyledAttributes(attrs,
- R.styleable.TopBar);
- //从TypedArray中取出对应的值来为要设置的属性赋值
- mLeftTextColor = ta.getColor(
- R.styleable.TopBar_leftTextColor, 0);
- mLeftBackground = ta.getDrawable(
- R.styleable.TopBar_leftBackground);
- mLeftText = ta.getString(R.styleable.TopBar_leftText);
- mRightTextColor = ta.getColor(
- R.styleable.TopBar_rightTextColor, 0);
- mRightBackground = ta.getDrawable(
- R.styleable.TopBar_rightBackground);
- mRightText = ta.getString(R.styleable.TopBar_rightText);
- mTitleTextSize = ta.getDimension(
- R.styleable.TopBar_titleTextSize, 10);
- mTitleTextColor = ta.getColor(
- R.styleable.TopBar_titleTextColor, 0);
- mTitle = ta.getString(R.styleable.TopBar_title);
- //获取完TypedArray的值后,一般要调用
- // recyle方法来避免重新创建的时候的错误
- ta.recycle();
这里需要注意的是,当获取完所有的属性值后,需要调用TypedArray的recyle方法来完成资源的回收。
3.6.2.2 组合控件
接下来,我们就可以开始组合控件了。UI模板TopBar实际上由三个控件组成,即左边的点击按钮mLeftButton,右边的点击按钮mRightButton和中间的标题栏mTitleView。通过动态添加控件的方式,使用addView()方法将这三个控件加入到定义的TopBar模板中,并给它们设置我们前面所获取到的具体的属性值,比如标题的文字颜色、大小等,代码如下所示。
- mLeftButton = new Button(context);
- mRightButton = new Button(context);
- mTitleView = new TextView(context);
- //为创建的组件元素赋值
- //值就来源于我们在引用的xml文件中给对应属性的赋值
- mLeftButton.setTextColor(mLeftTextColor);
- mLeftButton.setBackground(mLeftBackground);
- mLeftButton.setText(mLeftText);
- mRightButton.setTextColor(mRightTextColor);
- mRightButton.setBackground(mRightBackground);
- mRightButton.setText(mRightText);
- mTitleView.setText(mTitle);
- mTitleView.setTextColor(mTitleTextColor);
- mTitleView.setTextSize(mTitleTextSize);
- mTitleView.setGravity(Gravity.CENTER);
- //为组件元素设置相应的布局元素
- mLeftParams = new LayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.MATCH_PARENT);
- mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
- //添加到ViewGroup
- addView(mLeftButton, mLeftParams);
- mRightParams = new LayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.MATCH_PARENT);
- mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
- addView(mRightButton, mRightParams);
- mTitlepParams = new LayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.MATCH_PARENT);
- mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
- addView(mTitleView, mTitlepParams);
那么如何来给这两个左、右按钮设计点击事件呢?既然是UI模板,那么每个调用者所需要这些按钮能够实现的功能都是不一样的。因此,不能直接在UI模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者,实现过程如下所示。
- 定义接口
在UI模板类中定义一个左右按钮点击的接口,并创建两个方法,分别用于左边按钮的点击和右边按钮的点击,代码如下所示。
- //接口对象,实现回调机制,在回调方法中
- //通过映射的接口对象调用接口中的方法
- //而不用去考虑如何实现,具体的实现由调用者去创建
- public interface topbarClickListener {
- //左按钮点击事件
- void leftClick();
- //右按钮点击事件
- void rightClick();
- }
- 暴露接口给调用者
在模板方法中,为左、右按钮增加点击事件,但不去实现具体的逻辑,而是调用接口中相应的点击方法,代码如下所示。
- //按钮的点击事件,不需要具体的实现,
- //只需调用接口的方法,回调的时候,会有具体的实现
- mRightButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- mListener.rightClick();
- }
- });
- mLeftButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- mListener.leftClick();
- }
- });
- //暴露一个方法给调用者来注册接口回调
- //通过接口来获得回调者对接口方法的实现
- public void setOnTopbarClickListener(topbarClickListener mListener) {
- this.mListener = mListener;
- }
- 实现接口回调
在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法,确定具体的实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常情况下,可以使用匿名内部类的形式来实现接口中的方法,代码如下所示。
- mTopbar.setOnTopbarClickListener(
- new TopBar.topbarClickListener() {
- @Override
- public void rightClick() {
- Toast.makeText(TopBarTest.this,
- "right", Toast.LENGTH_SHORT)
- .show();
- }
- @Override
- public void leftClick() {
- Toast.makeText(TopBarTest.this,
- "left", Toast.LENGTH_SHORT)
- .show();
- }
- });
这里为了简单演示,只显示两个Toast来区分不同的按钮点击事件。除了通过接口回调的方式来实现动态的控制UI模板,同样可以使用公共方法来动态地修改UI模板中的UI,这样就进一步提高了模板的可定制性,代码如下所示。
- /**
- *设置按钮的显示与否通过id区分按钮,flag区分是否显示
- *
- * @param id id
- * @param flag是否显示
- * /
- public void setButtonVisable(int id, boolean flag) {
- if (flag) {
- if (id == 0) {
- mLeftButton.setVisibility(View.VISIBLE);
- } else {
- mRightButton.setVisibility(View.VISIBLE);
- }
- } else {
- if (id == 0) {
- mLeftButton.setVisibility(View.GONE);
- } else {
- mRightButton.setVisibility(View.GONE);
- }
- }
- }
通过如上所示代码,当调用者通过TopBar对象调用这个方法后,根据参数,调用者就可以动态地控制按钮的显示,代码如下所示。
- //控制topbar上组件的状态
- mTopbar.setButtonVisable(0, true);
- mTopbar.setButtonVisable(1, false);
3.6.2.3 引用UI模板
最后一步,自然是在需要使用的地方引用UI模板,在引用前,需要指定引用第三方控件的名字空间。在布局文件中,可以看到如下一行代码。
- xmlns:android="http://schemas.android.com/apk/res/android"
这行代码就是在指定引用的名字控件xmlns,即xml namespace。这里指定了名字空间为“android”,因此在接下来使用系统属性的时候,才可以使用“android:”来引用Android的系统属性。同样地,如果要使用自定义的属性,那么就需要创建自己的名字空间,在Android Studio中,第三方的控件都使用如下代码来引入名字空间。
- xmlns:custom="http://schemas.android.com/apk/res-auto"
这里我们将引入的第三方控件的名字空间取名为custom,之后在XML文件中使用自定义的属性时,就可以通过这个名字空间来引用,代码如下所示。
- <com.imooc.systemwidget.TopBar
- android:id="@+id/topBar"
- android:layout_width="match_parent"
- android:layout_height="40dp"
- custom:leftBackground="@drawable/blue_button"
- custom:leftText="Back"
- custom:leftTextColor="#FFFFFF"
- custom:rightBackground="@drawable/blue_button"
- custom:rightText="More"
- custom:rightTextColor="#FFFFFF"
- custom:title="自定义标题"
- custom:titleTextColor="#123412"
- custom:titleTextSize="10sp" />
使用自定义的View与系统原生的View最大的区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。
再更近一步,如果将这个UI模板写到一个布局文件中,代码如下所示。
- <com.xys.mytopbar.Topbar xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:custom="http://schemas.android.com/apk/res-auto"
- android:id="@+id/topBar"
- android:layout_width="match_parent"
- android:layout_height="40dp"
- custom:leftBackground="@drawable/blue_button"
- custom:leftText="Back"
- custom:leftTextColor="#FFFFFF"
- custom:rightBackground="@drawable/blue_button"
- custom:rightText="More"
- custom:rightTextColor="#FFFFFF"
- custom:title="自定义标题"
- custom:titleTextColor="#123412"
- custom:titleTextSize="15sp">
- </com.xys.mytopbar.Topbar>
通过如上所示的代码,我们就可以在其他的布局文件中,直接通过<include>标签来引用这个UI模板View,代码如下所示。
- <include layout="@layout/topbar" />
这样就更加满足了我们的模板需求。
运行程序后,显示效果如图3.9所示。
图3.9 组合控件
当调用公共方法setButtonVisable()来控制左右两个按钮的显示和隐藏的时候,显示效果如图3.10所示。
图3.10 隐藏右按钮
3.6.3 重写View来实现全新的控件
当Android系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现需要的功能。创建一个自定义View,难点在于绘制控件和实现交互,这也是评价一个自定义View优劣的标准之一。通常需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义View的可定制性。
下面就通过几个实例,让大家了解如何创建一个自定义View,不过为了让程序尽可能简单,这里就不去自定义属性值了。
3.6.3.1 弧线展示图
在PPT的很多模板中,都有如图3.11所示的这样一张比例图。
图3.11 比例图
这个比例图可以非常清楚地展示一个项目所占的比例,简洁明了。因此,实现这样一个自定义View用在我们的程序中,可以让整个程序实现比较清晰的数据展示效果。那么该如何创建一个这样的自定义View呢?很明显,这个自定义View其实分为三个部分,分别是中间的圆形、中间显示的文字和外圈的弧线。既然有了这样的思路,只要在onDraw()方法中一个个去绘制就可以了。这里为了简单,我们把View的绘制长度直接设置为屏幕的宽度。首先,在初始化的时候,设置好绘制三种图形的参数。圆的代码如下所示。
- mCircleXY = length / 2;
- mRadius = (float) (length * 0.5 / 2);
绘制弧线,需要指定其椭圆的外接矩形,代码如下所示。
- mArcRectF = new RectF(
- (float) (length * 0.1),
- (float) (length * 0.1),
- (float) (length * 0.9),
- (float) (length * 0.9));
绘制文字,只需要设置好文字的起始绘制位置即可。
接下来,我们就可以在onDraw()方法中进行绘制了,代码如下所示。
- //绘制圆
- canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
- //绘制弧线
- canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint); //绘制文字
- canvas.drawText(mShowText, 0, mShowText.length(),
- mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
相信这些图形如果单独让你去绘制,应该是非常容易的事情,只是这里进行了一下组合,就创建了一个新的View。其实,不论是多么复杂的图形、控件,它都是由这些最基本的图形绘制出来的,关键就在于你如何去分解、设计这些图形,当你的脑海中有了一幅设计图之后,剩下的事情就只是对坐标的计算了。
当然,对于这个简单的View,有一些方法可以让调用者来设置不同的状态值,代码如下所示。
- public void setSweepValue(float sweepValue) {
- if (sweepValue != 0) {
- mSweepValue = sweepValue;
- } else {
- mSweepValue = 25;
- }
- this.invalidate();
- }
例如,当用户不指定具体的比例值时,可以默认设置为25,而调用者可以通过如下代码来设置相应的比例值。
- CircleProgressView circle =
- (CircleProgressView) findViewById(R.id.circle);
- circle.setSweepValue(70);
3.6.3.2 音频条形图
以下这个问题来源于群里一位开发者的问题,他想实现类似在PC上某些音乐播放器上根据音频音量大小显示的音频条形图,如图3.12所示。
下面就教大家如何实现这个简单的案例。由于只是演示自定义View的用法,我们就不去真实地监听音频输入了,随机模拟一些数字即可。这个实例比上面比例图的实例稍微复杂一点,主要复杂在绘制的坐标计算和动画效果上,我们先来看一下最终实现的效果图,如图3.12所示。
图3.12 音频条形图
如果要实现一个如图3.13所示的静态音频条形图,相信大家应该可以很快找到思路,也就是绘制一个个的矩形,每个矩形之间稍微偏移一点距离即可。如下代码就展示了一种计算坐标的方法。
图3.13 静态音频条形图
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- for (int i = 0; i < mRectCount; i++) { canvas.drawRect(
- (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
- currentHeight,
- (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
- mRectHeight,
- mPaint);
- }
- }
如上代码中,我们通过循环创建这些小的矩形,其中currentHeight就是每个小矩形的高,通过横坐标的不断偏移,就绘制出了这些静态的小矩形。下面我们再让这些小矩形的高度进行随机变化,通过Math.random()方法来随机改变这些高度值,并赋值给currentHeight,代码如下所示。
- mRandom = Math.random();
- float currentHeight = (float) (mRectHeight * mRandom);
这样,我们就完成了静态效果的绘制,那么如何实现动态效果呢?其实非常简单,只要在onDraw()方法中再去调用invalidate()方法通知View进行重绘就可以了。不过,在这里不需要每次一绘制完新的矩形就通知View进行重绘,这样会因为刷新速度太快反而影响效果。因此,我们可以使用如下代码来进行View的延迟重绘,代码如下所示。
- postInvalidateDelayed(300);
这样每隔300ms通知View进行重绘,就可以得到一个比较好的视觉效果了。最后,为了让自定义View更加逼真,可以在绘制小矩形的时候,给绘制的Paint对象增加一个LinearGradient渐变效果,这样不同高度的矩形就会有不同颜色的渐变效果,更加能够模拟音频条形图的风格,代码如下所示。
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mWidth = getWidth();
- mRectHeight = getHeight();
- mRectWidth = (int) (mWidth * 0.6 / mRectCount);
- mLinearGradient = new LinearGradient(
- 0,
- 0,
- mRectWidth,
- mRectHeight,
- Color.YELLOW,
- Color.BLUE,
- Shader.TileMode.CLAMP);
- mPaint.setShader(mLinearGradient);
- }
从这个例子中,我们可以知道,在创建自定义View的时候,需要一步一步来,从一个基本的效果开始,慢慢地增加功能,绘制更复杂的效果。不论是多么复杂的自定义View,它一定是慢慢迭代起来的功能,所以不要觉得自定义View有多难。千里之行始于足下,只要开始做,慢慢地就能越来越熟练。
