Android-控件架构
Android的控件是Android的血与肉;本篇会讲解Android的View架构,view的测量与绘制,自定义view和控件的事件分发拦截机制
控件架构
1.View的测量
在OnMeasure()方法中进行,Android提供了一个短小但强大的类MeasureSpec(),通过它来帮助测量View。MeasureSpec是一个32位的Int值,高2位是测量的模式,低30位是测量的大小,使用这种位运算是为了提高测量的效率。
测量模式
- EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 - AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 - UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
View默认的onMeasure只支持EXACTLY模式
直接继承View,就不能用wrap_content
属性;想用就必须重写onMeasure
方法
通过MeasureSpec
这个类,我们就获取到了View的测量模式和绘制的大小,有了这些信息我们就可以控制View最后显示的大小了
首先要知道重写OnMeasure
方法还是调用的父类的方法,而父类又是调用setMeasuredDimension
方法
//继承View,重写OnMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//点进上面OnMeasure中,View类的OnMeasure方法就是又调用setMeasuredDimension方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
例子,自定义View,继承View,实现使用wrap_content
属性:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//由于我们直接调用setMeasuredDimension方法,就不需要调用父类的方法了,可以注释掉
setMeasuredDimension(measrueWidth(widthMeasureSpec), measrueHeight(heightMeasureSpec));
}
//测量宽度,先判断模式为EXACTLY
private int measrueWidth(int widthMeasureSpec) {
int result = 0;
//获取模式是EXACTLY还是其他模式
int specMode = MeasureSpec.getMode(widthMeasureSpec);
//获取控件大小
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//如果不是EXACTLY模式则需要设置一个默认大小
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
//取其中小的一个
result = Math.min(result, specSize);
}
}
return result;
}
//测量高度,类似宽度,如上
private int measrueHeight(int heightMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
}
2.View的绘制
重写View的OnDraw()
方法,介绍一下Canvas
,就是一个画布,有了它就可以绘图了。
Canvas mCanvas = new Canvas(bitmap);
传递一个bitmap
是为了记录保存下绘图的像素信息,有了这些信息才能进行绘制,两者紧紧联系在一起的。也就是说传的bitmap
值不同,画的就不同;传递的是同一个bitmap
,画的也是相同的。
3.ViewGroup的测量和绘制
- 测量: 这个是在面试的时候被坑过了的,如果属性是
wrap_content
主要就是调用OnLayout
方法遍历子View,调用子View的Measure
方法;不是则设定指定值。
如果是自定义ViewGroup的话,跟View一样;想用wrap_content
属性就必须重写OnMeasure
方法。 - 绘制: 使用
dispatchDraw()
方法来调用子View的OnDraw()
方法 进行绘制。或者如果设置了背景颜色或图片的情况下也会调用自己的OnDraw()
方法
4.自定义View
大家最喜欢的也是最重点内容终于来了,初学时一直觉得感觉很深奥的样子,其实也很简单的啦
在View中比较重要的回调方法:
-
OnfinishInflate
:从XML加载组件后回调 -
OnSizeChanged
:组件大小改变时回调 -
OnMeasure
:回调该方法来进行测量 -
OnLayout
:回调该方法确定显示的位置 -
OnTouchEvent
:监听触摸事件回调
一般来说可以对现有控件进行拓展:例如继承TextView
来实现具有外框的TextView
@Override
protected void onDraw(Canvas canvas) {
//回调之前实现自己的逻辑,即在绘制文本之前
super.onDraw(canvas);
//回调之后实现自己的逻辑,即在绘制文本之后
}
自定义属性
在res中的values文件夹下,创建名为attrs的Values resouce File
的文件,<declare-styleable name="TopBar">
用来确定是作用于那个View的,下面的attr name
就是每个属性的名称。
这里说明一下属性:
- diemnsion: 一般为文字大小
- color:颜色
- String:字符串
- reference:引用,一般为background
- 还可以两者结合,用|
分隔开
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TopBar">
<attr name="top_bar_title" format="string"/>
<attr name="title_text_size" format="dimension"/>
<attr name="title_text_color" format="color"/>
<attr name="left_text_color" format="color"/>
<attr name="left_btn_back" format="reference|color"/>
<attr name="left_text" format="string"/>
<attr name="right_text_color" format="color"/>
<attr name="right_btn_back" format="reference|color"/>
<attr name="right_text" format="string"/>
</declare-styleable>
</resources>
何如在代码中获取定义好的属性呢?
private TypedArray mTypedArray;
public void initTypedArray(Context context){
//指定是从哪读取的属性
mTypedArray = context.obtainStyledAttributes(R.styleable.TopBar);
mTxtTile = mTypedArray.getString(R.styleable.TopBar_top_bar_title);
mTxtLeft = mTypedArray.getString(R.styleable.TopBar_left_text);
mTxtRight = mTypedArray.getString(R.styleable.TopBar_right_text);
mTitleColor = mTypedArray.getColor(R.styleable.TopBar_title_text_color, Color.WHITE);
mLeftColor = mTypedArray.getColor(R.styleable.TopBar_left_text_color,Color.WHITE);
mRightColor = mTypedArray.getColor(R.styleable.TopBar_right_text_color,Color.WHITE);
mLeftBackground = mTypedArray.getDrawable(R.styleable.TopBar_left_btn_back);
mRightBackground = mTypedArray.getDrawable(R.styleable.TopBar_right_btn_back);
mTextSize = mTypedArray.getDimension(R.styleable.TopBar_title_text_size,10);
//获取完值后一般都会调用recycle方法,来避免重新创建时候的错误
mTypedArray.recycle();
}
XML中能用吗?怎么用呢?
既然是自定义的控件属性,当然也可以在XML中使用
看到上图的topbar
属性了吗?都是自定义的属性,那么怎么才能这样定义呢?看看Android原本的控件如何定义的吧
这是使用自定义控件的根布局LinearLaout
,看其中的xmlns:android
就是系统的,而我们的就是xmlns:topbar
;xmlns
就是命名空间;这个名字可以随意更改,直接在根布局中改就可以了
组合控件
组合控件一般都是动态添加子View的,所以所有控件都是在Java代码中添加,与xml无关。这时控件大家都会定义,属性也会设置如setTextColor()
;但是Layout的位置呢?应该如下所示:
private ViewGroup.LayoutParams layoutParams;
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
//然后就可以用addView方法添加到组合控件中了
//例如定义了一个mLeftButton
addView(mLeftButton,layoutParams);
//查看addView源码
public void addView(View view, ViewGroup.LayoutParams params);
可以看到第四个方法有view和layoutParams的属性,就选这个
自定义点击事件接口
一般一个控件都会有点击事件吧,但是一个组合控件里面有多个控件,这时你就要自己写一个点击事件的接口让别人用了。
public interface OnTopBarClickListener {
void onLeftClick();
void onRightClick();
}
定义完后只是空的接口,需要有里面的点击响应:
OnTopBarClickListener mListener;
mLeftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//自行在外定义接口对象
mListener.onLeftClick();
}
});
最后暴露出一个方法让调用者注册接口回调
public void setOnTopBarClickListener(OnTopBarClickListener listener){
this.mListener = listener;
}
这样就ok了,是不是跟用系统的setOnTouchListener
一样用了?就是这么简单
事件拦截机制简单分析
首先要知道什么是触摸事件;比如说按下一个按钮,按下按钮是事件一,不小心滑动是时间二,抬起手是事件三;
Android为触摸事件封装了一个类——MotionEvent
;基本上所有与事件相关的操作都需要这个类
- 事件传递
dispatchTouchEvent
:父控件—>子控件
返回值:true拦截;false不拦截 - 事件处理
OnInterceptTouchEvent
:子控件—>父控件
返回值:true处理了;false给上级处理
初始情况下返回值都是false。
父控件想拦截事件就给OnInterceptTouchEvent
设为true
,这样就不会传递给子控件
子控件不想让父控件知道自己干了什么,就把OnInterceptTouchEvent
置为true
;这样就不会向父控件报告
是不是非常浅显易懂?这里就不过多阐述,只要知道是这样的就够了,深入的以后再说,或者可以看源码了解