自定义View的内容很多,原本只是想写一篇博客,现在觉得我需要新建一个自定义View的文集了,这一篇主要是讲什么是自定义View,以及自定义View中的自定义属性,其中的几个方法。接下来几篇会写几个自定义控件。此文主要参考:
http://blog.csdn.net/xmxkf/article/details/51490283
注意:
1.在ListView中,条目中如果有Button之类带点击效果的控件,那么必须要处理一下,不然它会抢走ListView的焦点,使ListView的ItemOnclick事件不生效。解决方法:在条目布局根节点声明一个属性:descendantFocusability,指的是:该条目内部子控件获取焦点的方式
它可以指定三个值,分别是:
1)afterDescendants 在条目获取之后,子控件才获取焦点
2)beforeDescendants 在条目获取焦点之前,子控件获取焦点
3)blocksDescendants 以区块的方式获取焦点,只有在点击到子控件所在的区块,子控件才会获取焦点。
2.PagerAdapter中需要子类重写的4个方法:
3.自定义控件的绘制是在界面打开之后的onCreate()方法之后绘制。
4.在自定义控件中调用系统方法:invalidate() 方法,会调用onDraw方法,使整个控件重新绘制,界面更新。
5.在自定义View中获取上下文,一般使用getContext()
自定义View分为三种:
1.组合已有控件实现自定义控件
2.完全自定义控件
3.继承已有控件,扩展其功能
1.组合已有控件实现自定义控件
旋转菜单效果图:
点击home键和menu键分别让外层的相对布局转入和转出。
点击menu键,为第三层布局添加动画。第三层布局显示,将第三层转出去,第三层布局隐藏,转进来。使用补间动画
1.转出动画:
2.转入动画:
1.旋转中心点:
相对于自己旋转,控件的长宽在坐标轴上均为1,中心点如上图红色点所示,点坐标是(0.5f,1f)
2.逆时针旋转,角度递减。顺时针旋转,角度递增。
3.点击home键,旋转第二层,点击menu键,旋转第三层。
在旋转时需要判断是转入,还是转出。如果布局显示,则转出,布局隐藏则转入。
4.点击home键时需要判断,第三层是否显示在屏幕上,如果第三层显示,先把第三层转出去。
这种情况,第二层布局需要添加延时执行动画,否则第二层第三层布局会同时转出。设置延时执行动画代码:
raOutAnimation.setStartOffset(100);//设置动画启动延时
在第二层旋转动画前加一个判断:如下:
5.设置动画执行延时,是否延时,要看外面那一级的菜单是否显示。设置一个变量,如果外面那层显示,将变量添加200毫秒
如果三级菜单显示,则delay+=200;如果三级菜单没显示,delay仍为0.
6.为了避免动画重复执行,设置一个变量,动画本身添加监听,动画开始执行时,将变量值++,动画执行结束,变量值- -,执行动画之前判断,如果变量值>0.则return;
7.手机键盘menu键,点击之后,三层布局全部执行转入转出动画。重写onKeyDown方法。
8.bug修复:
补间动画缺点,虽然布局转出去了,但是其实控件仍然在原来的位置,点击menu键原来的位置,第三层仍然会执行动画。
解决方法:转出动画时把按钮的点击事件屏蔽掉,转入动画再解除屏蔽。
2.完全自定义控件:继承自View
绘制完全自定义控件步骤:
1.继承View,覆盖构造方法
2.自定义属性
3.重写onMeasure方法测量宽高
4.重写onDraw方法绘制控件
1.继承View,覆盖构造方法
因为View类不具有无参的构造函数,因此,自定义View需要重写其构造方法,一般重写三个构造方法,但此处,把第四个构造方法也详细的记录一下:
1)带一个参数的构造方法:从代码创建时走此构造方法,用于代码创建。如:newTextView(mContext);
源码中的解释:Simple constructor to use when creating a view from code.
2) 带两个参数的构造方法:在xml中使用时走此构造方法,可用于指定自定义属性。
源码中的解释:Constructor that is called when inflating a view from XML. This is called
when a view is being constructed from an XML file, supplying attributes
that were specified in the XML file. This version uses a default style of
0, so the only attribute values applied are those in the Context's Theme
and the given AttributeSet.
如:这样几行代码,只在带两个参数的构造方法中打印一段话,并且将其attrs也打印出来看看
public SwitchButtonView(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
Log.i("带两个参数的构造方法", "SwitchButtonView: ");
for(int i=0;i<attrs.getAttributeCount();i++){
Log.d("带两个参数的构造方法attrs", attrs.getAttributeName(i)+" : "+attrs.getAttributeValue(i));
}
}
可以看到打印的结果:
可以看到,当我们直接在xml文件中使用,在类中绑定时,走的时带两个参数的构造方法,打印出的attrs,正是我们指定的属性,因此,在这个方法中,可以获取用户输入的自定义属性的值。
注意:如果xml中指定了样式,走的仍然是这个构造方法。也就是说,系统默认只会调用Custom View的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数).
3)带三个和四个参数的构造方法:通常是在第二个构造函数中调用,一般用来获取用户的自定义属性。
获取自定义属性的代码:
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);
String attr1 = ta.getString(R.styleable.MyCustomView_custom_attr1);
String attr2 = ta.getString(R.styleable.MyCustomView_custom_attr2);
String attr3 = ta.getString(R.styleable.MyCustomView_custom_attr3);
String attr4 = ta.getString(R.styleable.MyCustomView_custom_attr4);
Log.e("customview", "attr1=" + attr1);
Log.e("customview", "attr2=" + attr2); Log.e("customview", "attr3=" + attr3); Log.e("customview", "attr4=" + attr4);
ta.recycle();
}
使用TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);这句代码获取自定义属性,通过对源码的追踪我们发现:
最终调用了Theme中的obtainStyledAttributes带有4个参数的构造方法:
1.AttributeSet set: 属性值的集合.
2.int[] attrs: 我们自定义属性集合在R类中生成的int型数组.这个数组中包含了自定义属性的资源ID.
3.int defStyleAttr:
这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defStyleAttr中查找默认值.
4.int defStyleRes: 这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用.
由于一个属性可以在很多地方对其进行赋值,包括: XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:
属性赋值优先级次序表:
在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值
2.自定义属性
当我们自定义一个View时,可以通过自定义属性来更改View的一些如字体大小,背景图片等属性。那么自定义属性怎么用的呢?
1.首先,在attrs.xml文件中定义一个resource,其中可以填写任何我们想要设置的属性,format的意思是该属性的取值是什么类型(支持的类型有string,color,demension,integer,enum,reference,float,boolean,fraction,flag) 如图:
这是我在自定义toolbar中定义的自定义属性,其中rightButtonIcon代表的是右侧按钮图标,类型是reference,意思是引用类型。
format有11中类型,其中无法从字面直接获取其意思的几个详细记录一下:
reference:参考某一资源ID,如图片等的设置
dimension:尺寸值 如设置宽高
fraction:百分数
enum:枚举值 如线性布局中设置方向
flag:位或运算
注意,在xml文件中使用自定义属性是不要忘记命名空间。
xmlns:openxu="http://schemas.android.com/apk/res-auto"
2.在构造方法中获取用户输入的自定义属性。并在CustomView.java中编写相关方法,用来更改控件的属性。如同我们上面所讲的,在带有三个参数的构造方法中获取用户输入的自定义属性。如下图
上图以右侧按钮图标为例,在View中编写setRightButtonIcon()方法。setRightButtonIcon()方法是用来把用户输入的自定义属性设置到右侧按钮上的。
这样就完成了给控件设置自定义属性。
3.重写onMeasure方法测量宽高
关于View在官方文档中的解释:
View这个类代表用户界面组件的基本构建块。View在屏幕上占据一个矩形区域,并负责绘制和事件处理。View是用于创建交互式用户界面组件(按钮、文本等)的基础类。它的子类ViewGroup是所有布局的父类,它是一个可以包含其他view或者viewGroup并定义它们的布局属性的看不见的容器。 实现一个自定义View,你通常会覆盖一些framework层在所有view上调用的标准方法。你不需要重写所有这些方法。事实上,你可以只是重写onDraw(android.graphics.Canvas)。
Android界面的绘制流程:
View:重写以下方法
onMeasure()(该方法用于指定自己的宽高)---->onDraw()(该方法用于绘制自己的内容)
ViewGroup:重写以下方法
onMeasure()(该方法用于指定自己的宽高,子view的宽高)---->onLayout()(摆放所有的子View)----->onDraw()(绘制内容)
以上的这些方法,均在Activity或者Fragment、View等使用该控件的类中的onResume()方法之后执行。
好了,一个一个来解决:
onMeasure()方法:测量,也就是控制View的大小
测量:我们在写onMeasure方法时,通常会这么写:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
用来获取宽高,那么MeasureSpec究竟是什么呢?跟踪一下源码,发现它是View中的一个静态内部类,是由尺寸和模式组合而成的一个值,用来描述父控件对子控件尺寸的约束,看看他的部分源码,一共有三种模式,然后提供了合成和分解的方法:
可以看到,其中有三种约束,UNSPECIFIED EXACTLY AT_MOST
当控件宽高设置为match_parent或者是具体宽高值的时候,模式为EXACTILY。
当控件宽高设置为warp_content时,模式为AT_MOST。
那么举个例子,来重写onMeasure方法:
http://blog.csdn.net/xmxkf/article/details/51490283
onMeasure()方法中调用了setMeasuredDimension(宽,高)方法,该方法时用来设置自定义控件的宽高,
完全自定义控件例子:自定义开关
1.绘制界面内容
2.响应触摸事件
3.接口监听
1.绘制界面内容
1)定义ToggleView继承View,重写View的三个构造方法
2).绘制界面内容,界面由两张图片组成,前景和背景。如下图:
在ToggleView类中,设置三个方法:
1.setToggleBackground(int background):设置开关背景
toggleBackground = BitmapFactory.decodeResource(getResources(), background);把用户传入的background转化成bitmap,通过onDraw方法画到控件上
2.setToggleForeground(int foreground):设置开关前景
toggleForeground = BitmapFactory.decodeResource(getResources(), foreground);同上
3.setToggleStatus(Boolean open):设置开关状态。通过用户输入的boolean值设置开关状态
3).重写onMeasure方法,设置控件的宽高
设置控件宽高,与背景图片一样宽、高:setMeasuredDimension(toggleBackground.getWidth(), toggleBackground.getHeight());
4).重写onDraw方法,把图片绘制到控件上,绘制的内容都会显示到控件上
//1.绘制背景
canvas.drawBitmap(toggleBackground,0,0,paint);//中间两个参数是距离控件原点(左上角坐标)的x、y轴距离
//2.根据开关状态绘制前景
if(isopen){
//开
//获取前景移动距离
int i =toggleBackground.getWidth() -toggleForeground.getWidth();
canvas.drawBitmap(toggleForeground,i,0,paint);
}else{
//关
canvas.drawBitmap(toggleForeground,0,0,paint);
}
控件内容绘制完成
2.响应触摸事件
重写View的onTouchEvent()方法:返回值改为true,控件才会消费用户的点击事件。
在触摸事件中,获取用户手指的X坐标,通过更新前景图标左上角的X坐标来更新控件。在onTouchEvent调用invalidate()方法,每次触摸都会重新绘制。在手指抬起时,根据前景图的坐标判断开关是什么状态。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//按下
/**
* 按下时,将isTouchMode改为true,更改currentX
*/
currentX = event.getX();
isTouchMode =true;
break;
case MotionEvent.ACTION_MOVE:
//移动
currentX = event.getX();
/**
* 移动时,更改currentX,通过调用invalidate()重绘界面
*/
break;
case MotionEvent.ACTION_UP:
//抬起
currentX = event.getX();
/**
* 抬起时,将isTouchMode设置为false
*/
boolean state = false;
if(currentX < center){
//在中间值左边 关
state = false;
}else if(currentX > center){
state = true;}
isopen = state;//把state的值赋值给开关状态
isTouchMode =false;
break;
}
invalidate();//调用该方法,每次触摸时都重新绘制控件
return true;//必须更改为true,控件才能消费掉用户的触摸事件
}
在onDraw()方法中绘制界面:如果是触摸模式,根据触摸坐标来绘制界面;否则,根据开关状态绘制界面。设置左右边界,不允许前景图片超过边界,
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(toggleBackground,0,0,paint);
//根据用户触摸坐标来绘制画面
if(isTouchMode){
currentX =currentX -toggleForeground.getWidth()/2.0f;//移动的坐标需要比手指点下的坐标左移半个前景图片大小,这样看起来就是点击在开关中间
//容错处理:设置左右边界
float maxLeft =toggleBackground.getWidth() -toggleForeground.getWidth();//开关能移到右侧的最大值
if(currentX<0){
currentX =0;
}else if(currentX>maxLeft){
currentX = maxLeft;
}
canvas.drawBitmap(toggleForeground,currentX,0,paint);
}else{
//根据开关状态绘制前景
if(isopen){
//开
//获取前景移动距离
int i =toggleBackground.getWidth() -toggleForeground.getWidth();
canvas.drawBitmap(toggleForeground,i,0,paint);
}else{
//关
canvas.drawBitmap(toggleForeground,0,0,paint);
}}}
3.接口监听
当用户操作自定义控件时,自定义控件内部需要通知外部(界面,程序)我的状态改变了,并把状态的boolean变量传出去。
// 1. 声明接口对象
public interface OnSwitchStateUpdateListener{
// 状态回调, 把当前状态传出去
void onStateUpdate(boolean state);
}
// 2. 添加设置接口对象的方法, 外部进行调用
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
// 3. 在合适的位置.执行接口的方法
onSwitchStateUpdateListener.onStateUpdate(state);
// 4. 界面/外部, 收到事件.
sbv.setOnSwitchStatusChangeListener(new SwitchButtonView.OnSwitchStatusChangeListener(){
@Override
public void onStatusChange(boolean status) {
Toast.makeText(mContext,"开关状态为"+status,Toast.LENGTH_SHORT).show();
}
});
手指抬起时,如果state的值改变了,那么说明开关状态改变了。因为state的值最后是赋值给了isopen,在赋值之前判断,如果两者不同,就说明开关状态改变,那么在此时执行3中的方法
if(state!=isopen&&onSwitchStatusChangeListener!=null){
onSwitchStatusChangeListener.onStatusChange(state);//这句代码执行时,外部(界面上该控件的该接口的监听被调用)
}