🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
#### **五、自定义View** 在自定义View 时,我们通常会去重写onDraw()方法来绘制View的显示内容。如果该View还需要使用wrap_content属性,那么还必须重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。 自定义View时有一些比较重要的回调方法如下: ~~~ onFinishInflate();//从xml加载组件后回调 onSizeChanged();//组件大小改变时回调 onMeasure();//回调该方法进行测量 onLayout();//回调该方法来确定显示的位置 onTouchEvent();//监听到触摸事件回调 ~~~ >[info] 注意:当然,创建自定义View的时候,并不需要重写所有的方毡,只需要重写特定条件的回调方法即可。这也是Android控件架构灵活性的体现。 **自定义View通常有三种情况**: **1、对现有控件进行拓展**: 这是一个非常重要的自定义View方法,它可以在原生控件的基础上进行拓展,增加新的功能、修改显示的UI等。一般来说,会在**onDraw**()方法中对原生控件行为进行拓展 ~~~ @Override protected void onDraw(Canvas canvas) { //在回调父类方法前,实现自己的逻辑,对TextView来说即是在绘制文本内容前 super.onDraw(canvas); //在回调父类方法后,实现自己的逻辑,对TextView来说即是在绘制文本内容后 } ~~~ **程序调用super.onDraw(canvas)方法来实现原生控件的功能,但是在调用super.onDraw(canvas)方法之前和之后,我们都可以实现自己的逻辑,分别在系统绘制文字前后,完成自己的操作。** **示例1:自定义修改TextView** :-: ![](https://box.kancloud.cn/6dfe2f9adc65d2402f20bafc5efb6a17_360x605.jpg) 图7 自定义修改TextView 代码如下所示 **[MyTextView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/MyTextView.java)** ~~~ public class MyTextView extends TextView { private Paint mPaint1, mPaint2; public MyTextView(Context context) { super(context); initView(); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { 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); } @Override protected void onDraw(Canvas 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(); } } ~~~ 而代码中最重要的部分则是.在onDraw()方法中,为了改变原生的绘制行为,在系统调用super.onDraw(canvas)方法前,也就是在绘制文字之下,绘制两个不同大小的矩形,形成一个重叠效果,再让系统调用super.onDraw(canvas)方法,执行绘制文字的工作。这样,我们就通过改变控件绘制行为,创建了一个新的控件。 **示例2:闪动的文字效果** :-: ![](https://box.kancloud.cn/18561af176d6aae8eba8515a1996dc8d_360x233.gif) 图8:闪动的文字 要想实现这样一个效果,可以充分利用Android中Paint 对象的Shader渲染器。通过设置一个不断变化的LinearGradient(Shader的子类),并使用带有该属性的Paint对象来绘制耍显示的文字。 代码如下所示 **[ShineTextView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/ShineTextView.java)** ~~~ public class ShineTextView extends TextView { private LinearGradient mLinearGradient; private Matrix mGradientMatrix; private Paint mPaint; private int mViewWidth = 0; private int mTranslate = 0; public ShineTextView(Context context, AttributeSet attrs) { super(context, attrs); } @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();//TextView自身的方法,返回TextPaint,text文本的画笔 mLinearGradient = new LinearGradient( 0, 0, mViewWidth, 0, new int[]{ Color.BLUE, 0xffffffff, Color.BLUE}, null, Shader.TileMode.CLAMP); mPaint.setShader(mLinearGradient); mGradientMatrix = new Matrix(); } } } @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); } } } ~~~ 首先,在onSizeChanged()方法中进行一些对象的初始化工作,并根据View的宽带设置一个LinearGradient渐变渲染器,其中最关键的就是使用getPaint()方法获取当前绘制TextView的Paint对象,并给这个Paint对象设置原生TextView没有的LinearGradient属性。最后,在onDraw()方法中,通过矩阵的方式来不断平移渐变效果,从而在绘制文字时,产生动态的闪动效果。 **2、通过组合来实现新的控件:** 这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的拓展性。下面就以一个TopBar为示例,讲解如何创建复合控件。 效果如图所示 :-: ![](https://box.kancloud.cn/248fc47c8752d2146db69abe2e17e8d2_374x554.jpg) 图9 Topbar 代码如下所示 **[TopBar](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/TopBar.java)** ~~~ public class TopBar extends RelativeLayout { // 包含topbar上的元素:左按钮、右按钮、标题 private Button mLeftButton, mRightButton; private TextView mTitleView; // 布局属性,用来控制组件元素在ViewGroup中的位置 private LayoutParams mLeftParams, mTitlepParams, mRightParams; // 左按钮的属性值,即我们在atts.xml文件中定义的属性 private int mLeftTextColor; private Drawable mLeftBackground; private String mLeftText; // 右按钮的属性值,即我们在atts.xml文件中定义的属性 private int mRightTextColor; private Drawable mRightBackground; private String mRightText; // 标题的属性值,即我们在atts.xml文件中定义的属性 private float mTitleTextSize; private int mTitleTextColor; private String mTitle; // 映射传入的接口对象 private topbarClickListener mListener; public TopBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public TopBar(Context context) { super(context); } public TopBar(Context context, AttributeSet attrs) { super(context, attrs); // 设置topbar的背景 setBackgroundColor(0xFFF59563); // 通过这个方法,将你在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(); 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); // 按钮的点击事件,不需要具体的实现, // 只需调用接口的方法,回调的时候,会有具体的实现 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; } /** * 设置按钮的显示与否 通过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); } } } // 接口对象,实现回调机制,在回调方法中 // 通过映射的接口对象调用接口中的方法 // 而不用去考虑如何实现,具体的实现由调用者去创建 public interface topbarClickListener { // 左按钮点击事件 void leftClick(); // 右按钮点击事件 void rightClick(); } } ~~~ 通常情况下,需要添加标题栏的界面都会抽象出来一个这样的TopBar,同时给TopBar增加相应的接口,可以更加灵活地控制TopBar,所以需要创建这样一个UI模板,首先,摸板应该具有通用性与可定制性。也就是说,我们需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息,而不是所有的模板都一样,那样就失去模扳的意义。 **(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”。 在构造方位中,通过如下代码来获取在XML布局文件中自定义的那些属性,即与我们使用系统提供的那些属性一样。 ~~~ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar); ~~~ TypedArray数据结构相当于Map,是键值对的映射,通过TypedArray的getColor()、getString()等方法来获取这些定义的属性值。最后记得调用ta.recycle();方法来进行资源的回收,否则会对下次的使用造成影响。 **(2)、组合控件** UI模板TopBar实际上由三个控件组成,即左边的点击按钮mLeftButton,右边的点击按钮mRightButton和中间的标题栏mTitleView。通过动态添加控件的方式,使用addView()方法将这3个控件加入到定义的TopBar模板中,并给它们设置前面所获取到的具体的属性值,比如标题的文字颜色、大小等。如上面的构造方法中的代码所述。 那么如何来给这两个左、右按钮设计点击事件呢?既然是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; } ~~~ **③、实现接口回惆** 在调用者的代码巾,调用者需要实现这样一个接口, 并完成接口中的方法,确定具体的实现逻辑,井使用第二步中暴露的方法,将接口的对象传递进去,从而完成回调。通常悄况下,可以使用匿名内部类的形式来实现接口中的方法,代码如下所示。 **[TopBarTest](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/TopBarTest.java)** ~~~ public class TopBarTest extends Activity { private TopBar mTopbar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.topbar_test); // 获得我们创建的topbar mTopbar = (TopBar) findViewById(R.id.topBar); // 为topbar注册监听事件,传入定义的接口 // 并以匿名类的方式实现接口内的方法 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(); } }); // 控制topbar上组件的状态 mTopbar.setButtonVisable(0, true); mTopbar.setButtonVisable(1, false); } } ~~~ 除了通过接口回调的方式来实现动态的控制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 对象调用这个方法后,根据参数,调用者就可以动态地控制按钮的显示。 **(3)、引用UI模板** ~~~ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="5dp" tools:context=".MainActivity"> <!-- <include layout="@layout/topbar" /> --> <com.imooc.systemwidget.TopBar android:id="@+id/topBar" android:layout_width="match_parent" android:layout_height="40dp" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" 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" /> </RelativeLayout> ~~~ 当然也可以将这个UI模板写到一个布局文件中,如 **topbar.xml** ~~~ <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模板,如下所示 ~~~ <include layout="@layout/topbar" /> ~~~ **3、重写View来实现全新的控件:** 当Android系统原生的控件无法满足我们的需求时,就需要创建一个全新的自定义View了。通常需要继承View类,并重写它的onDraw()、onMeasure()等方法实现绘制逻辑,同时通过重写onTouchEvent()等触控事件来实现交互逻辑,还可以引入自定义属性,丰富自定义View的可定制性。 **(1)弧线展示图** :-: ![](https://box.kancloud.cn/45f3c853f60470b3b7775aec020def97_365x603.jpg) 图10 弧线展示图 很明显,这个自定义view分为3个部分,分别是中间的圆形,中间显示的文字和外圈的弧线,既然有了思路,只要在onDraw()方法中一个个去绘制就可以了。 >[info] 注意:这里简单处理,把view的绘制长度直接设置为屏幕的宽度 代码如下所示 **[CircleProgressView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/CircleProgressView.java)** ~~~ public class CircleProgressView extends View { private int mMeasureHeigth; private int mMeasureWidth; private Paint mCirclePaint; private float mCircleXY;//圆心坐标 private float mRadius;//半径 private Paint mArcPaint; private RectF mArcRectF;//椭圆形的边界 private float mSweepAngle;// private float mSweepValue = 66; private Paint mTextPaint; private String mShowText; private float mShowTextSize; public CircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CircleProgressView(Context context, AttributeSet attrs) { super(context, attrs); } public CircleProgressView(Context context) { super(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec); mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(mMeasureWidth, mMeasureHeigth); initView(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆 canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint); // 绘制弧线 canvas.drawArc(mArcRectF,270, mSweepAngle, false, mArcPaint); // 绘制文字 canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint); } private void initView() { float length = 0; if (mMeasureHeigth >= mMeasureWidth) { length = mMeasureWidth; } else { length = mMeasureHeigth; } //初始化圆形的参数 mCircleXY = length / 2; mRadius = (float) (length * 0.5 / 2); mCirclePaint = new Paint(); mCirclePaint.setAntiAlias(true);//抗锯齿 mCirclePaint.setColor(getResources().getColor(android.R.color.holo_blue_bright)); //用来定义弧线的形状和大小 mArcRectF = new RectF( (float) (length * 0.1), (float) (length * 0.1), (float) (length * 0.9), (float) (length * 0.9)); mSweepAngle = (mSweepValue / 100f) * 360f; mArcPaint = new Paint(); mArcPaint.setAntiAlias(true); mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright)); mArcPaint.setStrokeWidth((float) (length * 0.1)); mArcPaint.setStyle(Style.STROKE);//空心,不设置style。默认是实心 mShowText = setShowText(); mShowTextSize = setShowTextSize(); mTextPaint = new Paint(); mTextPaint.setTextSize(mShowTextSize); mTextPaint.setTextAlign(Paint.Align.CENTER); } private float setShowTextSize() { this.invalidate(); return 50; } private String setShowText() { this.invalidate(); return "Android Skill"; } public void forceInvalidate() { this.invalidate(); } public void setSweepValue(float sweepValue) { if (sweepValue != 0) { mSweepValue = sweepValue; } else { mSweepValue = 25; } this.invalidate(); } } ~~~ 首先,在初始化时,设置好绘制3个图形的参数,圆的代码如下 ~~~ 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); ~~~ >[info] 总结:相信这些图形如果单独让你去绘制,是非常容易的事情,只是这里进行一下组合,就创建了一个新的View。其实,不论是多么复杂的图形、控件,它都是由这些最基本的图形绘制出来的,关键就在于如何去分解、设计这些图形,当你的脑海中有了一幅设计图之后,剩下的事情就只是对坐标的计算。 **(2)音频条形图** 效果如下所示 :-: ![](https://box.kancloud.cn/96f453d572b64d6b426c2c5dbd42ccc2_361x597.gif) 图11 音频条形图 源码如下所示 [VolumeView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架构/SystemWidget/app/src/main/java/com/imooc/systemwidget/VolumeView.java) ~~~ /** * 音频条形图 */ public class VolumeView extends View { private int mWidth; private int mRectWidth; private int mRectHeight; private Paint mPaint; private int mRectCount; private int offset = 5; private double mRandom; private LinearGradient mLinearGradient;//线性着色器 渲染器 public VolumeView(Context context) { super(context); initView(); } public VolumeView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public VolumeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { mPaint = new Paint(); mPaint.setColor(Color.BLUE); mPaint.setStyle(Paint.Style.FILL);//实心 mRectCount = 12; } @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);//shader着色器,渲染器,实现一系列的渐变渲染效果 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < mRectCount; i++) { mRandom = Math.random(); float currentHeight = (float) (mRectHeight * mRandom); canvas.drawRect( (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight, (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint); } postInvalidateDelayed(300); } } ~~~ **分析:** 如果,我们取某一帧的静态图来绘制,其实就是绘制一个个的矩形,每一个矩形之间稍微偏移一点举例即可,代码如下所示 ~~~ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < mRectCount; i++) { mRandom = Math.random(); float currentHeight = (float) (mRectHeight * mRandom); canvas.drawRect( (float) (mWidth * 0.4 / 2 + mRectWidth * i + offset), currentHeight,//每一个小矩形的高 (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1)), mRectHeight, mPaint); } postInvalidateDelayed(300); } ~~~ 通过循环创建这些小矩形,通过横坐标的不断偏移,就可绘制出这些小矩形。下面让这些小矩形高度随机变化, ~~~ mRandom = Math.random(); float currentHeight = (float) (mRectHeight * mRandom); ~~~ 然后每隔一段时间, `postInvalidateDelayed(300);`进行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);//shader着色器,渲染器,实现一系列的渐变渲染效果 } ~~~ **总之,不管多么复杂的自定义View都是慢慢迭代起来的功能**