想来说说基础版自定义View的步骤:
实现自定义View的属性设置,需要:
在values目录下建立attrs.xml文件,添加属性内容
在布局文件中添加新的命名空间xmlns,然后可以使用命名空间给自定义的空间设置属性
设置完属性之后,当然还要对其进行处理。在自定义View类中的构造方法中进行处理
首先贴一段attrs的代码:
<resources>
<attr name="myTextWord" format="string" />
<attr name="myTextColor" format="color" />
<attr name="myTextSize" format="dimension" />
<declare-styleable name="MyView">
<attr name="myTextWord" />
<attr name="myTextColor" />
<attr name="myTextSize" />
</declare-styleable>
</resources>
接着是命名空间的事,这个我被eclipse坑了好久,要是studio就没有这个问题了,所以最后说,最后再写。所以我们来看一看自定
义View的代码,我就不多介绍了,因为我的注释已经做的真的很详细了,再不懂真没办法了。多的不说,贴代码:
public class MyView extends View
{ //TextView的文字设定
private String textWord;
//TextView的颜色设定
private int textColor;
//TextView的字体大小设定
private int textSize;
/**
* Paint即画笔,画笔主要保存了颜色,样式等绘制信息
* 画笔对象有很多设置方法,大体上可以分为两类。
* 一类与图形绘制相关
* 一类与文本绘制相关
*/
private Paint mPaint;
/**
* Rect类主要用于表示坐标系中的一块矩形区域,并可以对其做一些简单操作
* 这块矩形区域,需要用左上右下两个坐标点表示(left,top,right,bottom)
* 你也可以获取一个Rect实例的Width和Height
* 需要注意的一点是:
* 例如:Rect r=new Rect(100,50,300,500);
* 其实(300,500)这个点是不在矩阵区域内的,但是如果自己调用android自己的函数就没问题,因为android内部计算方式是统一的
*/
private Rect mBound;
/**
* 自定义view时肯定要覆写构造方法的,目前构造参数在5.0已经达到了四个,平时我们一般使用的其实两个就够了。
* 第一个参数属于程序内实例化采用
* 在java代码创建视图时调用,如果由xml填充的视图就不会使用这个构造
* 第二个参数属于layout文件实例化,会将xml中的参数通过attributeSet传入到view中
* 在xml创建但没有指定style的时候调用
* 第三个参数属于style信息,也会从xml中带入
* 在xml创建,并指定了style的时候调用
* 这里的调用的虽然是第三个构造,但是defStyle是系统默认的数值,并没有由外部传入任何数据
*/
public MyView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public MyView(Context context) {
this(context,null);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
/**
* TypedArray是一个数组,用来记录attributeSet的值,也是android提供给我们的一个工具类,
* 所以这个类是可以用,也可以不用的,不用的话可以用一个数组存放attributeSet中的参数属性值,不过写起来很麻烦
* TypedArray存放恢复obtainStyledAttributes(AttributeSet, int[], int, int)
* 或 obtainAttributes(AttributeSet, int[])值的一个数组容器,
* 当操作完成以后,一定要调用recycle()方法。
* 用于检索的索引值在这个结构对应的位置给obtainStyledAttributes属性。
* 使用这个类的时候,先要在valuse文件夹下创建:atts.xml文件
*/
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyle, 0);
//返回我们所使用属性的个数
int n = a.getIndexCount();
for (int i = 0; i < n; i++)
{ //返回属性id,自定义属性初始化和设置默认值
int attr = a.getIndex(i);
switch (attr)
{
case R.styleable.MyView_myTextWord:
textWord = a.getString(attr);
break;
case R.styleable.MyView_myTextColor:
// 默认颜色设置为黑色
textColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.MyView_myTextSize:
// 默认设置为16sp,TypeValue也可以把sp转化为px
textSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
a.recycle();
/**
* 获得绘制文本的宽和高
*/
mPaint = new Paint();
//绘本字体大小
mPaint.setTextSize(textSize);
// mPaint.setColor(mTitleTextColor);绘本字体颜色
//view 的绘图范围
mBound = new Rect();
/**
* 这里的代码需要倒着看,倒着写就好理解一些
* 首先我们需要得到文本Text的视图大小(不是TextView视图大小)
* 需要传入的参数和实例有画笔mPaint,有mBound矩阵实例,有TextWord文字
* 并且需要规定好文本字体的大小,应为他会影响视图范围,所以上面代码中
* 设画笔颜色的代码可以注销,但设置文本字体的代码不能少。
*/
mPaint.getTextBounds(textWord, 0, textWord.length(), mBound);
//这里是添加点击事件,动态的更改文本内容,程序员都知道,不用看,主要需呀看得就一句postInvalidate();
this.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{ //设置文本呢
textWord = setMyTextWord();
/**
* android提供的更新ui界面的方法有两个
* 一个是Invalidate();
* 一个是postInvalidate();
* 其中前者是在UI线程自身中使用,而后者在非UI线程中使用。
* 很显然两个用法是不同的,我们分开说:
* Android提供了Invalidate方法实现界面刷新,但是Invalidate不能直接在线程中调用,
* 因为他是违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中调用。
* 利用invalidate()刷新界面实例化一个Handler对象,
* 并重写handleMessage方法调用invalidate()实现界面刷新
* 而在线程中通过sendMessage发送界面更新消息。
*
* 使用postInvalidate()刷新界面使用postInvalidate则比较简单,
* 不需要handler,直接在线程中调用postInvalidate即可。
* 这里我们就直接调用了postInvalidate();这个方法,实现text文字的刷新了。
*
* Android中View的绘制过程当Activity获得焦点时,它将被要求绘制自己的布局,
* Android framework将会处理绘制过程,Activity只需提供它的布局的根节点。
* 绘制过程从布局的根节点开始,从根节点开始测量和绘制整个layout tree。
* 每一个ViewGroup 负责要求它的每一个孩子被绘制,每一个View负责绘制自己。
* 因为整个树是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。
*/
postInvalidate();
}
});
}
//略过,可以不看你
public String setMyTextWord(){
Set<Integer>set=new HashSet<Integer>();
Random r=new Random();
while(set.size()<4){
int num=r.nextInt(10);
set.add(num);
}
StringBuffer sb=new StringBuffer();
for(Integer i:set){
sb.append(i);
}
return sb.toString();
}
/**
* onMeasure方法,获取视图view的宽高尺寸
* widthMeasureSpec宽的详细测量值
* heightMeasureSpec高的详细测量值
* 表示的是view视图想要的大小宽高,但不一定就是最终实际的大小宽高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
/**
* MeasureSpec这是一个封装在View里面的内部类
* 其内部处理的是measurespec详细测量中,size大小,model模式之间呢的关系
* MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求
* MeasureSpec由size和mode组成。
* 三种Mode:
* 1.UNSPECIFIED
* 父不没有对子施加任何约束,子可以是任意大小(也就是未指定)
* (UNSPECIFIED在源码中的处理和EXACTLY一样。当View的宽高值设置为0的时候或者没有设置宽高时,模式为UNSPECIFIED
* 2.EXACTLY
* 父决定子的确切大小,子被限定在给定的边界里,忽略本身想要的大小。
* (当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
* 3.AT_MOST
* 子最大可以达到的指定大小
* (当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
*
* MeasureSpecs使用了二进制去减少对象的分配。
*/
int width = 0;
int height = 0;
/**
* 设置宽度
*/
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
switch (specMode)
{
case MeasureSpec.EXACTLY:// 明确指定了
width = getPaddingLeft() + getPaddingRight() + specSize;
break;
case MeasureSpec.AT_MOST:// 一般为WARP_CONTENT
width = getPaddingLeft() + getPaddingRight() + mBound.width();
break;
}
/**
* 设置高度
*/
specMode = MeasureSpec.getMode(heightMeasureSpec);
specSize = MeasureSpec.getSize(heightMeasureSpec);
switch (specMode)
{
case MeasureSpec.EXACTLY:// 明确指定了
height = getPaddingTop() + getPaddingBottom() + specSize;
break;
case MeasureSpec.AT_MOST:// 一般为WARP_CONTENT
height = getPaddingTop() + getPaddingBottom() + mBound.height();
break;
}
/**
* 在onMeasure中只调用了setMeasuredDimension()方法,接受两个参数,
* 这两个参数是通过getDefaultSize方法得到的
* getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
* 这里就是获取最小宽度作为默认值,然后再根据具体的测量值和选用的模式来得到widthMeasureSpec。
* heightMeasureSpec同理。
* 之后将widthMeasureSpec,heightMeasureSpec传入setMeasuredDimension()方法。
*/
setMeasuredDimension(width, height);
}
/**
* 在这里就对canvas解释一下
* Canvas类(android.graphics.Canvas)。
* Canvas类就是表示一块画布,你可以在上面画你想画的东西。
* 当然,你还可以设置画布的属性,如画布的颜色/尺寸等。
* canvas提供的方法有很多,这里就不细说了,自己百度,但是就我被坑惨了的一点来谈谈。
* canvas.drawText方法,这个方法中第二,第三两个参数
* 第二的参数标示的是视图左边的距离
* 第三个参数标示的是基准线baseline的设置
* 它的baseline 指的是这个UI控件中文字Text的baseline 到UI控件顶端的偏移值
* 可以理解为text下面那条看不见的线
* 如果baseline的设置和左距的设置一样,很有可能你找不到你的text,只能看见背景图
* 两种设置方式可供参考吧,一种如下
* 另一种很取巧,不过试过,能用,getHeight()/2+mBound.Height()/2+6;
*/
@Override
protected void onDraw(Canvas canvas) {
//画笔填充背景颜色
mPaint.setColor(Color.GRAY);
//画布画背景区域
canvas.drawRect(0, 0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
//画笔填充文字颜色
mPaint.setColor(textColor);
//画布画文字
canvas.drawText(textWord, getWidth()/2-mBound.width()/2, (getHeight()-mBound.height())/2+mBound.height()-6, mPaint);
}
}
最后来谈一谈命名空间,首先让我们来看看命名空间是什么鬼,有什么作用。
xmlns:android="http://schemas.android.com/apk/res/android"
声明xml命名空间。xmlns意思为“xml namespace”.冒号后面是给这个引用起的别名。
schemas是xml文档的两种约束文件其中的一种,规定了xml中有哪些元素(标签)、元素有哪些属性及各元素的关系,当然从面向 对象的角度理解schemas文
件可以认为它是被约束的xml文档的“类”或称为“模板”。
早期或简单的xml用的是另一种约束,称为DTD,这东西大家天天都见到。html/xhtml中都存在(早期的html可能没有),如"<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"。
现在大部分xml文档的约束都换成schema了,原因是schema本身也是xml,二schema扩展性强。
首先说说在studio中添加命名空间,就是添加xmlns:myview="http://schemas.android.com/apk/res-auto"就ok了,myview 就是你的可用命名空间了。
但是在eclipse中,你使用xmlns:myview="http://schemas.android.com/apk/res/包名"你这么写是会报警的,警告是unused namespace,并且你在自定义view中调用自己定义的属性,使用自己命名空间myview 是调不出任何东西的,这个时候不用慌,要相信自己,自己的命名空间,自己的属性全部用敲的,不要用提示,也提示不出鬼东西,写完保存,然后你会发现属性能用,这就ok了。
都是基础知识,大家不要嫌弃。
注释一句,代码是鸿洋老师的代码,想看原版的,给个链接:
http://blog.csdn.net/lmj623565791/article/details/24252901/