第六章 自定义组件
6.1、概述
Android SDK为我们提供了一天完整的组件库,数量多,功能强,涉及到到方方面面。但是,我们依然看到软件市场上的每一个 APP都有自己独特的东西,绝不是千篇一律的,而且也会和 iOS 互相借鉴。这就需要我们对组件进行定制,实现自己独树一帜的用户体验和界面风格 不管是谱图的组件还是容器,开发时都有章可循的,找到其中的规律,根据实际的需求,一步步慢慢的就能实现。学习是一个循次渐进,由浅入深的一个过程。可以通过于都别人一写好的代码去理解别人思维模式和编程技巧。然后消化成为自己的思维和风格。当然,做好的源码莫过于 Google 自家的 API Demos ,里面包含了我们需要的方方面面。 通常说,自定义组建有三种自定方式- 从 0 开始定义自定义的组件,组件集成至 View
- 从已有组件扩展。比如: 从 ImageView 类扩展出功能更强大或者更有个性化的组件
- 将多个已有的组件合成一个新歌组件。比如,侧边带字母索引的 ListView
6.2、自定义组件的基本结构
组件主要由两部分构成:组件类和属性定义。我们从第一种定义方式说起 创建自定义组件类最基本的做法就是集成自类 View。其中,有三个构造方法和两个重写的方法比较重要。- 构造方法
- public 类名(Context context)
- public 类名(Context context, AttributeSet attrs)
- public 类名(Context context, AttributeSet attrs, int defStyleAttr)
- 第一个只有一个参数
- 在代码中创建组件时会调用该构造方法。比如:创建一个按钮,Button btnOK = new Button(this),this 是指当前 Activity,activity是 Context的子类
- 第二个两个参数
- 在 layout 布局文件中使用时调用。参数 attrs 表示当前配置中的属性集合。例如:在 layout.xml 中的 android:layout_height = “wrap_co-ntent”
- 第三个三个参数
- 第三个参数一般不会自动调用,当我们在 Theme中定义了 style属性时通常在第 2 个构造方法中,手动调用
- 绘图
- protected void onDraw(Canvas canvas)
- 该方法用于显示组件的外观。最终的先显示结果需要通过canvas 绘制出来。在 View 类中,该方法并没有任何的默认实现
- 测量尺寸
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- 这是一个 protected 方法,意味着该方法主要用于子类的重写和扩展,如果不重写该方法,父类 View 有自己的默认实现。在 Android 中,自定义组件的大小都由自身通过 onMeasure() 进行测量,不管界面布局有多么复杂,每个组件都负责计算自己大小
6.3、重写 onMeasure( )方法
View 类对于 onMeasure( )方法有自己的默认实现 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在该方法中,调用了 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)方法,应用测量后的宽度和高度,这是必须调用的,我们可以通过 getMeasuredWidth() 和 getMeasuredHeight() 方法获取这个宽度和高度 大部分情况下,onMeasure( )方法,都是需要重写的,用于计算组件的宽度值和高度值。定义组件时,必须指定 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方法传入。关于 MeasureSpec.UNSPECIFIED 常量的用法参考 10-5小节
了解了其实现,接下来我们需要继续思考如何获取 widthMeasureSpec 和 heightMeasureSpec 参数的前 2 位,与 后 30位? 其实通过 为运算可以得到,我们以 widthMeasureSpec 为例:
- 获取尺寸模式:widthMeasureSpec &0x3 << 30
- 获取尺寸大小:widthMeasureSpec << 2 >> 2
上面的写法不一而足,很显然这样并不适合开发。所以,提供了一个名为 MeasureSpec 的类用于计算模式和大小:
- int mode = MeasureSpec.getMode(widthMeasureSpec)
- int size = MeasureSpec.getSize(widthMeasureSpec)
为了充分的说明 onMeasure( )方法的作用,我们通过下面代码模拟 TextView的功能,也就是在组件中绘制文字,为了简单起见,我们只写一行以内的字符串。在案例中,比较麻烦的是沪指文字时,public void drawText(String text, float x, float y, Paint
paint)方法中的参数 y 确定,这样从字体的基本结构说起(链接:FontMetrics 的图解和Log)
如图 6-2 所示,从技术层面上来说:字符有下面几部分分构成,从文字上理解可能比较晦涩。通过 6-2 所示的示意图也许很容易找到答案。简单的来说,常用的字符的高度是 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 是文字所在区域的高度 下面是 上面大致的实现代码,我们定义一个 private Rect getTextRect()方法 ,用于获取文组所占区域大小,其组件的宽度和高度我们也进行了设置public class FirstView extends View {
private static final String TEXT = "三千越甲可吞吴" ;
private Paint paint ;
public FirstView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setTextSize(60) ;
paint.setColor(Color.RED) ;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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);
}
@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) ;
System.out.println("viewWidth = "+viewWidth+"\n_viewHeight"+viewHeight+"\n_height = "+textRect.height() +"\n_width = "+textRect.width()+"\n_descent = "+fontMetrics.descent
+"\n_ascent = "+fontMetrics.ascent);
canvas.drawText(TEXT, x, y, paint);
}
/**获取文字所占的尺寸*/
private Rect getTextRect() {
//根据 Paint 设置的绘制参数计算文字所占的宽度
Rect rect = new Rect() ;
//文字所占的区域保存在 rect中
paint.getTextBounds(TEXT, 0, TEXT.length(), rect) ;
return rect;
}
private int measureWidth(int widthMeasureSpec, int textWidth) {
int mode = MeasureSpec.getMode(widthMeasureSpec) ;
int size = MeasureSpec.getSize(widthMeasureSpec) ;
int width = 0 ;
if(mode == MeasureSpec.EXACTLY){
//宽度为 math_parent 和 具体值,直接将 size 作为组件的宽度
width = size ;
}else if(mode == MeasureSpec.AT_MOST){
//宽度为 wrap_content ,宽度需要计算,此处为位子的宽度
width = textWidth;
}
return width;
}
private int measureHeight(int heightMeasureSpec, int textHeight) {
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 = textHeight;
}
return height;
}
}
上述代码中,测试组件宽度时,定义了 private int measureWidth(int widthMeasureSpec, int textWidth) ,表示宽度可能为 match_parent 或精确值,直接将获取的尺寸大小返回。如果模式为 MeasureSpec.AT_MOST,表示、宽度为 warp_content,则需要计算组件的宽度,因为组件内容为文字。所以,文字占用的宽度时多少组件的宽度也是多少。此时,组件的宽度时 textWidth。测试高度也是同样的道理 重写 onDraw( )方法绘制组件的外观时,需要将文字在指定的位置上绘制来,x 方法比较简单,其值为组件宽度减去文字所占宽度除以 2;而 y 的大小则是字体的 baseline 值,其大小为 viewHeight /2 + (FontMetrics.descent - fontMetrics.ascent)/2 - fontMetrics.descent,viewHeight 是组件测量后的高度 最后,我们比较一下 layout_width 和 layout_height 两个属性的值在不同情况下的运行结果 可以看出,当 layout_width=“wrap_content”时,组件大小恰好是文字所占区域大小,文字刚刚能显示;当 layout_width=”match_parent”且 layout_height=”match_parent”时,组件占满了整个区域;当 layout_width=”match_parent” 且 layout_height=”
wrap_content “时,水平方向占满了整个宽度,而高度七号是文子的高度
6.4、组件的属性
在 FirstView 组件类中,要显示的文字定义了常量——private static final String TEXT = “三千越甲可吞吴” ;显示这样做事不可取,我们应该可以随意定义文字,这需要用到组件的属性从 View 继承后,View已经具备了若干你默认属性。比如:layout_width、layout_height,所以,在 FirstView 类中,指定该类的宽度和高度时,我们并没有特别定义和编程,找到 sdk/platforms/android-21/data/res/values/attrs.xml 文件,打开后,定位到 这一行,接下来的 500多行都是与 View的默认属性有关,常用的属性比如:layout_width、layout_height、background、alpha 等属性都是默认的属性。你可以打开上述文件进行更详细的了解。下面大致介绍自定义属性的定义
6.4.1 属性的基本定义
除了 View类中定义的默认属性外,我们也能自定义属性。自定义属性主要有以下几个步骤 相关链接htyman:Android 深入理解Android中的自定义属性 :- 在 res/values/attrs.xml 文件中为指定组件定义 declare-styleable 标记,并将所有的属性都定义在该标记中
- 在 layout 文件中使用自定义属性
- 在组件的构造方法中读取属性
在 res/values 目录下,创建 attrs.xml 文件,内容大概如下:
<declare-styleable name="FirstView">
<attr name="attr" foramt="string">
</declare-styleable>
组件的属性都应该定义在 declare-styleable 标记中,该标记的 name 属性值一般来说都是组件类的名称(此处为 FirstView)、虽然也可以区别的名字,但和组件相同可以提高代码的可读性。组件的属性都定义的 declare-styleable 标记内,成为 declare-styleable 标记的字标记,每隔属性有两部分组成——属性名和属性类型。属性通过 attr 来标识,属性类型为 format。可选的属性类型如图 6-6 所示
- string:字符串
- boolean:布尔
- color:颜色
- dimension:尺寸,可以带单位,比如长度通常为 dp,字体通常为 sp
- enum:枚举,需要在 attr 标记中使用标记定义枚举值,例如 sex 作为性别,有两个枚举值:MALE 和 FEMALE
-
- <attr name=”Sex” format=”eunm”>
- <enum name=”MALE” value=”0”/>
- <enum name=”FEMALE” value=”1”/>
- </attr>
- flag:标识符,常见的 gravity 属性就是属性该类型,如图 6-7 所示
- flag类型的属性也有一个字标记,语法形如;
-
- <attr name=”x” format=”flag”>
- <flag name=”f1” value=”0”/>
- <flag name=”f2” value=”1”/>
- <attr>
- float:浮点型
- fraction:百分数,在动画资源<scale><ratate>等标记中<fromX><fromY>等属性就是fraction类型的属性
- integer:整数
- reference:引用,引用另一个资源。比如 android:paddingRight = “@dimen/activity_horizontal_margin”就是引用另一个资源
在 FirstView 组件中,text应该作为属性来定义,并且为 string 类型,我们在 attrs.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 class styleable {
public static final int[] FirstView = {
0x7f010000
};
public static final int FirstView_text = 0;
};
定义好属性的名称和类型后,属性就可以使用了,在布局文件 latout.xml 中,首先要定义好 属性的命名空间(namespace),默认情况下,xml 文件中的根元素按如下定义<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"
....
</RelativeLayout>
默认的命名空间为“android”,是由语句 xmlns:android=“http://schemas.android.com/apk/res/anroid”决定的,对于自定义属性来说,必须定义其它的命名空间,且必须按下面的要求定义 xmlns:app=”http://schemas.android.com/apk/res-auto” 其中,app 指自定义的命名空间,也可以使用其它代替。后面的 “http://schemas.android.com/apk/res-auto“ 则是固定的,有了至二个命名空间后,访问前面的 text 属性则因该之二杨赋值: app:text]=”Android 自定义组件” 。事实上,IDE 也有相应的提示(Android Studio 的智能图示功能比 eclipse ADT 要强大的多,在 attrs.xml文件中后者没有提示)我们需要在 FirstView类中读取 app 属性,组件运行后,所有属性都将保存在 AttributeSet 集合中并通过构造方法传入,我们通过 TypedArray 可以读取指定的属性值
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.FirstView) ;
text = a.getString(R.styleable.FirstView_text) ;
a.recycle() ;
语句 TypedArray a = context.obtainStyledAtrributes(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()方法释放资源6.4.2 读取来自 style 和 Theme 中的属性
组件的属性可以在下面 4 个地方定义- 组件
- 组件的 style 属性
- theme
- theme 的 style 属性
<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 属性<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.anroid.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FourActivity"
android:orientation="vertical"
>
<myview.AttrView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:attr1="arrt1"
style="@style/viewStyle"
></myview.AttrView>
</LinearLayout>
app:attr1=“attr1”应用了属性 attr1,style=“@style/viewStyle”应用了属性 attr2,其中,@style/viewStyle 定义在 res/values/style.xml 文件中。当然,该文件还行一了整个 app 工程的主题(Theme),配置如下<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/myDefault,@style/myDefault 中应用了属性 attr4。总结起来,attr1是组件的直接属性,attr2 是组件的 style 属性引用的属性。attr3 是工程主题(theme)属性,attr4是工程主题(theme)的 style 属性。现在,我们在 AttrView 构造方法中读取这 4 个属性值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);
我们在 AttrView(Context context,Attribute attrs)构造方法中,调用了 AttrView(Context context,AttributeSet attrs,int defStyleAttr)构造方法,与上一个案例相比,我们调用了另一个重载的 obtainStyleAttributes( )方法,该方法的原型为 public TypedArray obtainStyledAttributes (AttributeSet set, int[] attrs, int defStyleAttr, intdefStyleRes),了解一下该方法的参数作用
- set:属性值的集合
- attrs:我们要获取的属性的资源 ID 的一个数组,我们定义了 attr1、attr2、attr3、attr4 这四个属性自动生成的索引会存储到 R.styleable.AttrView 数组中,该数组就是 attr 参数
-
- 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 属性赋值时起作用
6.5、案例 1 :圆形 ImageView 组件
ImageView是我们常用的组件之一,但该组件存在一定的局限性。比图只能显示矩形的图片,现在很多 App 在显示头像的时都支持圆形或其它形状,所以大致介绍 通过 ImageView 实现圆形图片因为是显示图片,我们自然想到组件类因该继承自 ImageView,Imageview 已经帮我们做了大部分工作,比如 : 已经重写了 onMeasure( )方法,不需要再重新计算尺寸,设置图片也已经实现了,我们还哟啊添加一些功能。比如:显示出来的图片是 圆的,支持添加圆形框线,为圆形框线指定颜色和大小等等。另外,还要删除 ImageView 与 本需求冲突的功能,Image 支持 scaleType,用于指定图片的缩放类型,但是我们打算把这个功能删除。要提醒的是,其实我们最终的图片是一个椭圆,如果要显示成圆形,请将组件的宽度和高度设为一致
首先,我们事先定义两个属性:圆形框线的粗细和颜色,定义粗细时使用 dimension 类型,而颜色则使用 color 类型
attr.xml
<declare-styleable name="CircleImageView">">
<attr name="circle_border" format="dimension"/>
<attr name="circle_border_color" format="color"/>
</declare-styleable>
其次,定义 CircleImageView组件类,该类继承自 Imageview 类public class CircleImageView extends ImageView {
private Paint paint ;
private Xfermode xfermode ;
private Path path = new Path() ;
private int border;
private int borderColor;
public CircleImageView(Context context) {
this(context,null);
}
public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
//去两者交集,交集为 下层(DST)
xfermode = new PorterDuffXfermode(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 layerld = canvas.saveLayer(getPaddingLeft(),getPaddingTop(),width-getPaddingRight(),height-getPaddingBottom()
,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(Style.FILL);
paint.setColor(Color.BLACK) ;
path.reset() ;
path.addOval(ovalRect, Direction.CCW) ;
canvas.drawPath(path, paint) ;
paint.setXfermode(null) ;
canvas.restoreToCount(layerld) ;
System.out.println(ovalRect.toString());
System.out.println("border = "+border);
//画空心圆
if(border != 0){
paint.setStyle(Style.STROKE) ;
paint.setColor(borderColor) ;
paint.setStrokeWidth(border) ;
ovalRect.inset(border*0.5f, border*0.5f);
canvas.drawOval(ovalRect, paint) ;
}
}
}
上述代码中,主要重写了 onDraw( )方法,ImageView 作为父类,可以通过 src 属性或 etImageResource()、setImageBitmap( )等方法设置图片,getDrawable( )方法用于获取设置的图片,得到图片后,需要在图片上画一个实心椭圆作为遮罩层,该椭圆的内切椭圆,通过语句 RectF ovalRect = new Rect(0,0,width,height)指定。画椭圆图片时,先创建一个 layer ,调用 canvas.drawBitmap(bitmap, new Rect(0,0,mDrawable.getIntrinsicWidth(),mDrawable.getIntrinsicHeight()),ovalRect,null) 语句将图片绘制到 canvas 画布上并进行缩放。然后,为 paint 指定 DST_IN 位图模式,早 Path 对象中添加一个椭圆,并与图片进行 DST.IN 位图运算(只有 Path 对象才能进行位图运算,不能直接调用 drawOval( )方法),于是就得到了圆形图片了 在绘制边框的时候,需要进行对基础进行大致的解释。调用 border = a.getDimensionPixelSize(R.styleable.CircleImageView_circle_border, 0) 语句获取边框的大小后,得到数据单位 始终是像素(px)。这样不管使用 dp 还是 sp 都可以得到一致的数值。画边框线时,仅仅只有 border 耗时不够的,应为 border 本身占用了一定的宽度,必须调用 ovalRect.inset(border*0.5f,border*0.5f)语句将圆形边框缩小(注意要除以 2)hyman 大神链接 :
6.6、案例 2:验证码组件 CodeView
验证码在 web 开发中非常常见,用于防止非法暴力破解,随着图形设别技术的发展,验证码也越来越复杂和多样化,以适应当前破解技术的不断提高。本节将定义一个验证码组件,并未用户提供订制功能,在运行过程中与组件交互我们将验证码组件名为 CodeView,默认情况下,随机生成 4个数字和 50 条干扰线,如果 用户测试次数过多你,可以动态加大验证码的难度,比如新增验证码的个数、增加干扰线条数、改变验证码颜色等。提供的主要功能有:
- 刷新验证码
- 改变验证码个数
- 改变验证码干扰线条数
- 改变验证码字体大小
- 改变验证码颜色
- 获取当前验证码
本组件的属性主要包括验证码个数、干扰条数、字体大小和字体颜色,在 attrs.xml 文件中定义如下属性。其中 font_size 表示字体大小,类型为 dimension,到时候使用 sp 作为字体单位
<declare-styleable name="CodeView">
<attr name="count" format="integer"/>
<attr name="line_count" format="integer"/>
<attr name="font_size" format="dimension"/>
<attr name="code_color" format="color"/>
</declare-styleable>
组件类 CodeView 从 View中派生,这是一个从 0 开始的自定义组件,其实从 TextView 继承也是一个不错的注意。在 CodeView 类中,定义了如下的成员变量和常量,常量主要是用于定于各种属性的缺省值 在构造方法 public CodeView(Context context, AttributeSet attrs, int defStyleAttr) 中读取出个属性的值,重点注意一下 字体大小的读取方法。字体大小设计单位问题,一般使用 sp 作为字体单位,而 我们使用 TypedValue 类的 getDimensionPixelSize( )方法读取像素。所以,需啊哟进行单位的转换,该工作交给 TypedValue 类的静态方法 applayDimension( )完成,applyDimension( )的作用是进行单位的换算,其方法原型为:public static float applyDimension(int unit, float value,DisplayMetrics metrics) ,其中 unit 是目标单位,可选值如图 6-14 所示。value 是要换算的值,metrics 通过 getResources().getDisplayMetrics() 即可得到 测量组件尺寸时,考虑了从 View 类继承下来的 padding 属性,该属性我们用于定义验证码与边框线的距离,增强视觉观感,getPaddingLeft() 、 getPaddingTop() 、 getPaddingRight() 、
getPaddingBottom() 四个方法分别用于获取离 上、下、左、右 四个方向的距离,计算组件的宽度和高度时,需要加上个方向的 padding 值
绘图分为三个部分,外边框、干扰线、验证码。外边框和验证码 颜色相同,为了显示的更加完整,在组件矩形区域大小的基础上向内收缩 2 个距离,边框线是空心矩形。所以,将 style 定义为 style.STROKE,完成后又还原成 Style.FILL,因为后面绘制文字时必须是实心样式。干扰线是若干条随机生成的直线,直线的两个点都是随机的,x 坐标在 0~width(组件宽度)之间,y 坐标在 0~height(组件高度)之间。文字的位置水平垂直居中。
最后,是相关的交互功能。当通过外部改变绘制的结果时,有两种情况:一种是只需要刷新即可。如:改变颜色、增减干扰线、刷新等功能。这种情况需要调用 invalidate( )方法进行重绘,另一种是组件的尺寸的变化,需要重新测量组件的大小。如:随机数个数字变化、字体大小改变等功能。这种情况需要调用 requestLayout( )方法,该方法依次调用 onMeasure( ) 和 onDraw( )两个方法,先重新测量组件的尺寸,在重新绘制
public class CodeView extends TextView {
/**验证码的数字个数*/
private int count;
/**干扰的线条数*/
private int lineCount ;
/**字体大小*/
private int fontSize ;
/**字体颜色*/
private int color;
/**验证码*/
private String code ;
private Random random ;
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) {
this(context,null);
}
public CodeView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CodeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CodeView) ;
count = a.getInt(R.styleable.CodeView_count, DEFAULT_COUNT) ;
lineCount = a.getInt(R.styleable.CodeView_line_count, DEFAULT_LINE_COUNT) ;
fontSize = a.getDimensionPixelSize(R.styleable.CodeView_font_size,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
DEFAULT_FONT_SIZE, getResources().getDisplayMetrics())) ;
color = a.getColor(R.styleable.CodeView_code_color, DEFAULT_COLOR) ;
a.recycle();
random = new Random();
paint = new Paint() ;
initPaint() ;
code = getCode();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Rect rect = getTextRect() ;
int width = this.measureWidth(widthMeasureSpec, rect) ;
int height = this.measureHeight(heightMeasureSpec, rect);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
System.out.println("code = "+code);
int width = getMeasuredWidth() ;
int height = getMeasuredHeight() ;
Rect rect = new Rect(0,0,width,height) ;
//绘制外围矩形框
paint.setStyle(Style.STROKE) ;
paint.setStrokeWidth(1) ;
Rect rect1 = new Rect(rect) ;
rect1.inset(2, 2) ; //缩小一点
canvas.drawRect(rect1, paint) ;
paint.setStyle(Style.FILL) ;
//绘制随机干扰线
paint.setColor(Color.GRAY) ;
for (int i = 0; i < lineCount; i++) {
int x1 = random.nextInt(width) ;
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.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);
}
private void initPaint() {
paint.reset();
paint.setAntiAlias(true) ;
paint.setColor(color);
paint.setTextSize(fontSize) ;
}
private String getCode() {
String str = "" ;
for (int i = 0; i < count; i++) {
str += random.nextInt(10) ;
}
return str;
}
private Rect getTextRect() {
Rect rect = new Rect();
paint.getTextBounds(code, 0, code.length(), rect) ;
return rect;
}
private int measureWidth(int widthMeasureSpec, Rect textRect){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec) ;
int widht = 0;
if(mode == MeasureSpec.EXACTLY){
widht = size ;
}else if(mode == MeasureSpec.AT_MOST){
widht = getPaddingLeft() + textRect.width() + getPaddingRight() ;
}
return widht ;
}
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 ;
}
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;
}
}