概述
Android SDK 为我们提供了一套完整的组件库,数量多、功能强,涉及到方方面面,但是,我们依然看到软件市场上的每个 App 都有自己独特的东西,绝不是千遍一律的,而且也会和 IOS相互借鉴,这就需要我们对组件进行定制,实现自己独树一帜的用户体验和界面风格。自定义组件到底难不难呢?如果前面五章的内容掌握好了,其实并不难。不管是普通的组件还是容器,开发时都有章可循的,找到其中的规律,根据实际的用户需求,一步步慢慢就能实现。学习要从简单的开始,不要想着一口吃成胖子,眼高手低,而是慢慢加大难度,循序渐进,方可成佛。另外,建议多阅读优秀源码,学习别人的思维模式和编程技巧,可能会有豁然开朗的功效。当然,最好的源码自然是 Google 提供的官方 Android API Demos 了,里面包含了开发的方方面面,这是一份最权威的 Demo 源码。
通常来说,自定义组件有三种定义方式:
Ø 从 0 开始定义自定义组件,组件类继承自 View;
Ø 从已有组件扩展,比如,从 ImageView 类扩展出功能更强或者更有个性化的组件;
Ø 将多个已有组件合成一个新的组件,比如,侧边带字母索引的 ListView。
本书将向大家介绍这三种组件的创建方式。技术永远说不完,最重要的是大家在学习过程中要触类旁通,举一反三,将技术学“活”。还是那句话,实践是通往真理的唯一通道。
自定义组件的基本结构
组件主要由两部分构成:组件类和属性定义。我们从第一种定义方式说起。
创建自定义组件类最基本的做法就是继承自类 View,其中,有三个构造方法和两个重写的
方法又是重中之重。下面是自定义组件类的基本结构:
public class FirstView extends View {
public FirstView(Context context) {
super(context);
}
public FirstView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
上述代码中,我们定义了一个名为 FirstView 的类,该类继承自 View,同时,为该类定义了三个构造方法并重写了另外两个方法:
Ø 构造方法
public FirstView(Context context)
public FirstView(Context context, AttributeSet attrs)
public FirstView(Context context, AttributeSet attrs, int defStyleAttr)
这三个构造方法的调用场景其实并不一样,第一个只有一个参数,在代码中创建组件
时会调用该构造方法,比如创建一个按钮:Button btnOK = new Button(this),this 是指
当前的 Activity,Activity 是 Context 的子类。第二个方法在 layout 布局文件中使用时调
用,参数 attrs 表示当前配置中的属性集合,例如在要 layout.xml 中定义一个按钮:
<Button android:layout_width = "match_parent" android:layout_height = "wrap_co-ntent"android:text = "OK"/>
Android 会调用第二个构造方法 Inflate 出 Button 对象。而第三
个构造方法是不会自动调用的,当我们在 Theme 中定义了 Style 属性时通常在第二个
构造方法中手动调用。
Ø 绘图
protected void onDraw(Canvas canvas)
该方法我们再熟悉不过了,前面 5 个章节一直重写了该方法,用于显示组件的外观。
最终的显示结果需要通过 canvas 绘制出来。在 View 类中,该方法并没有任何的默认
实现。
Ø 测量尺寸
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这是一个 protected 方法,意味着该方法主要用于子类的重写和扩展,如果不重写该方
法,父类 View 有自己的默认实现。在 Android 中,自定义组件的大小都由自身通过
onMeasure()进行测量,不管界面布局有多么复杂,每个组件都负责计算自己的大小。
重写 onMeasure 方法
View 类对于 onMeasure()方法有自己的默认实现。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(
getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在该方法中,调用了 protected final void setMeasuredDimension(int measured-Width, int
measuredHeight)方法应用测量后的高度和宽度,这是必须调用的,以后我们可以调用
getMeasuredWidth()和 getMeasuredHeight()方法获取这个宽度和高度值。大部分情况下,protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法都要重写,用于计算组件的宽度值和高度值。定义组件时,必须指定 android:layout_width 和android:layout_height 属性,属性值有三种情况:match_parent、wrap_content 和具体值。match_parent 表示组件的大小跟随父容器,所在的容器有多大,组件就有多大;wrap_content 表示组件的大小则内容决定,比如 TextView 组件的大小由文字的多少决定,ImageView 组件的大小由图片的大小决定;如果是一个具体值,相对就简单了,直接指定即可,单位为 dp。
总结来说,不管是宽度还是高度,都包含了两个信息:模式和大小。模式可能是match_parent、wrap_content 和具体值的任意一种,大小则要根据不同的模式进行计算。其实 match_parent 也是一个确定了的具体值,为什么这样说呢?因为 match_parent 的大小跟随父容器,而容器本身也是一个组件,他会算出自己的大小,所以我们根本不需要去重复计算了,父容器多大,组件就有多大,View 的绘制流程会自动将父容器计算好的大小通过参数传过来。
模式使用三个不同的常量来区别:
Ø MeasureSpec.EXACTLY
当组件的尺寸指定为 match_parent 或具体值时用该常量代表这种尺寸模式,很显然,处于该模式的组件尺寸已经是测量过的值,不需要进行计算。
Ø MeasureSpec.AT_MOST
当组件的尺寸指定为wrap_content时用该常量表示,因为尺寸大小和内容有关,所以,我们要根据组件内的内容来测量组件的宽度和高度。比如 TextView 中的 text 属性字符串越长,宽度和高度就可能越大。
Ø MeasureSpec.UNSPECIFIED
未指定尺寸,这种情况不多,一般情况下,父控件为 AdapterView 时,通过 measure 方
法传入。
最后,我们来考虑最关键的问题,如何获得当前组件的尺寸模式和尺寸大小?秘密隐藏在
protected void onMeasure(int widthMeasureSpec, int heightMeasure-Spec)方法的参数中,参数widthMeasureSpec 和 heightMeasureSpec 看起来只是两个整数,其实每个参数都包含了两个值:模式和尺寸。我们知道,int 类型占用 4 个字节,一共 32 位,参数 widthMeasureSpec 和heightMeasureSpec 的前两位代表模式,后 30 位则表示大小。
真相大白,接下来继续思考如何获取 widthMeasureSpec 和 heightMeasureSpec 参数的前 2 位与后 30 位,其实通过位运算即可得到,我们以 widthMeasureSpec 为例:
获取尺寸模式:widthMeasureSpec & 0x3 << 30
获取尺寸大小:widthMeasureSpec << 2 >> 2
上面的写法不一而足,显然,这样会给开发人员带来难度,所以,提供了一个名为MeasureSpec 的类用于计算模式和大小:
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
现在,我们来看看 onMeasure()的基本写法吧,因为要同时考虑宽度和高度,往往会定义两个方法分别计算,这样显然有更清晰的思路和逻辑。
public class FirstView extends View {
public FirstView(Context context) {
super(context);
}
public FirstView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FirstView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (mode == MeasureSpec.EXACTLY) {
//宽度为 match_parent 和具体值时,直接将 size 作为组件的宽度
width = size;
} else if (mode == MeasureSpec.AT_MOST) {
//宽度为 wrap_content,宽度需要计算
}
return width;
}
private int measureHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if (mode == MeasureSpec.EXACTLY) {
//宽度为 match_parent 和具体值时,直接将 size 作为组件的高度
height = size;
} else if (mode == MeasureSpec.AT_MOST) {
//高度为 wrap_content,高度需要计算
}
return height;
}
}
上面的代码依然什么事也干不了,表达的是一种基本思路。我们定义了一个组件类FirstView,从 View 类派生;定义了三个构造方法(虽然什么都没干),重写了 onDraw()方法用于绘制组件外的外观(这里啥都没干);重写的 onMeasure()方法用于计算组件的高度和宽度(嗯,measure 的意思是测量,我们直接理解成计算好了),在该方法中,定义了两个方法,其中 measureWidth()方法用于计算组件的宽度,如果组件的 layout_width 属性为 match_parent 或指定了具体值,则直接从参数 widthMeasureSpec 获取,如果为 wrap_content,则要通过计算才能得到(因为没有设定具体的功能,所以我们也不知道该干什么)。另一个方法 measureHeight()则用于计算组件的高度,代码实现和 measureWidth()类似,不再赘述。
那么,为了充分说明 onMeasure()方法的作用,我们将 FirstView 模拟 TextView 的功能,也就是在组件中绘制文字,为了简单起见,我们只考虑一行文字(多行文字会让代码变得十分复杂)。
在本案例中,比较麻烦的是绘制文字时,public void drawText(String text, float x, float y, Paint paint)方法中参数 y 的确定,这要从字体的基本结构说起。
如图 所示,从技术层面上来说,字符由下面几个部分构成,从文字上理解可能比较晦涩,
通过所示的示意图也许很容易找到答案。简单来说,常用字符的高度是 ascent 和 descent 的和,但是,一些特殊字符比如拼音的音调等则会延伸到 top 的位置。
Ø baseline:基准点;
Ø ascent:baseline 之上至字符最高处的距离;
Ø descent:baseline 之下至字符最低处的距离;
Ø top:字符可达最高处到 baseline 的值,即 ascent 的最大值;
Ø bottom:字符可达最低处到 baseline 的值,即 descent 的最大值。
在 Android 中,字体的信息使用 Paint.FontMetrics 类来表示,该类源码如下:
public static class FontMetrics {
public float top;
public float ascent;
public float descent;
public float bottom;
public float leading;
}
FontMetrics 类作为 Paint 的内部类,定义了 5 个属性,除了 leading 在上面没有说明外,其他都有图示与说明。leading 是指上一行字符的 descent 到下一行的 ascent 之间的距离,因为案例中只显示单行字符,所以我们并不打算关注。
要获取 FontMetrics 对象,调用 Paint 类的 getFontMetrics()即可,而在 drawText()方法中,参数 y 就是 baseline 的值,因为 FontMetrics 类并没有声明 baseline 属性,所以,我们需要通过下面的公式计算出来:int baseline = height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
其中,height 是文字所在区域的高度。
下面是 FirstView 类的完整实现,我们定义了一个方法 private Rect getTextRect()用于获取文字所占的区域大小,measureWidth()和 measureHeight()方法也作了修改。
public class FirstView extends View {
private static final String TEXT = "FirstView 绘制文字";
private Paint paint;
public FirstView(Context context) {
super(context);
}
public FirstView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(100);
paint.setColor(Color.RED);
}
public FirstView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将文字放在正中间
Rect textRect = this.getTextRect();
int viewWidth = getMeasuredWidth();
int viewHeight = getMeasuredHeight();
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
int x = (viewWidth - textRect.width()) / 2;
int y = (int) (viewHeight / 2 +
(fontMetrics.descent- fontMetrics.ascent) / 2
- fontMetrics.descent);
canvas.drawText(TEXT, x, y, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Rect rect = getTextRect();
int textWidth = rect.width();
int textHeight = rect.height();
int width = measureWidth(widthMeasureSpec, textWidth);
int height = measureHeight(heightMeasureSpec, textHeight);
setMeasuredDimension(width, height);
}
/**
* 获取文字所占的尺寸
* @return
*/
private Rect getTextRect(){
//根据 Paint 设置的绘制参数计算文字所占的宽度
Rect rect = new Rect();
//文字所占的区域大小保存在 rect 中
paint.getTextBounds(TEXT, 0, TEXT.length(), rect);
return rect;
}
/**
* 测量组件宽度
* @param widthMeasureSpec
* @param textWidth 文字所占宽度
* @return
*/
private int measureWidth(int widthMeasureSpec, int textWidth){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if(mode == MeasureSpec.EXACTLY){
//宽度为 match_parent 和具体值时,直接将 size 作为组件的宽度
width = size;
}else if(mode == MeasureSpec.AT_MOST){
//宽度为 wrap_content,宽度需要计算,此处为文字宽度
width = textWidth;
}
return width;
}
/**
* 测量组件高度
* @param heightMeasureSpec
* @param textHeight 文字所占高度
* @return
*/
private int measureHeight(int heightMeasureSpec, int textHeight){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY){
//宽度为 match_parent 和具体值时,直接将 size 作为组件的高度
height = size;
}else if(mode == MeasureSpec.AT_MOST){
//高度为 wrap_content,高度需要计算,此处为文字高度
height = textHeight;
}
return height;
}
}
上述代码中,测试组件宽度时,定义了 private int measureWidth(int widthMeasureSpec, int textWidth)方法,如果尺寸模式为 MeasureSpec.EXACTLY,表示宽度可能为 match_parent 或精确值,直接将获取的尺寸大小返回。如果尺寸模式为 MeasureSpec.AT_MOST,表示宽度为wrap_content,则需要计算组件的宽度,因为组件内容为文字,所以文字占用的宽度是多少组件的宽度也是多少,此时,组件的宽度就是 textWidth。测量高度也是同样的道理。
重写 onDraw()方法绘制组件外观时,需要将文字在指定的位置上绘制出来,x 方向比较简单,其值为组件宽度减去文字所占宽度除以 2;而 y 的大小则是字体的 baseline 值,其大小为viewHeight / 2 + (fontMetrics.descent- fontMetrics.ascent) / 2 - fontMetrics.descent,viewHeight 是组件测量后的高度。
最后,我们比较一下 layout_width 和 layout_height 两个属性的值在不同情况下的运行结果。
组件属性
在 FirstView 组件类中,要显示的文字定义成了常量——private static final String TEXT = “FirstView 绘制文字”,显然,这并不可取,我们应该可以随意定义文字,这需要用到组件的属性。
从 View 继承后,View 已经具备了若干默认属性,比如 layout_width、layout_height,所以,在 FirstView 类中,指定该类的宽度和高度时,我们并没有特别定义和编程。大家找到
sdk/platforms/android-21/data/res/values/attrs.xml 文 件 , 打 开 后 , 定 位 到
<declare-styleablename="View">
这一行,接下来的 500 多行都是与 View 的默认属性有关的,常用的属性比如layout_width、layout_height、background、alpha 等属性都是默认的属性。您可以打开上述文件进行更详细的了解。下面我们将向您介绍自定义属性的定义。
属性的基本定义
除了 View 类中定义的默认属性外,我们也能自定义属性。自定义属性主要有以下几个步骤:
Ø 在 res/values/attrs.xml 文件中为指定组件定义 declare-styleable 标记,并将所有的属性
都定义在该标记中;
Ø 在 layout 文件中使用自定义属性;
Ø 在组件类的构造方法中读取属性值。
在 res/values 目录下,创建 attrs.xml 文件,内容大概如下:
<declare-styleable name="FirstView">
<attr name="attr" format="string"/>
</declare-styleable>
组件的属性都应该定义在 declare-styleable 标记中,该标记的 name 属性值一般来说都是组件类的名称(此处为 FirstView),虽然也可以取别的名称,但和组件名相同可以提高代码的可读性。组件的属性都定义在 declare-styleable 标记内,成为 declare-styleable 标记的子标记,每个属性由两部分组成——属性名和属性类型。属性通过 attr 来标识,属性名为 name,属性类型为format,可选的属性类型如图 所示。
Ø string:字符串
Ø boolean:布尔
Ø color:颜色
Ø dimension:尺寸,可以带单位,比如长度通常为 dp,字体大小通常为 sp
Ø enum:枚举,需要在 attr 标记中使用标记定义枚举值,例如 sex 作为性别,有
两个枚举值:MALE 和 FEMALE。
<attr name="sex" format="enum">
<enum name="MALE" value="0"/>
<enum name="FEMALE" value="1"/>
</attr>
Ø flag:标识位,常见的 gravity 属性就是属性该类型,如图 所示。
flag 类型的属性也有一个子标记,语法形如:
<attr name="x" format="flag">
<flag name="f1" value="0"/>
<flag name="f2" value="1"/>
</attr>
Ø float:浮点数
Ø fraction:百分数,在动画资源<scale>、<rotate>
等标记中,fromX、fromY 等属性就是
fraction 类型的属性
Ø integer:整数
Ø reference : 引 用 , 引 用 另 一 个 资 源 , 比 如 android:paddingRight=-
“@dimen/activity_horizontal_margin”就是引用了一个尺寸资源。
在 FirstView 组件中,text 应该作为属性来定义,并且为 string 类型,我们在 attrs.xml 中定义如下的 xml 内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FirstView">
<attr name="text" format="string" />
</declare-styleable>
</resources>
上述的属性配置好之后,会在工程的 R.java 文件中自动生成形如下面的索引,读取属性时将会使用这些索引名称来进行访问。
public static final int[] FirstView = {
0x7f01002d
};
public static final int FirstView_text = 0;
定义好属性的名称和类型后,属性就可以使用了,在布局文件 layout.xml 中,首先要定义好属性的命名空间(namespace),默认情况下,xml 文件中的根元素按如下定义:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
</RelativeLayout>
默 认 的 命 名 空 间 为 “android” , 是 由 语 句 xmlns:android=
“http://schemas.android.com/apk/res/android 决定的,对于自定义属性来说,必须定义其他的命名空间,且必须按下面的要求定义:xmlns:trkj=”http://schemas.android.com/apk/res-auto”其中 , trkj 是自定义的命名空间 , 也可以使用其他代替 , 后面的http://schemas.android.com/apk/res-auto 则是固定的,有了这个命名空间后,访问前面的 text 属性则应该这样赋值:trkj:text=”Android 自定义组件开发详解”。事实上,IDE 也有相应的提示(Android Studio 的智能提示功能比 eclipse ADT 要强大得多,在 attrs.xml 文件中后者没有提示),如图所示。
完整的 xml 配置如下(请注意下划线部分):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:trkj="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:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<bczm.graphics.view.FirstView
android:layout_width="match_parent"
android:layout_height="wrap_content"
trkj:text="Android 自定义组件开发详解"
android:background="@android:color/holo_blue_bright"/>
</RelativeLayout>
接下来我们需要在 FirstView 类中读取 trkj:text 属性,组件运行后,所有属性都将保存在
AttributeSet 集合中并通过构造方法传入,我们通过 TypedArray 可以读取出指定的属性值。
public FirstView(Context context, AttributeSet attrs) {
super(context, attrs);
……
//读取属性值
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FirstView);
text = a.getString(R.styleable.FirstView_text);
a.recycle();
}
语 句 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FirstView) 中 参 数
R.styleable.FirstView 是<declare-styleable name="FirstView">
配置中的 name 值,TypedArray 对象的getString()方法用于读取特定属性的值(R.styleable.FirstView_text 是指 text 属性),TypedArray 类中定义了很多 getXXX()方法,“XXX”代表对应属性的类型,有些 get 方法有两个参数,第二个参数通常是指默认值。最后,需要调用 TypedArray 的 recycle()方法释放资源。
至此,FirstView 已经修改完成了,因为改动并不大,限于篇幅,这里我们把改动的代码列出来,并用下划线标识。
改动 1:
public class FirstView extends View {
private static final String TEXT = "FirstView 绘制文字";
private String text;
改动 2:
public FirstView(Context context, AttributeSet attrs) {
……
//读取属性值
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FirstView);
text = a.getString(R.styleable.FirstView_text);
a.recycle();
}
改动 3:
private Rect getTextRect(){
//根据 Paint 设置的绘制参数计算文字所点的宽度
Rect rect = new Rect();
//文字所占的区域大小保存在 rect 中
paint.getTextBounds(this.text, 0, this.text.length(), rect);
return rect;
}
改动 4:
protected void onDraw(Canvas canvas) {
……
canvas.drawText(this.text, x, y, paint);
}
运行效果图如图所示。
读取来自 style 和 theme 中的属性
组件的属性可以在下面 4 个地方定义:
Ø 组件
Ø 组件的 style 属性
Ø theme
Ø theme 的 style 属性
这个问题说起来可能有点儿绕,所以我们索性通过一个案例来进行学习和讲解。假如我们有一个组件类 AttrView,从 View 类派生,AttrView 类有 4 个属性:attr1、attr2、attr3、attr4。另外,定义了一个属性 myStyle,该属性定义在 declare-styleable 标记之外,类型为 reference,用于theme 的 style 属性。这些属性在 res/values/attrs.xml 文件中定义如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AttrView">
<attr name="attr1" format="string"></attr>
<attr name="attr2" format="string"></attr>
<attr name="attr3" format="string"></attr>
<attr name="attr4" format="string"></attr>
</declare-styleable>
<attr name="myStyle" format="reference"></attr>
</resources>
我们将这 4 个属性应用在不同的场合,分别为组件、组件的 style 属性、theme 和 theme 的style 属性。
attr_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:trkj="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.trkj.lizanhong.chapter6.AttrView
android:layout_width="match_parent"
android:layout_height="match_parent"
trkj:attr1="attr1"
style="@style/viewStyle"/>
</LinearLayout>
trkj:attr1=”attr1” 应 用 了 属 性 attr1 , style=”@style/viewStyle” 应 用 了 属 性 attr2 , 其 中@style/viewStyle 定义在 res/values/style.xml 文件中,当然,该文件还定义了整个 App 工程的主题(theme),配置如下:
style.xml:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="attr3">attr3</item>
<item name="myStyle">@style/ myDefaultStyle</item>
</style>
<style name=" myDefaultStyle">
<item name="attr4">attr4</item>
</style>
<style name="viewStyle">
<item name="attr2">attr2</item>
</style>
</resources>
在工程的主题(theme) AppTheme 中,应用了属性 attr3,同时应用了 style 属性 myStyle,该 style 属性又引用了@style/ myDefaultStyle,@style/ myDefaultStyle 中应用了属性 attr4。总结起来,attr1 是组件的直接属性,attr2 是组件的 style 属性引用的属性,attr3 是工程主题(theme)属性,attr4 是工程主题(theme)的 style 属性。现在,我们在 AttrView 构造方法中读取这 4 个属性值。
public class AttrView extends View {
private static final String TAG = "AttrView";
public AttrView(Context context) {
super(context);
}
public AttrView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.myStyle);
}
public AttrView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AttrView,
defStyleAttr, R.style. myDefaultStyle);
String attr1 = a.getString(R.styleable.AttrView_attr1);
String attr2 = a.getString(R.styleable.AttrView_attr2);
String attr3 = a.getString(R.styleable.AttrView_attr3);
String attr4 = a.getString(R.styleable.AttrView_attr4);
Log.i(TAG, attr1 + "");
Log.i(TAG, attr2 + "");
Log.i(TAG, attr3 + "");
Log.i(TAG, attr4 + "");
}
}
我们在 AttrView(Context context, AttributeSet attrs)构造方法中,调用了 AttrView(Context
context, AttributeSet attrs, int defStyleAttr)构造方法,与上一个案例相比,我们调用了另一个重载的 obtainStyledAttributes()方法,该方法的原型为:
public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, int
defStyleRes),我们来了解一下该方法参数作用:
set:属性值的集合。
attrs:我们要获取的属性的资源 ID 的一个数组,我们定义了 attr1、attr2、attr3 和 attr4,这
4 个属性自动生成的索引会存储到 R.styleable.AttrView 数组中,该数组就是 attrs 参数。
public static final int[] AttrView = {
0x7f010020, 0x7f010021, 0x7f010022, 0x7f010023
};
defStyleAttr:当前 Theme 中 style 属性,如果组件和组件的 style 属性都没有为 View 指定属性时,将从 Theme 的 Style 中查找相应的属性值。
defStyleRes:指向一个 Style 的资源 ID,但是仅在 defStyleAttr 为 0 或 defStyleAttr 不为 0 但Theme 中没有为 defStyleAttr 属性赋值时起作用。
我们通过如图所示的流程图来了解 View 是如何读取属性的。图中我们试图读取 attr 属
性,从流程图中也可以看出各个环节的优先级顺序。
如图是最后的运行结果,在控制台输出了每一个属性的值。大家也可以思考一下如果
同一个属性在不同的地方都出现,根据优先级关系判断最后输出的属性值是多少。
案例 1 : 圆形 ImageView 组件
ImageView 是我们常用的组件之一,但该组件存在一定的局限性,比如只能显示矩形的图片,现在很多 App 在显示头像时都支持圆形或其他形状,所以,我们将向大家介绍如何定制支持圆形图片的 ImageView 组件。
因为是显示图片,我们自然想到组件类应该继承自 ImageView,ImageView 已经帮我们做了大部分工作,比如已经重写了 onMeasure()方法,不再需要重新计算尺寸,设置图片也已经实现了。我们还要添加一些功能,比如显示出来的图片是圆的,支持添加圆形框线,为圆形框线指定颜色和大小等等。另外,还要删除 ImageView 与本需求冲突的功能,ImageView 支持 scaleType,用于定指图片的缩放类型,但我们打算把这个功能删除(别问为什么,任性!^_^)。要提醒的是,其实我们最终显示的图片是一个椭圆,如果要显示成圆形,请将组件的宽度和高度设成一致。
首先,我们事先定义两个属性:圆形框线的粗细与颜色,定义粗细时使用 dimension 类型,而颜色则使用 color 类型。
attrs.xml:
<declare-styleable name="CircleImageView">
<attr name="circle_border" format="dimension"></attr>
<attr name="circle_border_color" format="color"></attr>
</declare-styleable>
其次,定义 CircleImageView 组件类,该类继承自 ImageView 类。
public class CircleImageView extends ImageView {
private static final String TAG = "CircleImageView";
private Paint paint;
private Xfermode xfermode ;
private Path path = new Path();
private int border;
private int borderColor;
public CircleImageView(Context context) {
super(context);
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLACK);
xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
path = new Path();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CircleImageView);
border = a.getDimensionPixelSize(
R.styleable.CircleImageView_circle_border, 0);
borderColor = a.getColor(R.styleable.CircleImageView_circle_border_color,
Color.GRAY);
a.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
Drawable mDrawable = getDrawable();
if (mDrawable == null) {
super.onDraw(canvas);
}
int width = getMeasuredWidth();
int height = getMeasuredHeight();
RectF ovalRect = new RectF(0, 0, width, height);
int layerId = canvas.saveLayer(getPaddingLeft(), getPaddingTop(), width,
height, null, Canvas.ALL_SAVE_FLAG);
Bitmap bitmap = ((BitmapDrawable) mDrawable).getBitmap();
canvas.drawBitmap(bitmap, new Rect(0, 0, mDrawable.getIntrinsicWidth(),
mDrawable.getIntrinsicHeight()), ovalRect, null);
paint.setXfermode(xfermode);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLACK);
path.reset();
path.addOval(ovalRect, Path.Direction.CCW);
canvas.drawPath(path, paint);
paint.setXfermode(null);
canvas.restoreToCount(layerId);
//画空心圆
if(border != 0) {
paint.setStyle(Paint.Style.STROKE);
paint.setColor(borderColor);
paint.setStrokeWidth(border);
ovalRect.inset(border / 2, border / 2);
canvas.drawOval(ovalRect, paint);
}
}
}
上述代码中,主要重写了 onDraw()方法,ImageView 作为父类,可以通过 src 属性或
setImageResource()、setImageBitmap()等方法设置图片,getDrawable()方法用于获取设置的图片,得到图片后,需要在图片上画一个实心椭圆作为遮罩层,该椭圆是组件的内切椭圆,通过语句RectF ovalRect = new RectF(0, 0, width, height)指定。画椭圆图片时,先创建一个 Layer,调用canvas.drawBitmap(bitmap, new Rect(0, 0, mDrawable.getIntrinsicWidth(),mDrawable.getIntrinsicHeight()), ovalRect, null)语句将图片绘制到 canvas 画布上并进行缩放,然后为 Paint 指定 PorterDuff.Mode.DST_IN 位图模式,在 Path 对象中添加一个椭圆,并与图片进行DST_IN 位图运算(只有 Path 对象才能进行位图运算,不能直接调用 drawOval()方法),于是就得到圆形图片了。
为图片绘制边框线就是一件相对简单的工作了,但也有几个地方需要交待。调用 border =
a.getDimensionPixelSize(R.styleable.CircleImageView_circle_border, 0)语句获取边框线的大小后,得到的数据单位始终为像素(px),这样不管使用 dp 还是 sp 都可以得到一致的数值。画边框线时,仅仅只有 border 还是不够的,因为 border 本身占用了一定的宽度,必须调用 ovalRect.inset(border/ 2, border / 2)语句将圆形边框缩小(注意要除以 2)。定义布局文件 circle_imageview.xml,其中 border 为 10dp,颜色为红色。最终的运行效果如图 所示。
案例 2: 验证码组件
验证码在 Web 开发中非常常见,用于防止非法暴力破解,随着图形识别技术的发展,验证码也越来越复杂化和多样化,以适应当前破解技术的不断提高。本小节将定义一个验证码组件,并为用户提供定制功能,在运行过程中与组件交互。
我们将验证码组件命名为 CodeView,默认情况下,随机生成 4 个数字和 50 条干扰线,如果用户测试次数过多,可以动态加大验证码的难度,比如增加验证码的个数、增加干扰线条数、改变验证码颜色等等。提供的主要功能有:
Ø 刷新验证码
Ø 改变验证码个数
Ø 改变干扰线条数
Ø 改变验证码字体大小
Ø 改变验证码字体颜色
Ø 获取当前验证码
先来看看效果图,如图所示:
本组件的属性主要包括验证码个数、干扰线条数、字体大小和字体颜色,在 attrs.xml 文件中定义如下属性,其中 font_size 表示字体大小,类型为 dimension,到时将使用 sp 作为字体单位。
组件类 CodeView 从 View 中派生,这是一个从 0 开始开发的自定义组件,其实从TextView继承也是一个不错的主意。在 CodeView 类中,定义了如下的成员变量和常量,常量主要是用于定义各属性的缺省值。
private static final String TAG = "CodeView";
private int count;//验证码的数字个数
private int lineCount; //干扰线的条数
private int fontSize; //字体大小
private int color;//字体颜色
private String code;//验证码
private Random rnd;
private Paint paint;
private static final int DEFAULT_COUNT = 4;
private static final int DEFAULT_LINE_COUNT = 50;
private static final int DEFAULT_FONT_SIZE = 12;//sp
private static final int DEFAULT_COLOR = Color.BLACK;
在构造方法 public CodeView(Context context, AttributeSet attrs, int defStyleAttr)中读取出各属性的值,重点强调一下字体大小的读取方法。字体大小涉及单位的问题,一般使用 sp 作为字体单位,而我们使用 TypedValue 类的 getDimensionPixelSize()方法读取的值是像素,所以需要进行单位转换,该工作交给 TypedValue 类的静态方法 applyDimension()完成,applyDimension()的作用是 进 行 单 位 换 算 , 其 方 法 原 型 为 : public static float applyDimension(int unit, float value,DisplayMetrics metrics),其中 unit 是目标单位,可选值如图所示,value 是要换算的值,metrics 通过 getResources().getDisplayMetrics()即可得到。
public CodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.CodeView);
count = typedArray.getInt(R.styleable.CodeView_count,DEFAULT_COUNT);
lineCount = typedArray.getInt(R.styleable.CodeView_line_count,DEFAULT_LINE_COUNT);
fontSize = typedArray.getDimensionPixelSize(R.styleable.CodeView_font_size,DEFAULT_FONT_SIZE);
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,DEFAULT_FONT_SIZE,getResources().getDisplayMetrics());
color = typedArray.getColor(R.styleable.CodeView_code_color,DEFAULT_COLOR);
typedArray.recycle();
rnd = new Random();
paint = new Paint();
initPaint();
code = getCode();
}
上述代码中,构造方法一旦调用,即生成验证码,验证码由 0~9 之间的数字构造,长度取决于属性 count 的值,默认长度为 4。
private String getCode(){
String str = "";
for(int i = 0; i < count; i ++){
str += rnd.nextInt(10);
}
return str;
}
测量组件尺寸时,考虑了从 View 类继承下来的 padding 属性,该属性我们用于定义验证码与边框线的距离,增强视觉观感,getPaddingLeft()、getPaddingTop()、getPaddingRight()和getPaddingBottom()四个方法分别用于获取离左、上、右、下四个方向的距离,计算组件的宽度和高度时,需要加上各方向的 padding 值。
/**
* 计算组件宽度
* @param widthMeasureSpec
* @param textRect
* @return
*/
private int measureWidth(int widthMeasureSpec, Rect textRect){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (mode == MeasureSpec.EXACTLY){
width = size;
}else if(mode == MeasureSpec.AT_MOST){
width = getPaddingLeft() + textRect.width() + getPaddingRight();
}
return width;
}
/**
* 计算组件的高度
* @param heightMeasureSpec
* @param textRect
* @return
*/
private int measureHeight(int heightMeasureSpec, Rect textRect){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY){
height = size;
}else if(mode == MeasureSpec.AT_MOST){
height = getPaddingTop() + textRect.height() + getPaddingBottom();
}
return height;
}
private Rect getTextRect(){
//根据 Paint 设置的绘制参数计算文字所点的宽度
Rect rect = new Rect();
//文字所占的区域大小保存在 rect 中
paint.getTextBounds(this.text, 0, this.text.length(), rect);
return rect;
}
绘制验证码时,首先要初始化 Paint 对象,包括设置画笔的初始颜色、初始字体大小等。
private void initPaint(){
paint.reset();
paint.setAntiAlias(true);
paint.setColor(color);
paint.setTextSize(fontSize);
}
绘图分为三个部分:外边框、干扰线和验证码。外边框的颜色和验证码颜色相同,为了显示得更加完整,在组件矩形区域大小的基础上向内收缩 2 个距离,边框线是空心矩形,所以将 Style定义为 Style.STROKE,完成后又还原成 Style.FILL,因为后面绘制文字时必须是实心样式。干扰线是若干条随机生成的直线,直线的两个点都是随机的,x 坐标在 0~width(组件宽度)之间,y 坐标在 0~height(组件高度)之间。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "code:" + code);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
Rect rect = new Rect(0, 0, width, height);
//绘制外围矩形框
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1);
Rect rect1 = new Rect(rect);
rect1.inset(2, 2);//缩小一点
canvas.drawRect(rect1, paint);
paint.setStyle(Paint.Style.FILL);
//绘制随机干扰线
paint.setColor(Color.GRAY);
for(int i = 0; i < lineCount; i ++){
int x1 = rnd.nextInt(width);
int y1 = rnd.nextInt(height);
int x2 = rnd.nextInt(width);
int y2 = rnd.nextInt(height);
canvas.drawLine(x1, y1, x2, y2, paint);
}
paint.setColor(color);
//绘制文字
Rect textRect = getTextRect();
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
int x = (width - textRect.width()) / 2;
int y = (int) (height / 2 +
(fontMetrics.descent- fontMetrics.ascent) / 2
- fontMetrics.descent);
canvas.drawText(code, x, y, paint);
}
最后,是相关交互功能的实现,当通过外部改变绘制结果时,有两种情况:一种是只需要刷新即可,如改变颜色、增减干扰线、刷新等功能,这种情况需要调用 invalidate()方法进行重绘;另一种是组件尺寸的变化,需要重新测量组件的尺寸,如随机数个数变化、字体大小改变等功能,这种情况需要调用 requestLayout()方法,该方法依次调用 onMeasure()和 onDraw()两个方法,先重新测量组件的尺寸,再重绘组件。
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
code = getCode();
requestLayout();//重新调整布局大小
}
public int getLineCount() {
return lineCount;
}
public void setLineCount(int lineCount) {
this.lineCount = lineCount;
invalidate();//重绘
}
public int getFontSize() {
return fontSize;
}
public void setFontSize(int fontSize) {
this.fontSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, fontSize,
getResources().getDisplayMetrics());
initPaint();
requestLayout();//重新调整布局大小
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
initPaint();
invalidate();//重绘
}
/**
* 刷新验证码
*/
public void refresh(){
code = getCode();
invalidate();
}
public String theCode(){
return code;
}
布局文件 code_layout.xml 定义了测试案例的 UI 界面,这是一个 LinearLayout 布局,垂直方向放置组件,第一行为验证码组件,第二行和第三行分别放置了三个按钮。每个按钮的单击事件都定义在 CodeActivity 类中。
code_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:trkj="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CodeView
android:id="@+id/code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="10dp"
trkj:code_color="#FF0000"
trkj:font_size="50sp"
trkj:line_count="100" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="refresh"
android:text="刷新" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_blue_bright"
android:onClick="changeColor"
android:text="改变颜色" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="changeFont"
android:text="改变字体" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="textCount"
android:text="随机数个数" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_blue_bright"
android:onClick="lineCount"
android:text="干扰线条数" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="get"
android:text="获取验证码" />
</LinearLayout>
</LinearLayout>
public class CodeActivity extends Activity {
private CodeView codeView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_code);
codeView = (CodeView) findViewById(R.id.code);
}
/**
* 刷新
* @param view
Android 自定义组件开发详解 株洲新程 IT 教育 李赞红
邮箱:lifenote@21cn.com 欢迎捐助,支持我分享更多技术! - 201 -
*/
public void refresh(View view){
codeView.refresh();
}
/**
* 改变颜色
* @param view
*/
public void changeColor(View view){
codeView.setColor(Color.BLUE);
}
/**
* 改变字体大小
* @param view
*/
public void changeFont(View view){
codeView.setFontSize(100);
}
/**
* 改变随机数个数
* @param view
*/
public void textCount(View view){
codeView.setCount(5);
}
/**
* 改变干扰线条数
* @param view
*/
public void lineCount(View view){
codeView.setLineCount(150);
}
/**
* 获取验证码
* @param view
*/
public void get(View view){
String code = codeView.theCode();
Toast.makeText(this, code, Toast.LENGTH_LONG).show();
}
}
public class CodeView extends View {
private static final String TAG = "CodeView";
private int count;//验证码的数字个数
private int lineCount; //干扰线的条数
private int fontSize; //字体大小
private int color;//字体颜色
private String code ;//验证码
private Random rnd;
private Paint paint;
private static final int DEFAULT_COUNT = 4;
private static final int DEFAULT_LINE_COUNT = 50;
private static final int DEFAULT_FONT_SIZE = 12;//sp
private static final int DEFAULT_COLOR = Color.BLACK;
public CodeView(Context context) {
super(context);
}
public CodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CodeView);
count = typedArray.getInt(R.styleable.CodeView_count,DEFAULT_COUNT);
lineCount = typedArray.getInt(R.styleable.CodeView_line_count,DEFAULT_LINE_COUNT);
fontSize = typedArray.getDimensionPixelSize(R.styleable.CodeView_font_size,DEFAULT_FONT_SIZE);
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,DEFAULT_FONT_SIZE,getResources().getDisplayMetrics());
color = typedArray.getColor(R.styleable.CodeView_code_color,DEFAULT_COLOR);
typedArray.recycle();
rnd = new Random();
paint = new Paint();
initPaint();
code = getCode();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Rect textRect = getTextRect();//getTextRect() 方法请参考上一章节实现
int width = this.measureWidth(widthMeasureSpec, textRect);
int height = this.measureHeight(heightMeasureSpec, textRect);
setMeasuredDimension(width, height);
}
private String getCode(){
String str = "";
for(int i = 0; i < count; i ++){
str += rnd.nextInt(10);
}
return str;
}
private void initPaint(){
paint.reset();
paint.setAntiAlias(true);
paint.setColor(color);
paint.setTextSize(fontSize);
}
/**
* 计算组件宽度
* @param widthMeasureSpec
* @param textRect
* @return
*/
private int measureWidth(int widthMeasureSpec, Rect textRect){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (mode == MeasureSpec.EXACTLY){
width = size;
}else if(mode == MeasureSpec.AT_MOST){
width = getPaddingLeft() + textRect.width() + getPaddingRight();
}
return width;
}
/**
* 计算组件的高度
* @param heightMeasureSpec
* @param textRect
* @return
*/
private int measureHeight(int heightMeasureSpec, Rect textRect){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY){
height = size;
}else if(mode == MeasureSpec.AT_MOST){
height = getPaddingTop() + textRect.height() + getPaddingBottom();
}
return height;
}
private Rect getTextRect(){
//根据 Paint 设置的绘制参数计算文字所点的宽度
Rect rect = new Rect();
//文字所占的区域大小保存在 rect 中
paint.getTextBounds(this.code, 0, this.code.length(), rect);
return rect;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "code:" + code);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
Rect rect = new Rect(0, 0, width, height);
//绘制外围矩形框
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1);
Rect rect1 = new Rect(rect);
rect1.inset(2, 2);//缩小一点
canvas.drawRect(rect1, paint);
paint.setStyle(Paint.Style.FILL);
//绘制随机干扰线
paint.setColor(Color.GRAY);
for(int i = 0; i < lineCount; i ++){
int x1 = rnd.nextInt(width);
int y1 = rnd.nextInt(height);
int x2 = rnd.nextInt(width);
int y2 = rnd.nextInt(height);
canvas.drawLine(x1, y1, x2, y2, paint);
}
paint.setColor(color);
//绘制文字
Rect textRect = getTextRect();
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
int x = (width - textRect.width()) / 2;
int y = (int) (height / 2 +
(fontMetrics.descent- fontMetrics.ascent) / 2
- fontMetrics.descent);
canvas.drawText(code, x, y, paint);
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
code = getCode();
requestLayout();//重新调整布局大小
}
public int getLineCount() {
return lineCount;
}
public void setLineCount(int lineCount) {
this.lineCount = lineCount;
invalidate();//重绘
}
public int getFontSize() {
return fontSize;
}
public void setFontSize(int fontSize) {
this.fontSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, fontSize,
getResources().getDisplayMetrics());
initPaint();
requestLayout();//重新调整布局大小
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
initPaint();
invalidate();//重绘
}
/**
* 刷新验证码
*/
public void refresh(){
code = getCode();
invalidate();
}
public String theCode(){
return code;
}
}
如图 所示是 layout_width 和 layout_height 属性在不同情况下的运行效果。
练习作业
1、请定义一个自定义组件,该组件派生自 ImageView,通过定义 water_image(水印图片)、water_text(水印文字)、water_position(水印的位置:左上角、右上角、左下角、右下角,枚举类型)三个属性将水印绘制在指定位置。
提示:如图 所示的效果图中,在图片的右上角添加了包含一个小图片和“韬睿科技”4 个字的水印。
谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309