前言:
Android开发与进阶的第一个门槛就是自定义view,大多数的设计不会按照Android所具有的优势和特性去设计,而是一味跟随IOS的设计风格去走,当然IOS的的设计相当漂亮,系统也提供了许多优秀的控件给开发者使用,这也符合苹果的始终所坚持的标准化与统一化。但是要把这些控件所带有的效果同样,甚至功能实现在Android上,是有许多不尽如人意的地方,有许多是Android所不擅长的,还有一些设计是系统无法满足的,所以学习自定view是相当重要的。好多同学一听自定view感觉比较难,想学习网上巴拉巴拉好多基本都是上来就,贴代码,连最基本的自定view的流程都不知道,云里雾里的看一下代码,看不懂,比较难就得过且过了。如果学自定义view最好是结合网上的一些优秀的帖子,和View的源码加官方的doc文档,虽然一些开发的同学英文不是很好还是尽量的去看,译文和原文还是有一定的差距的。
本篇目录结构:
一:查看官方文档及其他资料了解View。
二:整体的了解自定View的具体的流程。
三:对自定View的一些扩展和总结。
<一>查看官方文档及其他资料了解View
首先查看官网了解view,原文就不上了截了一些关键图上来
View的官方说明文档:https://developer.android.com/reference/android/view/View.html
View的直接子类和间接子类,Direct Subclasses,是直接的子类的意思,indirect是间接子类的意思,View的间接子类就比较多了。看一下我们自定义View所用的到的几个方法和对应的参数都是做什么的,先看构造方法:
、它有四个构造方法,说明一下这几个参数的意思Context这个是最常用的了上下文环境, AttributeSet attrs 是属性集合,int defStyleAttr是默认的指定的一个style中的属性,int defStyleRes默认的一个stye资源。第一个构造方法是在代码当中我们New的时候调用的,第二个构造方法是系统调用的,当布局文件中使用该view,第三个方法一般就是我们自己调用了,至于第四个构造方法,没有深入的研究有两篇讲的比较好的博客给大家,大家可以去深入的了解。
http://blog.csdn.net/yuzhouxiang/article/details/6958017
http://blog.csdn.net/mybeta/article/details/39993449
这两篇讲的比较详细,也比较清楚。
再来看一下onDraw这个方法:
提供一个画布供你去操作。
再来看看onMeasure方法:
里面返回了2个参数,int widthMeasureSpec, int heightMeasureSpec 父类的约束条件,大概意思就是子类可以重写该方法,但是约定子类重写该方法必须调用 setMeasuredDimension(int, int) 方法存储处理过后的宽和高。
还有一个方法就是onLayout:
这个方法在自定义的View当中不常用,但是View还可以作为容器放别的View,这个就比较重要了,他有五个参数是boolean changed, int left, int top, int right, int bottom,是否发生变化,上下左右这几个参数就是当子View发生变化的时候将会被调用。所以当我们自定义的布局容器里面要摆放子View的位置就在这里面操作。
自定义View大概就了解这些。
<二>整体的了解自定义View的一些流程。
1,根据自己的需求自定义控件属性
2.重写构造方(必须)
3.重写onDraw方法(必须)
4.重写onMeasure
5.重写onLayout**
流程1,4,5根据自己的View去分析要不要重写,但是2,3必须要重写。
我们自定义一个View,类似于Textview的一个控件吧,先来自定义属性,首先是字体大小,字体颜色,字体text这几个就行了。
1,.自定义属性:在values下的新建attrs.xml文件,里面自定义的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--定义属性-->
<attr name="mText" format="string"></attr>
<attr name="mTextColor" format="color"></attr>
<attr name="mTextSize" format="dimension"></attr>
<!--指定具体的View所拥有的属性-->
<declare-styleable name="MyTextView">
<attr name="mText"></attr>
<attr name="mTextColor"></attr>
<attr name="mTextSize"></attr>
</declare-styleable>
</resources
format指定了参数的类型,类型共有这几种:
declare-styleable指定了那个View拥有那几个属性。
在XML布局文件中使用需要:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
xmlns:customview="http://schemas.android.com/apk/res-auto"
>
<com.example.customviewdemo.customview.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
customview:mText="测试文本啊哈哈哈哈哈哈哈,哈哈哈啊哈哈哈哈大街上来看建档,laaldlsakdl,立卡简单快乐数据库第六届奥斯卡拉家带口老实交代啦"
customview:mTextSize="15sp"
customview:mTextColor="#2a2a58"
android:padding="20dp"
android:background="#ff0000"
>
</com.example.customviewdemo.customview.MyTextView>
</LinearLayout>
xmlns:customview=”http://schemas.android.com/apk/res-auto”,里面的customview可以自己定义,在控件中引用是必须使用该名字。
这样简单的自定属性就好了,还要把自定义的属性的值取出来用。
2.重写构造方法
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取指定View所拥有的属性的列表
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyTextView, defStyleAttr, 0);
text = typedArray.getString(R.styleable.MyTextView_mText);
textColor = typedArray.getColor(R.styleable.MyTextView_mTextColor, Color.BLACK);
textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_mTextSize, 100);
typedArray.recycle();
initData();
}
我是让第一个构造方法调第二个构造方法,第二个调用第三个构造方法,在第三个方法里面把布局里面的属性的值取出来,当解析布局文件后,将属性集合传回来,我们根据attrs中定义的属性id来获取对应的值,取完一定要回收typedArray.recycle();,不然会报错。取到值就该干嘛,干嘛。
3.重写onDraw将text,color,textsize设置
在重写之前先把设置一下字体的颜色,字体的大小与颜色。
//初始化数据
private void initData() {
textList = new ArrayList<>();
paint = new Paint();
rect = new Rect();
//获取
paint.setTextSize(textSize);
paint.setColor(textColor);
//将字符串的填充到矩形中
paint.getTextBounds(text, 0, text.length(), rect);
}
我们直接无法获取字符串的宽度和高度,别说是length那是字符串的长度。利用Rect,将字符串填充到一个长方形中,获取该长方形的宽高就是字符传的宽高。这是重写的onDraw方法:
//一般重写ondraew方法,
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//String text, float x, float y, Paint paint
//1.第一个参数是要写的字符串,第二个参数是字符串开始的X轴上的位置,第三个参数是Y轴的坐标,第三个参数是画笔
//2.计算字符串要显示到画布上的位置,如果要居中显示,控件大小的一半减去字符串一半的位置作为起始点即X轴坐标,高度也是一样的
//3.将设置了颜色的画笔设置好
//canvas.drawText(text, getWidth() / 2 - rect.width() / 2, getHeight() / 2 -rect.height() / 2, paint);
//换行,满一行换一行,动态的设置起始点的Y轴的位置,
for (int i = 0; i < textList.size(); i++) {
paint.getTextBounds(textList.get(i), 0, textList.get(i).length(), rect);
//speacd是行间距了,不然每一行与下面的一行靠的比较近,比较难看,第一行就不需要了,第一行有padingtop
canvas.drawText(textList.get(i), (getWidth() / 2 - rect.width() / 2), (getPaddingTop() +sepcpading*i+ rect.height() * i), paint);
}
}
有的同学可能一看里面的计算就蒙蔽了不知道算啥呢,画个图就知道了
T就是top的高度,H就是文本的高度,S就是间隔的高度,所以每一行开始的Y轴坐标是
(getPaddingTop() +sepcpading*i+ rect.height() * i)
至于宽度我们是让文本居中显示的看图:
黄色的是控件,红色的是字符串 他们的一半之差就是在控价显示,在X轴上的坐标。因此是
getWidth() / 2 - rect.width() / 2
到这里控件就可以显示文本内容了,但是当我们设置的文本内容比较长的时候会抛出控件之外看不见,还有一个问题就是让我们设置了控件的宽高是包裹着内容,的时候控件会充满父控件,老司机估计都遇到过这个问题。
先解决控件宽度设置为
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
后充满父控件,的原因。先看一哈默认的onMeasure的源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
根据specMode 给返回一个值,当这个specMode返回类型是MeasureSpec.UNSPECIFIED返回控件所支持的最小值,当specMode的是 case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY:返回的数据是父控件给予的最大空间,所以再看看MeasureSpec,是定义在View中的一个内部类:
/**
* measurespec封装了父控件对他的孩子的布局要求。
* 一个measurespec由大小和模式。有三种可能的模式:
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //0
public static final int EXACTLY = 1 << MODE_SHIFT; //1073741824
public static final int AT_MOST = 2 << MODE_SHIFT; //-2147483648
public static int makeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec 有三种模式,UNSPECIFIED ,EXACTLY ,AT_MOST 三种状态,未指定,精确的,至多。在什么情况下会给这三种状态:看下面的图:
所以当我们设置例 了wrap_content返回的是AT_MOST 所以会全屏展示。
第二个问题是超出控件,解决方法是换行,当我们把获取到的字符串,根据字符串的宽度去截取字符串,计算出行数,显示出来就可以了。
要想解决这两个问题就必须重写onMeasure去根据控件的实际尺寸和父控件所给的约数条件去切割字符串,计算行数,存储宽高。
//当View的宽高设置为wrap_content时,父控件的宽高为match_parent,子控件将会全屏显示将会全屏展示
//MeasureSpec.AT_MOST和MeasureSpec.EXACTLY返回的都是父控件所剩余的空间,
// 在子控件为wrap_content和match_parent下都会返回父控件的剩余空间造成上面的现象出现
//解决方法:重写测量方法当模式为MeasureSpec.AT_MOST时将子空间的大小返回给父类即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽高模式,模式为数字
int widthModle = MeasureSpec.getMode(widthMeasureSpec);
int heightModle = MeasureSpec.getMode(heightMeasureSpec);
//获取父类的约数条件下的宽高
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.v("custom", "宽的模式:"+widthModle);
Log.v("custom", "高的模式:"+heightModle);
Log.v("custom", "宽的尺寸:"+widthSize);
Log.v("custom", "高的尺寸:"+heightSize);
int textWidth = rect.width();
Log.v("custom", "文所需宽度:"+textWidth);
if (textList.size() == 0) {
//没有将总的字符串分割,判断一行最多能够放多长的字符串,即父类所给的剩余空间的宽度减去控件内部
// 的pading即为一行的最大长度。
int pading = getPaddingLeft() + getPaddingRight();
int speaceWidth = widthSize - pading;
Log.v("custom", "除去pading的的宽度:"+speaceWidth);
//字符串的长度小于减去pading的长度,一行可以放得下
if (textWidth < speaceWidth) {
textList.add(text);
lineNum = 1;
} else {
isOneLines = false;
spLineNum = textWidth / speaceWidth;
Log.v("custom", "没有去除小数的行数:"+spLineNum);
//除不清,说明多一行需额外加1行
if ((spLineNum + "").contains(".")) {
lineNum = Integer.parseInt((spLineNum + "").substring(0, (spLineNum + "").indexOf("."))) + 1;
Log.v("custom", "去除小数之后的行数"+lineNum);
} else {
lineNum = spLineNum;
}
//字符串的总长度/行数,即每一行的字符串的长度
float nLine = (text.length() / lineNum);
int lineLength;
if((nLine + "").contains(".")){
lineLength = Integer.parseInt((nLine + "").substring(0, (nLine + "").indexOf("."))) + 1;
}else {
lineLength=(int)nLine;
}
Log.v("custom", "文本内容:"+text);
Log.v("custom", "文本总长度:"+text.length());
Log.v("custom", "能绘制文本每一行的字符串的长度:"+lineLength);
Log.v("custom", "需要绘制:"+lineNum+"行");
Log.v("custom", "lineLength:"+lineLength);
//分割字符串,将字符串分割成
for (int i = 0; i < lineNum; i++) {
String lineStr;
if (text.length() < lineLength) {
lineStr = text.substring(0, text.length());
} else {
lineStr = text.substring(0, lineLength);
}
textList.add(lineStr);
//每次重新给text辅助覆盖,使得text的值越来越少,直到空为止
if (!TextUtils.isEmpty(text)) {
if (text.length() < lineLength) {
text = text.substring(0, text.length());
} else {
text = text.substring(lineLength, text.length());
}
} else {
break;
}
}
}
}
int width;
int height;
if (widthModle == MeasureSpec.EXACTLY) {
//如果是精确数据直接保存父类给予的精确空间,否则就返回字符串的宽度
width = widthSize;
} else {
if (isOneLines) {
//padding+文本的宽度就是整个子控件的宽度
width = getPaddingLeft() + textWidth + getPaddingRight();
} else {
//多行处理,返回父控件的宽度,即
width = widthSize;
}
}
if (heightModle == MeasureSpec.EXACTLY) {
//如果是精确数据直接保存父类给予的精确空间,否则就返回字符串的宽度
height = heightSize;
} else {
float textHeight = rect.height();
if(isOneLines){
height = (int)(getPaddingTop() + textHeight + getPaddingBottom());
}else {
//多行根据行高设置
height=(int)(getPaddingTop() + textHeight*lineNum + getPaddingBottom());
}
}
setMeasuredDimension(width, height);
}
代码量不大,只是处理一些细节,首先是判断字符串是否用一行就可以展示,如果不够就去计算需要几行,平均每行要几个字符串,计算出肌肤穿的个数,从总出的字符串里面截取出来放在list中,最后有?Ondraw去绘制出来即可。这里要注意一下,当计算到行数与每一行字符串的个数时有可能产生小数,当计算行有小数是说明除不清,还需要一行,需要多加一行;计算每一行所需字符串时,有小数说明比如字符串总长度56,有四行。57/4=14.25,int默认是14,将会导致部分字符串取不到丢失,判断一下自动多加一个字符就可以了。
最后的保存的控件高度就是top+行数X行高+(行数-1)*X行距+bottom就可以啦,宽度就是leftpading+行宽+rightPading就可以啦。
总结:
1.自定义属性
2.重写构造方法
3.重写onDraw
4.重写onMeasure
知道了这些知识,对于该控件的还有许多可以扩展的:
简单的扩展有文本显示方向,靠左靠右,居中,控件底部显示,甚至竖直方向的显示都是可以实现的。只要你理解了,就可以举一反三的操作,展示。
当然这个只是简单的自定义view,学会这些还不足以满足开发。后面我将会边学边写,将自定义一些比较复杂的view来不断地提高自己。
结果图:
GitHub:https://github.com/AndrewGeorge/customviewdemo
参考资料:
https://developer.android.com/guide/topics/graphics/2d-graphics.html
http://blog.csdn.net/xmxkf/article/details/51454685
http://blog.csdn.net/yuzhouxiang/article/details/6958017
http://blog.csdn.net/mybeta/article/details/39993449