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 自定义View - 图1 图3.6 自定义修改TextView

我们先来分析一下如何实现这个效果,原生的TextView使用onDraw()方法绘制要显示的文字。当继承了系统的TextView之后,如果不重写其onDraw()方法,则不会修改TextView的任何效果。可以认为在自定义的TextView中调用TextView类的onDraw()方法来绘制了显示的文字,代码如下所示。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. }

程序调用super.onDraw(canvas)方法来实现原生控件的功能,但是在调用super.onDraw()方法之前和之后,我们都可以实现自己的逻辑,分别在系统绘制文字前后,完成自己的操作,即如下所示。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. //在回调父类方法前,实现自己的逻辑,对TextView来说即是在绘制文本内容前
  4. super.onDraw(canvas);
  5. //在回调父类方法后,实现自己的逻辑,对TextView来说即是在绘制文本内容后
  6. }

以上就是通过改变控件的绘制行为创建自定义View的思路。有了上面的分析,我们就可以很轻松地实现图3.6所示的自定义TextView了。我们在构造方法中完成必要对象的初始化工作,如初始化画笔等,代码如下所示。

  1. mPaint1 = new Paint();
  2. mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
  3. mPaint1.setStyle(Paint.Style.FILL);
  4. mPaint2 = new Paint();
  5. mPaint2.setColor(Color.YELLOW);
  6. mPaint2.setStyle(Paint.Style.FILL);

而代码中最重要的部分则是在onDraw()方法中,为了改变原生的绘制行为,在系统调用super.onDraw(canvas)方法前,也就是在绘制文字之下,绘制两个不同大小的矩形,形成一个重叠效果,再让系统调用super.onDraw(canvas)方法,执行绘制文字的工作。这样,我们就通过改变控件绘制行为,创建了一个新的控件,代码如下所示。

  1. //绘制外层矩形
  2. canvas.drawRect(
  3. 0,
  4. 0,
  5. getMeasuredWidth(),
  6. getMeasuredHeight(),
  7. mPaint1);
  8. //绘制内层矩形
  9. canvas.drawRect(
  10. 10,
  11. 10,
  12. getMeasuredWidth() - 10,
  13. getMeasuredHeight() - 10,
  14. mPaint2);
  15. canvas.save();
  16. //绘制文字前平移10像素
  17. canvas.translate(10, 0);
  18. //父类完成的方法,即绘制文本
  19. super.onDraw(canvas);
  20. canvas.restore();

下面再来看一个稍微复杂一点的TextView。在前面一个实例中,我们直接使用了Canvas对象来进行图像的绘制,然后利用Android的绘图机制,可以绘制出更加复杂丰富的图像。比如可以利用LinearGradient Shader和Matrix来实现一个动态的文字闪动效果,程序运行效果如图3.7所示。

3.6 自定义View - 图2 图3.7 闪动的文字效果</h4>

要想实现这一个效果,可以充分利用Android中Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制要显示的文字。首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽带设置一个LinearGradient渐变渲染器,代码如下所示。

  1. @Override
  2. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  3. super.onSizeChanged(w, h, oldw, oldh);
  4. if (mViewWidth == 0) {
  5. mViewWidth = getMeasuredWidth();
  6. if (mViewWidth > 0) {
  7. mPaint = getPaint();
  8. mLinearGradient = new LinearGradient(
  9. 0,
  10. 0,
  11. mViewWidth,
  12. 0,
  13. new int[]{
  14. Color.BLUE, 0xffffffff,
  15. Color.BLUE},
  16. null,
  17. Shader.TileMode.CLAMP);
  18. mPaint.setShader(mLinearGradient);
  19. mGradientMatrix = new Matrix();
  20. }
  21. }
  22. }

其中最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后,在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的闪动效果,代码如下所示。

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. if (mGradientMatrix != null) {
  5. mTranslate += mViewWidth / 5;
  6. if (mTranslate > 2 * mViewWidth) {
  7. mTranslate = -mViewWidth;
  8. }
  9. mGradientMatrix.setTranslate(mTranslate, 0);
  10. mLinearGradient.setLocalMatrix(mGradientMatrix);
  11. postInvalidateDelayed(100);
  12. }
  13. }

3.6.2 创建复合控件

创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的拓展性。下面就以一个TopBar为示例,讲解如何创建复合控件。

我们知道为了应用程序风格的统一,很多应用程序都有一些共通的UI界面,比如图3.8中所示的TopBar这样一个标题栏。

3.6 自定义View - 图3 图3.8 界面上的TopBar

通常情况下,这些界面都会被抽象出来,形成一个共通的UI组件。所有需要添加标题栏的界面都会引用这样一个TopBar,而不是每个界面都在布局文件中写这样一个TopBar。同时,设计者还可以给TopBar增加相应的接口,让调用者能够更加灵活地控制TopBar,这样不仅可以提高界面的复用率,更能在需要修改UI时,做到快速修改,而不需要对每个页面的标题栏都进行修改。

下面我们就来看看该如何创建一个这样的UI模板。首先,模板应该具有通用性与可定制性。也就是说,我们需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息,而不是所有的模板都一样,那样就失去了模板的意义。

3.6.2.1 定义属性

为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过如下代码定义相应的属性即可。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3.  
  4. <declare-styleable name="TopBar">
  5. <attr name="title" format="string" />
  6. <attr name="titleTextSize" format="dimension" />
  7. <attr name="titleTextColor" format="color" />
  8. <attr name="leftTextColor" format="color" />
  9. <attr name="leftBackground" format="reference|color" />
  10. <attr name="leftText" format="string" />
  11. <attr name="rightTextColor" format="color" />
  12. <attr name="rightBackground" format="reference|color" />
  13. <attr name="rightText" format="string" />
  14. </declare-styleable>
  15.  
  16. </resources>

我们在代码中通过<declare-styleable>标签声明了使用自定义属性,并通过name属性来确定引用的名称。最后,通过<attr>标签来声明具体的自定义属性,比如在这里定义了标题文字的字体、大小、颜色,左边按钮的文字颜色、背景、字体,右边按钮的文字颜色、背景、字体等属性,并通过format属性来指定属性的类型。这里需要注意的就是,有些属性可以是颜色属性,也可以是引用属性。比如按钮的背景,可以把它指定为具体的颜色,也可以把它指定为一张图片,所以使用“|”来分隔不同的属性——“reference|color”。

在确定好属性后,就可以创建一个自定义控件——TopBar,并让它继承自ViewGroup,从而组合一些需要的控件。这里为了简单,我们继承RelativeLayout。在构造方法中,通过如下所示代码来获取在XML布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样。

  1. TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleable的TopBar,就是我们在XML中通过<declare-styleable name="TopBar">所指定的name名。接下来,通过TypedArray对象的getString()、getColor()等方法,就可以获取这些定义的属性值,代码如下所示。

  1. //通过这个方法,将你在atts.xml中定义的declare-styleable
  2. //的所有属性的值存储到TypedArray中
  3. TypedArray ta = context.obtainStyledAttributes(attrs,
  4. R.styleable.TopBar);
  5. //从TypedArray中取出对应的值来为要设置的属性赋值
  6. mLeftTextColor = ta.getColor(
  7. R.styleable.TopBar_leftTextColor, 0);
  8. mLeftBackground = ta.getDrawable(
  9. R.styleable.TopBar_leftBackground);
  10. mLeftText = ta.getString(R.styleable.TopBar_leftText);
  11.  
  12. mRightTextColor = ta.getColor(
  13. R.styleable.TopBar_rightTextColor, 0);
  14. mRightBackground = ta.getDrawable(
  15. R.styleable.TopBar_rightBackground);
  16.  
  17. mRightText = ta.getString(R.styleable.TopBar_rightText);
  18.  
  19. mTitleTextSize = ta.getDimension(
  20. R.styleable.TopBar_titleTextSize, 10);
  21. mTitleTextColor = ta.getColor(
  22. R.styleable.TopBar_titleTextColor, 0);
  23. mTitle = ta.getString(R.styleable.TopBar_title);
  24.  
  25. //获取完TypedArray的值后,一般要调用
  26. // recyle方法来避免重新创建的时候的错误
  27. ta.recycle();

这里需要注意的是,当获取完所有的属性值后,需要调用TypedArray的recyle方法来完成资源的回收。

3.6.2.2 组合控件

接下来,我们就可以开始组合控件了。UI模板TopBar实际上由三个控件组成,即左边的点击按钮mLeftButton,右边的点击按钮mRightButton和中间的标题栏mTitleView。通过动态添加控件的方式,使用addView()方法将这三个控件加入到定义的TopBar模板中,并给它们设置我们前面所获取到的具体的属性值,比如标题的文字颜色、大小等,代码如下所示。

  1. mLeftButton = new Button(context);
  2. mRightButton = new Button(context);
  3. mTitleView = new TextView(context);
  4.  
  5. //为创建的组件元素赋值
  6. //值就来源于我们在引用的xml文件中给对应属性的赋值
  7. mLeftButton.setTextColor(mLeftTextColor);
  8. mLeftButton.setBackground(mLeftBackground);
  9. mLeftButton.setText(mLeftText);
  10.  
  11. mRightButton.setTextColor(mRightTextColor);
  12. mRightButton.setBackground(mRightBackground);
  13.  
  14. mRightButton.setText(mRightText);
  15.  
  16. mTitleView.setText(mTitle);
  17. mTitleView.setTextColor(mTitleTextColor);
  18. mTitleView.setTextSize(mTitleTextSize);
  19. mTitleView.setGravity(Gravity.CENTER);
  20.  
  21. //为组件元素设置相应的布局元素
  22. mLeftParams = new LayoutParams(
  23. LayoutParams.WRAP_CONTENT,
  24. LayoutParams.MATCH_PARENT);
  25. mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
  26. //添加到ViewGroup
  27. addView(mLeftButton, mLeftParams);
  28.  
  29. mRightParams = new LayoutParams(
  30. LayoutParams.WRAP_CONTENT,
  31. LayoutParams.MATCH_PARENT);
  32. mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
  33. addView(mRightButton, mRightParams);
  34.  
  35. mTitlepParams = new LayoutParams(
  36. LayoutParams.WRAP_CONTENT,
  37. LayoutParams.MATCH_PARENT);
  38. mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
  39. addView(mTitleView, mTitlepParams);

那么如何来给这两个左、右按钮设计点击事件呢?既然是UI模板,那么每个调用者所需要这些按钮能够实现的功能都是不一样的。因此,不能直接在UI模板中添加具体的实现逻辑,只能通过接口回调的思想,将具体的实现逻辑交给调用者,实现过程如下所示。

  • 定义接口

在UI模板类中定义一个左右按钮点击的接口,并创建两个方法,分别用于左边按钮的点击和右边按钮的点击,代码如下所示。

  1. //接口对象,实现回调机制,在回调方法中
  2. //通过映射的接口对象调用接口中的方法
  3. //而不用去考虑如何实现,具体的实现由调用者去创建
  4. public interface topbarClickListener {
  5. //左按钮点击事件
  6. void leftClick();
  7. //右按钮点击事件
  8. void rightClick();
  9. }
  • 暴露接口给调用者

在模板方法中,为左、右按钮增加点击事件,但不去实现具体的逻辑,而是调用接口中相应的点击方法,代码如下所示。

  1. //按钮的点击事件,不需要具体的实现,
  2. //只需调用接口的方法,回调的时候,会有具体的实现
  3. mRightButton.setOnClickListener(new OnClickListener() {
  4.  
  5. @Override
  6. public void onClick(View v) {
  7. mListener.rightClick();
  8. }
  9. });
  10.  
  11. mLeftButton.setOnClickListener(new OnClickListener() {
  12.  
  13. @Override
  14. public void onClick(View v) {
  15. mListener.leftClick();
  16. }
  17. });
  18.  
  19. //暴露一个方法给调用者来注册接口回调
  20. //通过接口来获得回调者对接口方法的实现
  21.  
  22. public void setOnTopbarClickListener(topbarClickListener mListener) {
  23. this.mListener = mListener;
  24. }
  • 实现接口回调

在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法,确定具体的实现逻辑,并使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常情况下,可以使用匿名内部类的形式来实现接口中的方法,代码如下所示。

  1. mTopbar.setOnTopbarClickListener(
  2. new TopBar.topbarClickListener() {
  3.  
  4. @Override
  5. public void rightClick() {
  6. Toast.makeText(TopBarTest.this,
  7. "right", Toast.LENGTH_SHORT)
  8. .show();
  9. }
  10.  
  11. @Override
  12. public void leftClick() {
  13. Toast.makeText(TopBarTest.this,
  14. "left", Toast.LENGTH_SHORT)
  15. .show();
  16. }
  17. });

这里为了简单演示,只显示两个Toast来区分不同的按钮点击事件。除了通过接口回调的方式来实现动态的控制UI模板,同样可以使用公共方法来动态地修改UI模板中的UI,这样就进一步提高了模板的可定制性,代码如下所示。

  1. /**
  2. *设置按钮的显示与否通过id区分按钮,flag区分是否显示
  3. *
  4. * @param id id
  5. * @param flag是否显示
  6. * /
  7. public void setButtonVisable(int id, boolean flag) {
  8. if (flag) {
  9. if (id == 0) {
  10. mLeftButton.setVisibility(View.VISIBLE);
  11. } else {
  12. mRightButton.setVisibility(View.VISIBLE);
  13. }
  14. } else {
  15. if (id == 0) {
  16. mLeftButton.setVisibility(View.GONE);
  17. } else {
  18. mRightButton.setVisibility(View.GONE);
  19. }
  20. }
  21. }

通过如上所示代码,当调用者通过TopBar对象调用这个方法后,根据参数,调用者就可以动态地控制按钮的显示,代码如下所示。

  1. //控制topbar上组件的状态
  2. mTopbar.setButtonVisable(0, true);
  3. mTopbar.setButtonVisable(1, false);

3.6.2.3 引用UI模板

最后一步,自然是在需要使用的地方引用UI模板,在引用前,需要指定引用第三方控件的名字空间。在布局文件中,可以看到如下一行代码。

  1. xmlns:android="http://schemas.android.com/apk/res/android"

这行代码就是在指定引用的名字控件xmlns,即xml namespace。这里指定了名字空间为“android”,因此在接下来使用系统属性的时候,才可以使用“android:”来引用Android的系统属性。同样地,如果要使用自定义的属性,那么就需要创建自己的名字空间,在Android Studio中,第三方的控件都使用如下代码来引入名字空间。

  1. xmlns:custom="http://schemas.android.com/apk/res-auto"

这里我们将引入的第三方控件的名字空间取名为custom,之后在XML文件中使用自定义的属性时,就可以通过这个名字空间来引用,代码如下所示。

  1. <com.imooc.systemwidget.TopBar
  2. android:id="@+id/topBar"
  3. android:layout_width="match_parent"
  4. android:layout_height="40dp"
  5. custom:leftBackground="@drawable/blue_button"
  6. custom:leftText="Back"
  7. custom:leftTextColor="#FFFFFF"
  8. custom:rightBackground="@drawable/blue_button"
  9. custom:rightText="More"
  10. custom:rightTextColor="#FFFFFF"
  11. custom:title="自定义标题"
  12. custom:titleTextColor="#123412"
  13. custom:titleTextSize="10sp" />

使用自定义的View与系统原生的View最大的区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmlns名字。

再更近一步,如果将这个UI模板写到一个布局文件中,代码如下所示。

  1. <com.xys.mytopbar.Topbar xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:custom="http://schemas.android.com/apk/res-auto"
  3. android:id="@+id/topBar"
  4. android:layout_width="match_parent"
  5. android:layout_height="40dp"
  6. custom:leftBackground="@drawable/blue_button"
  7. custom:leftText="Back"
  8. custom:leftTextColor="#FFFFFF"
  9.  
  10. custom:rightBackground="@drawable/blue_button"
  11. custom:rightText="More"
  12. custom:rightTextColor="#FFFFFF"
  13. custom:title="自定义标题"
  14. custom:titleTextColor="#123412"
  15. custom:titleTextSize="15sp">
  16.  
  17. </com.xys.mytopbar.Topbar>

通过如上所示的代码,我们就可以在其他的布局文件中,直接通过<include>标签来引用这个UI模板View,代码如下所示。

  1. <include layout="@layout/topbar" />

这样就更加满足了我们的模板需求。

运行程序后,显示效果如图3.9所示。

3.6 自定义View - 图4 图3.9 组合控件

当调用公共方法setButtonVisable()来控制左右两个按钮的显示和隐藏的时候,显示效果如图3.10所示。

3.6 自定义View - 图5 图3.10 隐藏右按钮

3.6.3 重写View来实现全新的控件

当Android系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现需要的功能。创建一个自定义View,难点在于绘制控件和实现交互,这也是评价一个自定义View优劣的标准之一。通常需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑。当然,我们还可以像实现组合控件方式那样,通过引入自定义属性,丰富自定义View的可定制性。

下面就通过几个实例,让大家了解如何创建一个自定义View,不过为了让程序尽可能简单,这里就不去自定义属性值了。

3.6.3.1 弧线展示图

在PPT的很多模板中,都有如图3.11所示的这样一张比例图。

3.6 自定义View - 图6 图3.11 比例图

这个比例图可以非常清楚地展示一个项目所占的比例,简洁明了。因此,实现这样一个自定义View用在我们的程序中,可以让整个程序实现比较清晰的数据展示效果。那么该如何创建一个这样的自定义View呢?很明显,这个自定义View其实分为三个部分,分别是中间的圆形、中间显示的文字和外圈的弧线。既然有了这样的思路,只要在onDraw()方法中一个个去绘制就可以了。这里为了简单,我们把View的绘制长度直接设置为屏幕的宽度。首先,在初始化的时候,设置好绘制三种图形的参数。圆的代码如下所示。

  1. mCircleXY = length / 2;
  2. mRadius = (float) (length * 0.5 / 2);

绘制弧线,需要指定其椭圆的外接矩形,代码如下所示。

  1. mArcRectF = new RectF(
  2. (float) (length * 0.1),
  3. (float) (length * 0.1),
  4. (float) (length * 0.9),
  5. (float) (length * 0.9));

绘制文字,只需要设置好文字的起始绘制位置即可。

接下来,我们就可以在onDraw()方法中进行绘制了,代码如下所示。

  1. //绘制圆
  2. canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
  3. //绘制弧线
  4. canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint); //绘制文字
  5. canvas.drawText(mShowText, 0, mShowText.length(),
  6. mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);

相信这些图形如果单独让你去绘制,应该是非常容易的事情,只是这里进行了一下组合,就创建了一个新的View。其实,不论是多么复杂的图形、控件,它都是由这些最基本的图形绘制出来的,关键就在于你如何去分解、设计这些图形,当你的脑海中有了一幅设计图之后,剩下的事情就只是对坐标的计算了。

当然,对于这个简单的View,有一些方法可以让调用者来设置不同的状态值,代码如下所示。

  1. public void setSweepValue(float sweepValue) {
  2. if (sweepValue != 0) {
  3. mSweepValue = sweepValue;
  4. } else {
  5. mSweepValue = 25;
  6. }
  7. this.invalidate();
  8. }

例如,当用户不指定具体的比例值时,可以默认设置为25,而调用者可以通过如下代码来设置相应的比例值。

  1. CircleProgressView circle =
  2. (CircleProgressView) findViewById(R.id.circle);
  3. circle.setSweepValue(70);

3.6.3.2 音频条形图

以下这个问题来源于群里一位开发者的问题,他想实现类似在PC上某些音乐播放器上根据音频音量大小显示的音频条形图,如图3.12所示。

下面就教大家如何实现这个简单的案例。由于只是演示自定义View的用法,我们就不去真实地监听音频输入了,随机模拟一些数字即可。这个实例比上面比例图的实例稍微复杂一点,主要复杂在绘制的坐标计算和动画效果上,我们先来看一下最终实现的效果图,如图3.12所示。

3.6 自定义View - 图7 图3.12 音频条形图

如果要实现一个如图3.13所示的静态音频条形图,相信大家应该可以很快找到思路,也就是绘制一个个的矩形,每个矩形之间稍微偏移一点距离即可。如下代码就展示了一种计算坐标的方法。

3.6 自定义View - 图8 图3.13 静态音频条形图

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. for (int i = 0; i < mRectCount; i++) { canvas.drawRect(
  5. (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
  6. currentHeight,
  7. (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)),
  8. mRectHeight,
  9. mPaint);
  10. }
  11. }

如上代码中,我们通过循环创建这些小的矩形,其中currentHeight就是每个小矩形的高,通过横坐标的不断偏移,就绘制出了这些静态的小矩形。下面我们再让这些小矩形的高度进行随机变化,通过Math.random()方法来随机改变这些高度值,并赋值给currentHeight,代码如下所示。

  1. mRandom = Math.random();
  2. float currentHeight = (float) (mRectHeight * mRandom);

这样,我们就完成了静态效果的绘制,那么如何实现动态效果呢?其实非常简单,只要在onDraw()方法中再去调用invalidate()方法通知View进行重绘就可以了。不过,在这里不需要每次一绘制完新的矩形就通知View进行重绘,这样会因为刷新速度太快反而影响效果。因此,我们可以使用如下代码来进行View的延迟重绘,代码如下所示。

  1. postInvalidateDelayed(300);

这样每隔300ms通知View进行重绘,就可以得到一个比较好的视觉效果了。最后,为了让自定义View更加逼真,可以在绘制小矩形的时候,给绘制的Paint对象增加一个LinearGradient渐变效果,这样不同高度的矩形就会有不同颜色的渐变效果,更加能够模拟音频条形图的风格,代码如下所示。

  1. @Override
  2. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  3. super.onSizeChanged(w, h, oldw, oldh);
  4. mWidth = getWidth();
  5. mRectHeight = getHeight();
  6. mRectWidth = (int) (mWidth * 0.6 / mRectCount);
  7. mLinearGradient = new LinearGradient(
  8. 0,
  9. 0,
  10. mRectWidth,
  11. mRectHeight,
  12. Color.YELLOW,
  13. Color.BLUE,
  14. Shader.TileMode.CLAMP);
  15. mPaint.setShader(mLinearGradient);
  16. }

从这个例子中,我们可以知道,在创建自定义View的时候,需要一步一步来,从一个基本的效果开始,慢慢地增加功能,绘制更复杂的效果。不论是多么复杂的自定义View,它一定是慢慢迭代起来的功能,所以不要觉得自定义View有多难。千里之行始于足下,只要开始做,慢慢地就能越来越熟练。