#### **五、自定义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都是慢慢迭代起来的功能**