View工作原理笔记

来源:互联网 发布:电子教室软件哪个好 编辑:程序博客网 时间:2024/06/07 03:13

1、导论

View系统定义了从用户输入消息到消息处理的全过程,而这个过程大致可以分为三个阶段,我们称之为消息处理前端、窗口管理系统、单独窗口中的View系统。
View系统全过程

  • 消息处理前段(JNI代码):指对消息的预处理,硬件传来的各种消息当然不适于操作系统直接处理,消息处理前端会把它们转化为易于处理的统一值。
  • 窗口管理系统(JNI代码+java代码):负责将消息分发到不同的窗口去。该系统中包含两个核心类,InputReader和InputDispatcher(这两个类都对应一个线程)。他们对应着一个生产者-消费者模式。前者获取消息处理前端放在EventQueue中的消息,后者负责向客户窗口分派信息。部分信息会先在WmS中做一定的处理。
  • 单独窗口中的View系统(java代码):View获取消息后,会按默认的逻辑来派发消息给各个子视图。该逻辑可以概括为委托模式。然后,如果派发来的消息不会引起界面变化,则其会被当作后台任务处理,如果会引起界面变化,则会触发界面重绘。界面重绘大概有三个步骤,measure-layout-draw。

2、用户消息类型

用户消息值消息处理前段把硬件物理消息转化为Framework内部定义的统一格式后的消息。用户消息类型分为三种,分别是按键消息,触摸消息,轨迹球消息。
而轨迹球消息已很少被使用,本文将不考虑。

2.1 按键消息
实现类:android.view.KeyEvent
常用API接口如下:

  • getAction():该函数返回按键动作。只有两个,UP和DOWN
  • getKeyCode():该函数返回按键的编码,它是Framework定义的
  • isShiftPressed()、isAltPressed:用于与getKeyCode结合来处理组合键。
  • getRepeat():该函数返回从按下后重复的次数。

2.2 触摸消息
实现类:android.view.MotionEvent
常用API接口如下:

  • getAction() :该函数返回消息动作。
  • getEventTime()、getDownTime():获取事件/按下事件发生的时间。
  • getPressure():获取用户点击的力量大小。其值可以大于1.
  • getSize():获取触摸面积大小,其值在0~1之间。(仅适用于电容屏)
  • getX(int index)、getY(int index):获取相对与View左上角的坐标。其中的index仅在多点触控时需要,值为点的序号,从0开始。
  • getRawX(),getRawY():获取绝对坐标。(相对于屏幕左上角)

3、窗口管理系统中的消息派发过程

窗口管理系统难点在于对与Android这种多进程系统来说,消息获取线程(系统进程)与用户线程(用户App进程)不在同一个进程之中,故需要用到IPC。而用户对View响应速度要求很高,Binder的延迟极大的影响了这一点。现在的Android系统是通过管道(Pipe)机制来实现窗口管理系统的IPC过程的。
Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存使得两个进程能够相互沟通。这里我们将不深入。
窗口管理系统的整个派发过程如下:
输入消息获取过程
首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入信息,并放入消息队列。InputDisPathcher从消息队列中取出原始消息并进行派发,过程如下:

  1. 应用程序添加窗口时,会调用WmS中Session对象的addWindow()方法来请求创建一个窗口,WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitor中,然后通过InputManager把该信息储存到NativeInputManager中。
  2. 当InputDispathcer收到用户消息后,会先根据NativeInputManager中储存的所有窗口信息判断当前活动窗口是什么,该用户消息应由哪一个窗口来处理。
  3. 假若消息是触摸消息,则直接通过管道传输到客户窗口。
  4. 假若消息是按键消息,则依次传给NativeInPutManager,InputManager,InputMonitor从而交给WmS做一定的处理并返还给InputDisPathcher,InputDisPathcher会把处理后的消息通过管道传给客户窗口。如果是系统按键消息(如Home键),则InputDisPathcher不会再把它传给客户窗口,因为系统按键消息不需要客户窗口来处理。

4、按键消息的派发过程

关于按键消息的在此过程的分发与触摸消息类似,故在此不做深入研究,只需记住最后会调用onKeyDown()与onKeyUp()即可。

5、触摸消息派发到View树

当窗口管理系统将消息传到该窗口的ViewRoot时,ViewRoot内部的mInputHandler的disPatchMotion()函数会发起一个DISPATCH_POINTER异步消息,消息的处理函数是deliverPointerEvent()。其具体过程如下:

  • 进行分辨率的转换,因为操作系统定义的分辨率与实际的分辨率可能存在区别 。
  • 将屏幕坐标转换成视图坐标,因为视图是没有边界的,其坐标是相对与视图左上角的,而我们获取的是相对于屏幕左上角的屏幕坐标,所以需要适当转换。
  • 调用mView.dispatchTouchEvent()将消息派发给根视图,根视图有两种,对于普通的含Activity的窗口,就是DectorView。而对于其他(如RemoteView)就是普通的一个ViewGroup。
  • 如果以上过程都没有消耗事件,则会执行屏幕偏移。这说明以上都没能成功处理该消息,则系统认为很可能是因为消息坐标在屏幕边缘而没能很好的处理,故会将该事件交由靠内部一定距离的view来处理,这个值在FrameWork中定义了,是一个常量。

经过以上几个过程,消息已经成功传入根视图,也就是传入了View树中了。

6、View树内的事件分发

这一部分有3个十分重要的方法,他们都是View自带的方法,分别是:

  • dispatchTouchEvent():用于进行事件的分发,如果事件传递给该View,则该方法一定会被调用。
  • onIterceptTouchEvent():判断是否拦截某个事件。
  • onTouchEvent():用来处理点击事件。

这三个类的关系可以很好的用如下伪代码描述。

public Boolean dispatchTouchEvent(MotionEvent ev){    Boolean consume = false;    if(onIterceptTouchEvent(ev)){        consume = onTouchEvent();    }else{        consume = child.dispatchTouchEvent();    }    return consume;}

当根视图收到用户消息的时候,就会开始对View树进行递归调用,对于View树上的每一个节点,会优先不消耗事件,而是把事件分发下去,委托子View去处理,而当一个事件直到最底层的View都没有被处理事,又会被逐层地返回给上层的View处理,可以简称为委托机制。至于设计成这样的原因,我认为这样有助于把事件交给最顶层的View去做处理,符合正常的用户体验,而当子View不能处理时,再把他传回父View,又确保了事件能够得到处理。当所有的View都无法处理时,事件会被传给Activity处理。有几点需要注意一下:

  1. 一个事件只能被一个View拦截消耗
  2. 一个View决定拦截,那么这一个事件序列都只能由它来处理
  3. 一个View一旦开始处理事件,如果他不消耗DOWN事件(说明出问题了!)那么同一事件序列的其他事件都不会再交给他来处理(上级不再相信你的处理能力
  4. 如果View不消耗DOWN以外的事件,这个点击事件会被交给Activity处理,View继续处理之后的事情
    事情
  5. ViewGroup默认不拦截任何事件

7、单个View的事件处理

至此,用户消息已经成功的传入的需要处理他的View中,这时会检查该View是否注册了onTouchListener,如果有,则消息交由其全权处理。否则,调用onTouchEvent()。接下来,我们会分析View默认的onTouchEvent()的处理逻辑,也就是View对事件的默认处理方法。

if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {                setPressed(false);            }            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return (((viewFlags & CLICKABLE) == CLICKABLE                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);        }        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }

首先,会判断该视图是否未被激活(disable),如果是,什么都不干并消耗该消息。然后处理消息代理TouchDelegate,这个东西比较有意思,他可以让View处理比自己还大的点击区域。所以,理所应当,当消息代理消耗了该事件,直接返回。
“`

            case MotionEvent.ACTION_UP:                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                    // take focus if we don't have it already and we should in                    // touch mode.                    boolean focusTaken = false;                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {                        focusTaken = requestFocus();                    }                    if (prepressed) {                        // The button is being released before we actually                        // showed it as pressed.  Make it show the pressed                        // state now (before scheduling the click) to ensure                        // the user sees it.                        setPressed(true, x, y);                   }                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                        // This is a tap, so remove the longpress check                        removeLongPressCallback();                        // Only perform take click actions if we were in the pressed state                        if (!focusTaken) {                            // Use a Runnable and post this rather than calling                            // performClick directly. This lets other visual state                            // of the view update before click actions start.                            if (mPerformClick == null) {                                mPerformClick = new PerformClick();                            }                            if (!post(mPerformClick)) {                                performClick();                            }                        }                    }                    if (mUnsetPressedState == null) {                        mUnsetPressedState = new UnsetPressedState();                    }                    if (prepressed) {                        postDelayed(mUnsetPressedState,                                ViewConfiguration.getPressedStateDuration());                    } else if (!post(mUnsetPressedState)) {                        // If the post failed, unpress right now                        mUnsetPressedState.run();                    }                    removeTapCallback();                }                mIgnoreNextUpEvent = false;                break;            case MotionEvent.ACTION_DOWN:                mHasPerformedLongPress = false;                if (performButtonActionOnTouchDown(event)) {                    break;                }                // Walk up the hierarchy to determine if we're inside a scrolling container.                boolean isInScrollingContainer = isInScrollingContainer();                // For views inside a scrolling container, delay the pressed feedback for                // a short period in case this is a scroll.                if (isInScrollingContainer) {                    mPrivateFlags |= PFLAG_PREPRESSED;                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPendingCheckForTap.x = event.getX();                    mPendingCheckForTap.y = event.getY();                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                } else {                    // Not inside a scrolling container, so show the feedback right away                    setPressed(true, x, y);                    checkForLongClick(0, x, y);                }                break;```

接下来这段代码比较长,它所做的,就是对事件进行分类处理。最终实现点击,长按等事件的判定。
首先,当Down事件发生时,首先判断是否是可滑动容器,如果是,检验是否发生了Tap(轻滑)。如果不是,则开启长按检验。
Tap检验及长按检验的本质都是通过View向MainLooper发送postDelay。停留一段时间后,如果消息还在looper中,则触发tap或长按事件。
然后当UP事件发生时,首先会调用perfromClick()来调用onClick()方法。并且把looper中还未被处理的消息去除(此时还未被处理说明未达到长按所需时间)。
还需要注意的一点是对于onLongClick(),OnTouchEvent()这些方法来说,返回值代表是否消耗该消息。如果消耗了,则比这个方法优先级低的方法都不会得到调用。
优先级:onTouchListener>onTouchEvent>onLongClickListener>OnClickListener
关于View通过Listener来扩展其功能也体现了一个设计模式和一个设计原理
设计原理:操作系统在调用我们,而不是我们在调用操作系统。
设计模式:策略模式。
关于View的绘制过程会在下片文章中讲到。

0 0