Android 中已经提供了很多的 View 给我们使用,但是有时候因为特殊需求的原因,这些 View 并不能满足需求,这个时候就需要自己来设计 View 。通常在自定义 View 的时候需要重写 onDraw() 方法来绘制需要显示的内容,如果这个 View 需要使用 wrap_content 属性,还需要重写 onMeasure() 方法,对于前言不明白的可以看看我的另一篇文章:
在 View 中有以下一些常用的方法:
- onFinishInflate() -- 从 XML 加载组件后的回调
- onSizeChanged() -- 组件大小发生变化时的回调
- onMeasure() -- 回调此方法对组件大小进行测量
- onLayout() -- 回调该方法来确定显示的位置
- onTouchEvent() -- 监听到触摸事件时的回调
并不需要重写以上所有的方法,根据自己的需求重写其中的部分方法即可,通常有三种方法来实现自定义控件。
- 扩展 -- 对现有控件进行扩展
- 组合 -- 将不同的控件组合在一起形成新的空间
- 重写 -- 通过重写来实现全新的控件
扩展
这是自定义 View 中重点方法之一,他可以在原生控件的基础上进行扩展,增加功能,修改 UI 显示效果等,下面以一个 TextView 为例子,看看如何对他进行扩展,比如如何让一个 TextView 的背景更加丰富,字体绚烂等 。
先看看普通的TextView 。
![](https://i-blog.csdnimg.cn/blog_migrate/1f7d0c7ad2451f5766c1de8bd500cf9c.webp?x-image-process=image/format,png)
要修改他的显示效果,应该重写它的 onDraw() 方法。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
程序调用 super.onDraw(canvas) 方法来实现原生控件的绘制,要想在这基础上进行实现自己的逻辑,可以在方法的前后添加代码,在方法前添加的,就是绘制在原生 TextView 的底层,在方法后添加的,就是绘制在原生 TextView 的上层。
我用一个实例来表现这个层叠关系,这是一个自定义的 TextView,我重写了 onDraw() 方法。
public class OneTextView extends TextView {
private Paint mPaint;
public OneTextView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
//这部分是在原生控件绘制之前进行绘制的,位于原生控件绘制区域底层,不会遮挡其他内容
//第一个矩形,大小跟此控件一样大,位于底层
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
//第二个矩形,稍微小一点,但是会遮挡第一个矩形
mPaint.setColor(Color.RED);
canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, mPaint);
//原生控件开始绘制,会遮挡上面代码绘制的内容。
super.onDraw(canvas);
//第三个矩形,这是在原生控件绘制完毕后进行绘制的,位于原生控制的上层,会遮挡所有之前绘制的内容
mPaint.setColor(Color.GREEN);
canvas.drawRect(20, 20,200, 100, mPaint);
}
}
看看最后的效果
![](https://i-blog.csdnimg.cn/blog_migrate/49b51182724f8f8d6030a416d4a840c3.webp?x-image-process=image/format,png)
可以看到最底层的蓝色矩形,第二层的红色矩形,第三层的文字,第四层的绿色矩形(从底层往上数),这下可以直观的理解在绘制过程中的层叠覆盖关系了吧,了解这个对于以后自定义控件的时候很有帮助。
下面我进行一个比较复杂的自定义 TextView 。字体能够实现渐变效果,这里我会用到 LinearGradient 以及 Matrix ,如果不了解可以查阅:
LinearGradient
Matrix
public class TwoTextView extends TextView {
int mViewWidth;
LinearGradient mLinearGradient;
Matrix mMatrix;
Paint mPaint;
int mTranslate=0;
public TwoTextView(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);
//在 onSizeChanged 方法中获取到宽度,并对各个类进行初始化
if (mViewWidth == 0) {
mViewWidth = getMeasuredWidth();
if (mViewWidth > 0) {
//得到 父类 TextView 中写字的那支笔。。。
mPaint = getPaint();
//初始化线性渲染器 不了解的请看上面连接
mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
new int[]{Color.BLUE, Color.YELLOW, Color.RED, Color.GREEN}, null, Shader.TileMode.CLAMP);
//把渲染器给笔套上
mPaint.setShader(mLinearGradient);
//初始化 Matrix
mMatrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
//先让父类方法执行,由于上面我们给父类的 Paint 套上了渲染器,所以这里出现的文字已经是彩色的了
super.onDraw(canvas);
if (mMatrix != null) {
//利用 Matrix 的平移动作实现霓虹灯的效果,这里是每次滚动1/10
mTranslate += mViewWidth / 10;
//如果滚出了控件边界,就要拉回来重置开头,这里重置到了屏幕左边的空间
if (mTranslate > mViewWidth) {
mTranslate = -mViewWidth/2;
}
//设置平移距离
mMatrix.setTranslate(mTranslate, 0);
//平移效果生效
mLinearGradient.setLocalMatrix(mMatrix);
//延迟 100 毫秒再次刷新 View 也就是再次执行本 onDraw 方法
postInvalidateDelayed(100);
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/2b1ff210adb8df2b6dc0ac54022f14dc.webp?x-image-process=image/format,png)
炫酷吧,重点其实在于如何利用好 LinearGradient 和 Matrix 还有 Paint , Canvas 也很重要,这些在自定义 View 中都是经常用到的。
组合
有的时候其实可以使用几个基本控件组合在一起,形成一个新的控件。这种方式通常都需要继承一个合适的 ViewGroup ,再给他添加指定功能的控件,形成新的空间。通过这种方式创建的控件我们还可以给他指定一些可配置的属性,增强它的可操控性,下面以一个标题栏为例子来说明如何创建组合控件。
一个标题栏通常贯穿了整个应用程序的大部分界面,大部分布局是左右两个 Button ,中间一个 TextView ,如果每个页面都去写一次,那未免太过繁琐了,我把它抽象出来,形成一个通用的标题栏,并且可以任意更改按钮文字和标题文字等属性,还要提供接口给调用者操作点击事件。
所以,我们需要对这个标题栏创建一些自定义属性,只需要在 res 资源目录的 values 目录下创建一个 attrs.xml 文件,并添加代码即可,看看下面的代码吧。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="titleText" format="string"/>
<attr name="titleTextSize" format="dimension"/>
<attr name="titleColor" format="color"/>
<attr name="leftText" format="string"/>
<attr name="leftTextColor" format="color"/>
<attr name="leftBackground" format="color|reference"/>
<attr name="rightText" format="string"/>
<attr name="rightTextColor" format="color"/>
<attr name="rightBackground" format="color|reference"/>
</declare-styleable>
</resources>
通过 declare-styleable 标签表示使用自定义属性,通过 name 属性来表示引用的名称,通过 format 来确定属性的类型,这里定义的东西就是我们平时写布局文件 XML 的时候调用的属性啦。这里分别设置了以下属性:
属性 | 解释 |
---|---|
titleText | 标题文本 |
titleColor | 标题文本颜色 |
titleTextSize | 标题文本字体大小 |
leftText | 左边按钮文本 |
leftTextColor | 左边按钮文本颜色 |
leftBackground | 左边按钮背景 |
rightText | 右边按钮文本 |
rightTextColor | 右边按钮文本颜色 |
rightBackground | 右边按钮背景 |
上面是对这个自定义标题栏的diy参数,那么在布局 XML 文件中应该如何使用?
<com.shire.testview.MyTopBar
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/mytopbar"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="#3F51B5"
custom:leftBackground="#ffffff"
custom:leftText="Back"
custom:leftTextColor="#000000"
custom:rightBackground="#ffffff"
custom:rightText="+"
custom:rightTextColor="#000000"
custom:titleColor="#ffffff"
custom:titleText="test"
custom:titleTextSize="8sp"
>
以上,注意第二行加入了一个命名空间 “http://schemas.android.com/apk/res-auto”,这个命名空间是使用自定义参数的时候使用的,这里我命名为了custom 。
完成 xml 文件的编写后,来看看这个自定义标题栏的类应该怎么写,这个自定义的标题栏需要继承一个 ViewGroup 来包裹多个普通 View , 这里继承的是 RelativeLayout。
public class MyTopBar extends RelativeLayout {
//定义了各个控件的属性变量
String titleText, leftText, rightText;
int titleColor, leftTextColor, rightTextColor;
float titleTextSize;
Drawable leftBackground, rightBackground;
Button rightButton, leftButton;
TextView titleTextView;
MyTopBarClickListener myTopBarClickListener;
public MyTopBar(Context context, AttributeSet attrs) {
super(context, attrs);
//通过 TypedArray 可以从 XML 文件中取出相应的属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
//使用R.styleable.xxxxx 就可以取出对应数据类型的具体数据,第二个参数是默认值 如果取值为空就会使用默认值
titleText = typedArray.getString(R.styleable.TopBar_titleText);
titleTextSize = typedArray.getDimension(R.styleable.TopBar_titleTextSize, 20);
titleColor = typedArray.getColor(R.styleable.TopBar_titleColor, 0);
leftText = typedArray.getString(R.styleable.TopBar_leftText);
leftTextColor = typedArray.getColor(R.styleable.TopBar_leftTextColor, 0);
leftBackground = typedArray.getDrawable(R.styleable.TopBar_leftBackground);
rightText = typedArray.getString(R.styleable.TopBar_rightText);
rightTextColor = typedArray.getColor(R.styleable.TopBar_rightTextColor, 0);
rightBackground = typedArray.getDrawable(R.styleable.TopBar_rightBackground);
typedArray.recycle();
}
通过上面部分的代码,我们取出了在 XML 文件中进行设置的参数,接下来应该是构造子控件,赋予参数,并添加到此 ViewGroup 中。
public void initView() {
//创建三个控件对象
rightButton = new Button(MainActivity.mContext);
leftButton = new Button(MainActivity.mContext);
titleTextView = new TextView(MainActivity.mContext);
//将之前得到的相应属性赋给相应的对象
rightButton.setText(rightText);
rightButton.setTextColor(rightTextColor);
rightButton.setBackground(rightBackground);
leftButton.setText(leftText);
leftButton.setTextColor(leftTextColor);
leftButton.setBackground(leftBackground);
titleTextView.setText(titleText);
titleTextView.setTextSize(titleTextSize);
titleTextView.setTextColor(titleColor);
//对各个子控件设置布局元素,并将它添加到此 ViewGroup 中。
LayoutParams leftParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
addView(leftButton, leftParams);
LayoutParams rightParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
addView(rightButton, rightParams);
LayoutParams titleParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
titleParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
addView(titleTextView, titleParams);
}
做完这一步,这个自定义标题栏其实就已经可以显示了,来看看吧。
![](https://i-blog.csdnimg.cn/blog_migrate/6f372e33ad7f5f8d541248805a33b9a0.webp?x-image-process=image/format,png)
虽然丑了一点,不过还算能用。。做实验就不讲究那么多了。到此,就实现了这个自定义的标题栏,可以自己设置9个属性以及其他系统自带的属性,那么仅此就够了吗?或许还应该给两边的按钮加上点击事件,但是不能直接写,直接写的话就成了固定的事件了,要开放一个接口给调用者,让调用者自己来编写逻辑代码,这样才能起到复用的作用。
/**
* 定义一个接口 让调用者自己实现具体逻辑
*/
public interface MyTopBarClickListener {
void leftClick();
void rightClick();
}
有了接口还要给外部调用者一个公开的方法来设置接口
/**
* 开放一个方法给外部来设置点击事件,参数用接口的形式得到调用者自己设置的逻辑
* @param myTopBarClickListener 监听器接口类
*/
public void setOnClickListener(MyTopBarClickListener myTopBarClickListener) {
this.myTopBarClickListener = myTopBarClickListener;
}
现在外部调用者可以使用 setOnClickListener 这个方法来编写点击事件的逻辑代码,我们得到这个接口的实现时候就可以用来填充到确切的点击事件中了。
private void setListener() {
rightButton.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
myTopBarClickListener.rightClick();
}
});
leftButton.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
myTopBarClickListener.leftClick();
}
});
}
最后,外部调用者只需要这样,就可以控制两个按钮的点击事件了。
MyTopBar myTopBar = (MyTopBar) findViewById(R.id.mytopbar);
myTopBar.setOnClickListener(new MyTopBar.MyTopBarClickListener() {
@Override public void leftClick() {
Toast.makeText(MainActivity.this, "左边被点击了", Toast.LENGTH_LONG).show();
}
@Override public void rightClick() {
Toast.makeText(MainActivity.this, "右边被点击了", Toast.LENGTH_LONG).show();
}
});
上面这段代码分别为两个按钮设置了点击事件,但是具体使用是使用外部调用者编写的逻辑。到这里,就实现由外部调用者自定义的点击事件的功能了。那么或许有时候会需要更多的功能,比如说有的时候我只想显示一个按钮,而不是两个按钮都显示。那么我们可以这样:
/**
* @param i 要控制的按钮,0为左边,1为右边
* @param x 要显示还是要隐藏
*/
public void setButtonVisable(int i, boolean x) {
if (x) {
if (i == 0) {
leftButton.setVisibility(View.VISIBLE);
}else {
rightButton.setVisibility(View.VISIBLE);
}
}else {
if (i == 0) {
leftButton.setVisibility(View.GONE);
}else {
rightButton.setTextColor(View.GONE);
}
}
}
在外部调用下面这行代码就可以实现隐藏左边按钮的功能了。
myTopBar.setButtonVisable(0,false);
![](https://i-blog.csdnimg.cn/blog_migrate/c4ff4f8fcdafb53e8328c48873260c7a.webp?x-image-process=image/format,png)
以上,就是组合类型的 View 实现方式,虽然很丑很简单,但是从中可以看出来,通过这种方式可以做出很多复杂功能的 View 。
重写
有时候,不管是继承原生控件或者是组合原生控件,都不能满足我们的特殊需求,这种时候就只能够自己重头完全的写一个全新的控件了。创建一个全新的 View 重点在于绘制和交互的部分,通常需要继承 View 类,并重写 onDraw() 、onMeasure() 等方法,还可以像刚才的组合控件一样,引入自定义属性来丰富控件的可控性。
接下来 我们想实现一个效果:中间有一个实心圆,外圈是一圈弧线,通过点击,可以增加弧线的长度直到360度。我们看看效果图。
![](https://i-blog.csdnimg.cn/blog_migrate/b3f0156ba9a4f090395599f647083c02.webp?x-image-process=image/format,png)
点击之后的样子
![](https://i-blog.csdnimg.cn/blog_migrate/05cb1d8f57a1fab896782d124858e0dc.webp?x-image-process=image/format,png)
其中自定义属性在上面的组合控件中已经试用过了,这里为了简化代码就不再使用,来看看绘制和交互的过程吧,我直接贴上所有的代码,代码中有详细的注释。
public class MyView extends View {
Paint circlePaint, arcPaint, textPaint;
RectF rectF;
//弧线度数变量
int i = 10;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
textPaint = new TextPaint(0);
textPaint.setColor(Color.RED);
textPaint.setTextSize(50);
//实心圆画笔
circlePaint = new Paint(0);
circlePaint.setColor(Color.BLUE);
//外圈弧线画笔
arcPaint = new Paint(0);
arcPaint.setColor(Color.GREEN); //设置画笔颜色
arcPaint.setStyle(Paint.Style.STROKE); //设置不填充中间
arcPaint.setStrokeWidth((float) 80.0); //画笔粗细
//用来定位弧线的矩形
rectF = new RectF();
rectF.top = 340;
rectF.bottom = 740;
rectF.left = 340;
rectF.right = 740;
}
@Override protected void onDraw(Canvas canvas) {
//这是圆心的坐标,我的屏幕的1080的,使用540的话就是横向居中了。
float xy = 540;
//圆圈的半径 这里是100
float radius = 100;
//画圆~
canvas.drawCircle(xy, xy, radius, circlePaint);
//画弧线
canvas.drawArc(rectF, 270, i, false, arcPaint);
//显示度数与提示
canvas.drawText(String.valueOf(i), 500, 560, textPaint);
canvas.drawText("点一点会转圈!", 400, 860, textPaint);
}
/**
* 公开一个方法,可以更新弧线的度数~
*/
public void ddd() {
//超过360度就还原到10度
if (i >= 360) {
i = 10;
postInvalidate();
} else {
this.i += 10;
postInvalidate();
}
}
}
这就是自定义控件的全部代码了,在外部只需要简单的调用,就可以控制弧线的度数,以下是外部调用的例子。
void initMyView()
{
final MyView myView = (MyView) findViewById(R.id.myview);
myView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
myView.ddd();
}
});
}
以上所有代码可以到开头的 github 地址下载。
总结
以上就是三种对 View 进行创新的方式。掌握好了这个才能做出美观的界面,掌握熟练之后还可以造轮子,做出漂亮的,可控性高的控件供重复使用。做 Android 大部分时间都是在做用户界面的交互,这其中 View 相当重要。
原文链接:http://www.jianshu.com/p/369f66035666#