View工作原理笔记
来源:互联网 发布:电子教室软件哪个好 编辑:程序博客网 时间:2024/06/07 03:13
1、导论
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从消息队列中取出原始消息并进行派发,过程如下:
- 应用程序添加窗口时,会调用WmS中Session对象的addWindow()方法来请求创建一个窗口,WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitor中,然后通过InputManager把该信息储存到NativeInputManager中。
- 当InputDispathcer收到用户消息后,会先根据NativeInputManager中储存的所有窗口信息判断当前活动窗口是什么,该用户消息应由哪一个窗口来处理。
- 假若消息是触摸消息,则直接通过管道传输到客户窗口。
- 假若消息是按键消息,则依次传给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处理。有几点需要注意一下:
- 一个事件只能被一个View拦截消耗
- 一个View决定拦截,那么这一个事件序列都只能由它来处理
- 一个View一旦开始处理事件,如果他不消耗DOWN事件(说明出问题了!)那么同一事件序列的其他事件都不会再交给他来处理(上级不再相信你的处理能力
- 如果View不消耗DOWN以外的事件,这个点击事件会被交给Activity处理,View继续处理之后的事情
事情 - 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的绘制过程会在下片文章中讲到。
- View工作原理笔记
- scroll view 工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View工作原理
- View的工作原理
- View的工作原理
- View 的工作原理
- View的工作原理
- view的工作原理
- View的工作原理
- View的工作原理
- 浅析View工作原理
- View的工作原理
- View的工作原理
- View工作原理
- View 的工作原理
- maven集成tomcat插件发布web项目
- Lucene 6.2.1入门教程(二) 添加索引时新API与旧API的一些不同之处
- Vector
- POJ 1125-Stockbroker Grapevine(最短路-含孤立点)
- Day_6.
- View工作原理笔记
- gulp-autoprefixer
- Java反射机制
- 初来乍到
- Fresco图片加载框架学习和那些坑
- Linux-ubuntu 日记( 2 )- 安装Java
- 算法与数据结构--插入排序(InsertSort)
- 第七章 课后简答答案
- 用tensorflow画ROC曲线