通常来说用户界面都是由Activity组成,Activity中关联了一个PhoneWindow创建,在这个窗口下管理了一个视图树。这颗视图树的顶级视图就是一个GroupView类型的DecorView,DecorView下就是各个ViewGroup,ViewGroup下是各个View。
View的位置由四个顶点决定,分别对应四个属性:top,left,right,bottom,通过这几个单词就很容易知道所代表的意思。但是,这些坐标都是相对于View的父容器来说的,是一种相对坐标。
当你手指接触屏幕后会产生一些列事件,主要有ACTION_DOWN,ACTION_MOVE,ACTION_UP,所以我们平时所知道的点击事件、滑动事件分别是DOWN->UP,DOWN->MOVE->MOVE….->UP。说到这里就必须要提到一个概念TouchSlop。TouchSlop是系统所能识别出的被认为是滑动的最小距离,当两次滑动之间的距离小于这个常量,系统就不认为你是在进行滑动。关于Android系统中的滑动事件我们另外再讲,这里主要讲自定义View控件和事件分发机制
虽然Android提供了很多强大的UI控件,但是依旧不能满足开发人员的需求。我们只能通过自定义View实现。自定义View也有几种实现类型,分别为继承View,继承现有控件(如ImageView),继承自ViewGroup实现布局类。比较主要的知识点就是测量与布局,View的绘制,触摸事件,动画等。
public class SimpleImageView extends View {
private Paint mPaint;
private Drawable drawable;
private int mWidth;
private int mHeight;
public SimpleImageView(Context context) {
super(context);
}
public SimpleImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initAttributeSet(attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
public SimpleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SimpleImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void initAttributeSet(AttributeSet attrs) {
if (attrs != null) {
TypedArray array = null;
try {
array = getContext().obtainStyledAttributes(attrs, R.styleable.SimpleImageView);
drawable = array.getDrawable(R.styleable.SimpleImageView_src);
measureDrawable();
} finally {
if (array != null) {
array.recycle();
}
}
}
}
private void measureDrawable() {
if (drawable == null) {
throw new RuntimeException("drawable非空");
}
mWidth = drawable.getIntrinsicWidth();
mHeight = drawable.getIntrinsicHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
if (drawable == null){
return;
}
canvas.drawBitmap(ImageUtils.drawableToBitmap(drawable),getLeft(),getTop(),mPaint);
}
}
我们首先创建了一个继承自View的SimpleImageView类,在含有构造函数我们会获取该控件的属性,并且进行初始化画笔和绘制图片。在vaule/attr.xml中我们定义了这个View的属性,便于后续使用,attr.xml文件如下:
<declare-styleable name="SimpleImageView">
<attr name="src" format="integer"/>
declare-styleable>
当我们在XML文件中使用该控件时,需要指定它的图片资源。当应用启动时会从这个xml布局中解析SimpleImageView的属性,例如宽、高。进入SimpleImageView的构造函数后会调用initAttrs函数进行初始化。在initAttrs函数中,我们会首先读取SimpleImageView的属性集TypedArray;再从该对象中读取SimpleImageView_src属性值,该属性是一个drawable的资源id,然后我们根据这个id从该TypedArray对象中获取到该id对应的Drawable,最后调用measureDrawable函数测量该图片的大小。
关于该View控件的宽高是这样的。我们在SimpleImageView中设置了两个字段mWidth,mHeight,分别表示该视图的宽高。在measureDrawable函数中,我们通过在xml文件中指定资源id对应的drawable得到图片的宽高,并且把它们作为SimpleImageView的宽高。然后在SimpleImageView被加载的时候,首先会调用onMeasure函数测量SimpleImageView的大小,然后再把图片绘制出来。
关于Canvas 和Paint的函数较多,但理解起来比较简单,我们不多讲。另外关于Scroller的使用我们单独讲。
我们上边讲了View的触摸事件,那么就会遇到View的另一大难题滑动冲突,解决办法的理论基础就是事件分发机制。
在介绍点击事件的传递规则之前,我们要明白这里分析的对象就是MotionEvent,即点击事件。当一个MotionEvent生成以后,系统需要把这个事件传递给一个具体的View,这个传递过程就是分发过程。点击事件的分发过程由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
用来进行事件分发。如果事件能够传递给当前View,那么此方法一定会调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的方法影响,表示是否消耗当前事件。
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个时间序列中,当前view无法再次接收到事件。其实,三个函数的关系可以用如下代码表示:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if (onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else {
consume = child.dispatchTouchEvent(event);
}
}
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截这个事件,这时当前事件就会传递给它的子元素,如此传递下去,知道事件最终被处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被调用。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,onTouchEvent方法将不调用。由此可见,给View设置的OnTouchListener,它的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可见,我们平时常用的OnClickListener,其优先级最低,即处于事件传递的最末端。
这里我们要提一点,事件的传递过程遵循如下顺序:Activity->Window-View.当View的onTouchEvent方法返回false时怎么办呢?这时,它的父容器的onTouchEvent方法被启用,以此类推。直至传递给Activity处理。
这里总结了一些结论,帮助大家理解事件的分发机制。
同一个事件序列是指从手指接触屏幕的那一刻算起,到手指离开屏幕的那一刻结束。也就是这个事件序列以down事件开始,中间含有不定数量的move事件,最终以up结束。
正常情况下,一个事件序列只能被一个View拦截且消耗