Github: https://github.com/aixiaozi/CustomView/tree/master
Known Issue:
york:image="@drawable/Icon"
这个暂时不能实现,有空的话会去 fix
10/4/2018, fix this issue: https://www.jianshu.com/p/7ad527ed6daa
1. 重要步骤
- 绘图,通过重写OnDraw方法控制View的渲染效果
- 交互,重写OnTouchEvent方法实现与用户的交互
- 测量,重写OnMeasure测量控件显示位置
- 属性,attrs.xml中自定义控件的属性,通过TypedArray读取属性
- 保存状态,避免配置改变时丢失View的状态,重写OnSaveInstanceState和OnRestoreInstanceState方法保存、恢复状态
接下来通过一个例子详细的介绍一下如何自定义View,实现一个图片➕文字说明的控件
2. 构造方法
class TitleImageView:View
{
public TitleImageView (Context context) : this (context, null)
{
}
public TitleImageView (Context context, IAttributeSet attrs) : this (context, attrs, 0)
{
}
public TitleImageView (Context context, IAttributeSet attrs, int defStyle) : base (context, attrs, defStyle)
{
}
}
3.通过 Xml 自定义 View 属性
在**Resources/values
下新建attrs.xml
文件,在attrs
**中定义属性和声明样式。确定我们能在xml中定义的属性,然后写如下定义:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="titleText" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="image" format="reference" />
<attr name="imageScaleType">
<enum name="fillXY" value="0" />
<enum name="center" value="1" />
</attr>
<declare-styleable name="TitleImageView">
<attr name="titleText" />
<attr name="titleTextSize" />
<attr name="titleTextColor" />
<attr name="image" />
<attr name="imageScaleType" />
</declare-styleable>
</resources>
另外一种写法:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TitleImageView">
<attr name="titleText" format="string" />
<attr name="titleTextSize" format="dimension" />
<attr name="titleTextColor" format="color" />
<attr name="image" format="reference" />
<attr name="imageScaleType">
<enum name="fillXY" value="0" />
<enum name="center" value="1" />
</attr>
</declare-styleable>
</resources>
定义了自定义属性,我们就可以在xml中进行使用,不同的是我们自定义属性的命名空间是不同的,我们需要在布局的根节点或自定义View中加上定义命名空间才能使用自定义属性
xmlns:app="http://schemas.android.com/apk/res-auto"
format是指该属性的取值类型:string,color,demension,integer,enum,reference,float,boolean,fraction,flag
具体介绍参考:http://www.jb51.net/article/40069.htm
4.借助TypedArray类提取我们定义的属性,编写类属性可以通过代码设置View属性
class TitleImageView:View
{
private Bitmap image;
private ImageScale imageScaleType;
private string titleText;
private Color titleTextColor;
private int titleTextSize;
public Bitmap Image {
get {
return image;
}
set {
image = value;
Invalidate ();
RequestLayout ();
}
}
public ImageScale ImageScaleType {
get {
return imageScaleType;
}
set {
imageScaleType = value;
Invalidate ();
}
}
public string TitleText {
get{
return titleText;
}
set{
titleText = value;
Invalidate ();
RequestLayout ();
}
}
public Color TitleTextColor {
get {
return titleTextColor;
}
set {
titleTextColor = value;
Invalidate ();
}
}
public int TitleTextSize {
get{
return titleTextSize;
}
set{
titleTextSize = value;
Invalidate ();
RequestLayout ();
}
}
public enum ImageScale
{
FillXY,
Center
}
public TitleImageView (Context context) : this (context, null)
{
}
public TitleImageView (Context context, IAttributeSet attrs) : this (context, attrs, 0)
{
}
public TitleImageView (Context context, IAttributeSet attrs, int defStyle) : base (context, attrs, defStyle)
{
TypedArray typedArray = Context.Theme.ObtainStyledAttributes (attrs, Resource.Styleable.TitleImageView, defStyle, 0);
int count = typedArray.IndexCount;
try {
for (int i = 0; i < count; i++) {
int index = typedArray.GetIndex (i);
switch (index) {
case Resource.Styleable.TitleImageView_image:
image = BitmapFactory.DecodeResource (Resources, typedArray.GetResourceId (index, 0));
break;
case Resource.Styleable.TitleImageView_imageScaleType:
imageScaleType = (ImageScale)typedArray.GetInt (index, 0);
break;
case Resource.Styleable.TitleImageView_titleText:
titleText = typedArray.GetString (index);
break;
case Resource.Styleable.TitleImageView_titleTextColor:
titleTextColor = typedArray.GetColor (index, Color.Black);
break;
case Resource.Styleable.TitleImageView_titleTextSize:
//获取尺寸三个方法的介
titleTextSize = typedArray.GetDimensionPixelSize (index, (int)TypedValue.ApplyDimension (ComplexUnitType.Sp, 16, Resources.DisplayMetrics));
break;
default:
break;
}
}
} catch (System.Exception ex) {
throw ex;
} finally {
typedArray.Recycle ();
}
}
}
代码中View的属性发生改变时我们需要进行重绘和重新布局。所以在属性赋值时调用了Invalidate
(重新绘制OnDraw
)和RequestLayout
(重新布局OnLayout
)方法.
5. 计算视图宽高
重写OnMeasure
方法,按照用户定义的宽度高度进行绘制,View
会先做一次测量,计算出自己占用多大的面积
protected override void OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
base.OnMeasure (widthMeasureSpec, heightMeasureSpec);
//计算宽度 以图片宽度作控件宽度
int minWidth = PaddingLeft + PaddingRight + image.Width;
var width = ResolveSizeAndState (minWidth, widthMeasureSpec, 0);
//计算高度
int minHeight = PaddingBottom + PaddingTop + image.Height + textBound.Height ();
var height = ResolveSizeAndState (minHeight, heightMeasureSpec, 0);
// 测量完成后必须调用setMeasuredDimension方法
SetMeasuredDimension (width, height);
}
ResolveSizeAndState
方法返回一个合适的尺寸,只要将测量模式和我们计算的宽度高度传进去即可,该方法在新的api中才有,无法兼容3.0以下,我们可以根据源码定义自己的ResolveSizeAndState
方法:
private int ResolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
int result = size;
int specMode = MeasureSpec.GetMode(measureSpec);
int specSize = MeasureSpec.GetSize(measureSpec);
switch (specMode) {
case MeasureSpecMode.Unspecified:
result = size;
break;
case MeasureSpecMode.AtMost:
if (specSize < size) {
result = specSize | View.MeasuredStateTooSmall;
} else {
result = size;
}
break;
case MeasureSpecMode.Exactly:
result = specSize;
break;
}
return result | (childMeasuredState&View.MeasuredStateMask);
}
6. 初始化画笔
在构造函数中初始化一个Paint和两个Rect
rect = new Rect (); //图片位置
paint = new Paint ();
paint.TextSize = TitleTextSize;
paint.Color = titleTextColor;
textBound = new Rect ();//底部说明文字位置
// 计算了描绘字体需要的范围
paint.GetTextBounds (titleText, 0, titleText.Length, textBound);
7. OnDraw
重写OnDraw方法,根据定义的属性绘制图形。在参数canvas上绘制我们希望的View样式
protected override void OnDraw (Canvas canvas)
{
base.OnDraw (canvas);
rect.Left = PaddingLeft;
rect.Right = Width - PaddingRight;
rect.Top = PaddingTop;
rect.Bottom = Height - PaddingBottom;
paint.TextSize = TitleTextSize;
paint.Color = titleTextColor;
paint.SetStyle (Paint.Style.Fill);
//当前设置的宽度小于字体需要的宽度,将字体改为xxx...
if (textBound.Width () > Width) {
TextPaint paint = new TextPaint (this.paint);
string msg = TextUtils.Ellipsize (titleText, paint, (float)Width - PaddingLeft - PaddingRight, TextUtils.TruncateAt.End);
canvas.DrawText (msg, PaddingLeft, Height - PaddingBottom, paint);
} else {
canvas.DrawText (titleText, Width / 2 - textBound.Width () / 2, Height - PaddingBottom, paint);
}
//取消使用掉的部分
rect.Bottom -= textBound.Height ();
if (imageScaleType == ImageScale.FillXY) {
canvas.DrawBitmap (image, null, rect, paint);
} else {
rect.Left = Width / 2 - image.Width / 2;
rect.Right = Width / 2 + image.Width / 2;
rect.Top = (Height - textBound.Height ()) / 2 - image.Height / 2;
rect.Bottom = (Height - textBound.Height ()) / 2 + image.Height / 2;
canvas.DrawBitmap (image, null, rect, paint);
}
}
8. 使用
Generic example:
namespace My.Namespace
{
class MyCustomView : View
{
}
}
Is used in following way in .axml
<my.namespace.MyCustomView />
Do mind case, namespace in .axml
is lowercase, classname
is same case as C#
declaration.
<view.TitleImageView
xmlns:york="http://schemas.android.com/apk/res-auto"
android:layout_width="100dp"
android:layout_height="200dp"
android:layout_margin="10dp"
android:padding="10dp"
york:image="@mipmap/icon"
york:imageScaleType="fillXY"
york:titleText="hello andorid ! "
york:titleTextColor="#ff0000"
york:titleTextSize="30sp" />