Anroid开发艺术探究
1.onStart 和 onResume、onPause和onStop从描述上来看差不多,对我们来说有什么实质的不同?
onStart 和 onStop是从Activity是否可见这个角度来回调的,而onResume和onPause是从Activity是否位于前台这个角度来回调的,除了这种区别,没有其他区别
2.假设当前Activity为A,如果这时用户打开一个新Activity B,那么 B 的onResume()和 A 的onPaus()哪个先执行呢?
启动Activity的请求会由Instrumentation来处理,然后它通过Binder向AMS发请求,AMS内部维护着一个ActivityStack并负责栈内的Activvity的状态,AMS通过
ActivityThread去同步Activity的状态从而完成生命周期方法的调用。在ActivityStack中的resumeTopActivityInnerLocked方法中,在新Acctivity启动之前,
栈顶的Activity需要先onPause()后,新Activity才能启动。最终在ActivityStackSupervisor中的realStartActivityLocked方法会调用scheduleLaunchActivity,
接着完成新Activity的onCreate、onStart、onResume的调用过程。因此,可以得到结论,是旧Activity先onPause,然后新Activity再启动。
3.Activity被销毁并重建后,我们去获取之前的存储字符串,接受的位置可以是onRestoreInstanceState 或者 onCreate 方法,两者区别是?
onRestoreInstanceState一旦被调用,其参数Bundle savedInstanceState 一定是有值的,我们不用额外的拍断是否为空;
但是onCreate不行,onCreate如果正常启动的话,其参数Bundle savedInstanceState为null, 所以必须要额外判断。
4.onSaveInstanceState和onRestoreInstanceState,在正常流程下会出发么?
系统只在Activity异常种植的时候才会调用onSaveInstanceState和onRestoreInstanceState来存储和恢复数据,其他情况不会出发这个过程。
5.比如目前任务栈S1中的情况为ABC,这个时候Activity D 以singleTask模式请求启动,其所需要的任务栈为 S2,由于S2和D的实例均不存在,所以系统会先创建
任务栈S2,然后在创建D的实例并将其入栈到S2.
6.TaskAffinity任务相关性。
这个参数标识一个Activity所需要的任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。可以为每个Activity都单独制定TaskAffinity属性,
这个属性必须不能和包名相同,否则就相当于没有制定。TaskAffinity属性主要和singleTask启动模式或者allTaskReparenting属性配对使用。当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity回运行在名字和TaskAffinity相同的任务栈中。
当TaskAffinity和allowTaskReparenting结合的时候,会产生特殊的效果。当一个应用A启动了应用B的某个Activity后,如果这个Activity的allTaskReparenting属性为true的话,那么当应用B被启动后,词Activity回直接从应用A的任务栈转移到应用B的任务栈中。
7.SharedPerences是否安全?
SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,因为SharedPreferences底层是通过读/写XML文件来实现的,并发会出问题。
每个应用的SharedPreferences文件都在当前包所在的data目录下查看到,目录位于 /data/data/pacakage name/shared_prefs目录下。
SharedPreferences也属于文件的一种,但是由于系统对他的读/写有一定的缓存策略,即在内存中有一份ShareePreferences文件的缓存,因此在多进程下,系统对它的读写诗不可靠的,当面对高并发的读/写访问,有很大几率丢失文件。
8.Serializable接口的原理及serialVersionUID作用?
想让一个对象实现序列化,只需要这个类实现Serializable接口并声明一个serialVersionUID即可。实际上,甚至连这个serialVersionUID也不是必须的。如何进行对象的序列化和反序列化也很简单,只需要采用ObjectOutputStream和ObjectInputStream,readObject()writeObject()函数。
serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前累的serialVersionUID相同时才能够正常的被反序列化。
serialVersionUID的详细工作机制时这样的:序列化的时候系统把当前类的serialVersionUID写入序列化文件中,当反序列化时候系统回去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本时相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变化,比如成员变量的数量、类型可能发生了改变,这个时候时无法正常反序列化的。
9.Parcelable接口原理?
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并通过Intent和Binder传递。
Parcel内部包装了可序列化的数据,可以在Binder中自由传输。
序列化的功能由writeToParcel方法来完成,最终时通过Parcel的一系列write方法来完成的;
反序列化功能由CREATOR来完成,其内部表明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程。
10.Parcelable和Serializable的区别?
Serializable是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作。
Parcelable是Android中的序列化方式,操作复杂,效率很高。
Parcelable主要用在内存序列化上,通过Parcelable将对象序列化到存储设备中或者将对象序列化后通过网络传输也都是可以的,但是
这个过程会稍显复杂,因此这两种情况建议使用Serializable。
11.AIDL流程及原理
1.创建aidl文件,声明函数及接口
2.系统根据IBookManager.aidl生成IBookManager.jar类,继承了IInterface接口,本身也是接口。在Binder中传输的接口都需要继承
IInterface接口。
3.该类声明aidl中的方法,同时声明了两个正形的id分别用于标识这两个方法,这两个id用于表示在transact过程中客户端所追求的到底是哪个方法。它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑由Stub的内部代理类Proxy来完成。
核心实现就是它的内部类Stub和Stub的内部代理类Proxy。
DESCRIPTOR:Binder的唯一标识,一半用单钱Binder的类名表示。
asInterface(android.os.IBinder obj):用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的数据,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。
asBindeeer:此方法用于返回当前Binder对象。
onTransact:这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层疯转后交由此方法处理。该方法的原型为public Boolean onTransact(int code,android.os.Parcel data,android.os.Parcel reply,int flags)。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数,然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值,onTransact方法执行过程是这样的。
需要注意的是,如果此方法返回false,那么客户端的请求会失败。
Proxy#geetBookList:这个方法运行在客户端,当客户端远程调用此方法时,,首先创建该方法所需要的输入型Parceel对象_data、输出型Parcel对象_reply和返回值对象List,然后把该方法的参数信息写入_data中;接着调用transact方法来发起RPC请求,同时当前线程刮起;然后服务端的onTransact方法会呗调用,知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_replay中的数据。
Proxy#addBook:这个方法运行在客户端,它的执行过程和getBookList是一样的。
注意点:1.当客户端发起远程请求时,由于当前线程会呗挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此次远程请求
2.由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。
12.Binder包含两个重要的方法linkToDeath和unlinkToDeath。Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这时到服务
端的Binder链接端礼,导致远程调用失败。为了解决这个问题,Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath可以给Binder设置一个死亡代理,当Linder死亡时,会收到通知,这时可以重新发起连接请求从而恢复连接。
可通过isBinderAlive可以判断BInder是否死亡。
1.声明DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法binderDied,我们需要实现这个方法,当Binder死亡的时候,系统就会毁掉binderDied方法
13.跨进程通讯Messenger原理?
通过Messenger在不同进程中传递Message对象。Messenger底层是AIDL。
1.服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回一个Messenger对象顶层的Binder即可
2.客户端进程中,首先绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息,发消息类型为Message对象。如果需要服务端能够回应客户端,就和服务端一样,还需要创建一个Handler并创建一个新的Messenger,并把这个Messengeer对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。
14.AIDL中能够使用的List只有ArrayList,但是使用了CopyOnWriteArrayList(注意它不是继承自ArrayList),为什么能够正常工作?
因为AIDL中所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,但是在Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList传递给客户端。所以,在服务端采用CopyOnWriteArrayList是完全可以的。
15.RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个范型,支持管理任意的AIDL接口,这点从它的生命可以看出,因为所有 的AIDL接口都继承自IInterface接口。原理是它的内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。
RemoteCallbackList内部自动实现了线程同步的功能,所以使用它来注册和解注册时,不需要做额外的线程同步工作。
17.Sockeet套接字,氛围流式套接字和用户数据套接字两种,分别对应于网络的传输控制层中的TCP和UDP。TCP时面向连接的协议,提供稳定的双向通讯功能,TCP连接的建立
需要经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,具有很高的稳定性。而UDP时无连接的,提供不稳定的单向通讯功能,当然UDP也可以实现双向通讯功能。UDP具有更好的效率,确定是不保证数据一定能够正确传输,尤其在网络拥塞的情况下。
18.TouchSlop时系统所能识别出的被认为是滑动的最小距离,当手指在屏幕滑动时,如果两次滑动指尖的距离小于这个常量,系统就不认为是在进行滑动操作。
ViewConfiguration.get(getContext()).getScaleedTouchSlop().
19.VelocityTracker速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
GestureDetector手势检测,用于辅助检测用护的点击、滑动、长安、双击等行为。创建GestrueDetector对象并实现OnGestureListener接口。
Scroller弹性滑动对象,用于实现View的弹性滑动。
20.View的滑动方式
1.通过View本身提供的scrollTo/scrollBy方法来实现滑动
2.通过动画给View施加平移效果实现滑动
3.改变View的LayoutParams使得View重新布局从而实现滑动
21.scrollTo和scrollBy区别
scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
22.View滑动过程中,view内部的两个属性mScrollX和mScrollY的改变规则,这两个属性可以通过getScrollX和getScrollY方法分别得到。
在滑动过程中,mScrollX的值总是等于View左边元和View内容左边元呢在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在水平方向的距离。
scrollTo和scrollBy只能改变View的内容的位置,而不能改变View在布局中的位置。
23.三种滑动对比
1.scrollTo/scrollBy:操作简单呢,适合对View内容的滑动。缺点只能滑动View的内容,不能滑动View本身。
2.动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果。
3.改变布局参数:操作稍微复杂,适用于有交互的View。
24.Scroller原理?
首先构造一个Scroller对象并调用它的startScroll方法,内部其实什么也没做,只是保存了我们传递的几个参数startScroll(int startx,int starty,int dx,int dy ,in duration),滑动指View内容的滑动而非View本身位置的改变。startScroll方法下面的invalidate方法。invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法,computeScroll方法在View中是一个空实现,因此需要我们去实现,上面的代码实现了computeScroll方法。
当View冲毁后会在draw方法中调用computeScroll,而computeScroll又会去想Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法来进行第二次重绘。这一次重绘的过程和第一次重绘一样,还是会导致computeScroll方法被调用,然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到最新的位置。
概括:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,他不断的让View重绘,二每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得到View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成Vieew滑动。就这样,Vieew的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。
25.点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生之后,系统需要把这个事件传递给一个具体的View,这个传递的过程就是分发过程。
分发过程主要由三个方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
1.dispatchToucherEveent(MotionEvent ev)
用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
2.onInterceptTouchEvent(MotionEvent ev)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
3.onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则同一事件序列中,当前View无法再次接收到事件。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev)
}else{
consume = child.dispatchTouchEvent(ev)
}
return consume;
}
26.事件传递规则
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法会被调用,如此反复知道事件被最终处理。
27.事件传递的优先级别 OnTouchListener > onTouchEvent -> OnCLickListener
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高.在onTouchEvent方法中,如果当前设置的有OnClickListeneer,那么它的onClick方法会被调用。可以看出,OnCLickListener,其优先级最低,即处于事件传递的尾端。
28.事件传递的顺序
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity在传递给Window,最后Window在传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。
29.关于事件传递的机制,一些结论
1.同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
2.正常情况下,一个序列事件只能被一个View拦截且消耗。因为一旦一个元素拦截了某个事件,那么同一事件序列内的所有事件都会直接全交给它处理,那么同一事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onToucheEvent强行传递给其他View处理。
3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEveent不会再被调用。当一个View决定拦截一个事件后,那么系统会把同一事件序列内的其他方法都直接交给它来处理,那么就不用再调用这个View的onInterceptTouchEevent去询问它是否要拦截了。
4.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新由它的父元素去处理,即父元素的onTouchEvent会被调用,意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。
5.如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
6.ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
7.View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
8.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
9.View的enable属性不影响onTouchEvent的默认值。哪怕一个View是diable状态的,只要他的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
10.onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
11.事件传递过程是由外向内的,即时间总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
30.事件分发源码解析
1.Activity点击事件的分发过程
点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体的工作是由Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。我们先从Activity的dispatchTouchEvent开始分析。
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getView().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}
首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
接下来看Window是如何将事件传递给ViewGroup的。Window是个抽象类,而Window的superDispatchTouchEEvent方法也是个抽象方法,因此我们必须找到Window的实现类才行。Window的实现类是PhoneWindow。window的superDispatchTouchEvent 最后调用的是mDecor.superDIspatchTouchEvent方法。通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentViewe设置的View是它的一个子View。目前事件传递到了DecorView这里,由于DecorView继承自FrameLayout且是父View,所以最终事件会传递给View。换句话说,事件肯定会传递到View,不然应用如何响应点击事件呢。从这里开始,事件已经传递到顶级View了,即在Activity中通过setContentView所设置的View,另外顶级View也叫根View,顶级View一般来说都是ViewGroup。
3.顶级View对点击事件的分发过程
点击事件达到顶级View以后,会调用ViewGroup的dispatchTouchEvent方法,如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,这时如果ViewGroup的mOnTouchEvent被设置,则onTouch方法会被调用,否则onTouchEvent会呗调用。也就是说,如果都提供的话,onTouch会屏蔽掉onTouchEvent方法。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递到了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
首先看ViewGroup对点击事件的分发过程,其主要实现在ViewGroup的dispatchTouchEvent方法中,它描述的是当前View是否拦截点击事件这个逻辑。
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget !=null){
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOWINTERCEPT != 0 )
if(!disallowIntercept){
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action)
}else{
intercepted = false;
}
}else{
intercepted = true;
}
从代码可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。ACTION_DOWN事件好理解,那么mFirstTouchTarget!=null是什么意思呢?当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,当ViewGroup不拦截事件并将事件交给子元素处理时mFirstTouchTarget!=null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget!=null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget!=null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件就会默认交给它处理。
当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN意外的恶其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会充值FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onIntercept方法来询问自己是否要拦截事件。
if(actionMasked == MotionEvent.ACTION_DOWN){
cancelAndClearTouchTargets(ev);
resetTouchState();
}
ViewGroup会在ACTION_DOWN事件到来时做重制状态的操作,而在resetTOuchState方法中会对FLAG_DISALLOW_INTERCEPT进行充值,因此子View调用request-DisallowInterceptTouchEvent方法并不影响ViewGroup对ACTION_DOWN事件的处理。
得出结论:当ViewGroup决定拦截事件后,后续的点击事件将会默认交给它处理并且不在调用它的onInterceptTouchEvent方法。
FLAG_DISALLOW_INTERCEPT这个标志的作用时让ViewGroup不在拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件。
价值:第一点,onInterceptToucheEvent不是每次事件都会被调用的,如果想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup;
第二点,FLAG_DISALLOW_INTERCEPT标记位的作用给提供了一个思路,当面对滑动冲突时,我们可以考虑用这种方法解决问题。
接着再看当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理
final View[] children = mChildren;
for(int i = childrenCount - 1; i >= 0 ; i –){
final int childIndex = customOrder ? getChildDrawingOrder(chilrenCount, i) : i ;
final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
if(!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x,y,child,null)){
continue;
}
newTouchTarget = getTouchTarget(child);
if(newTouchTarget != null){
newTouchTarget.pointerIdBits != idBitsToAssign;
break;
}
}
resetCancelNextUpFlag(child);
if(dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)){
mLastTouchDownTime = ev.getDownTime();
if(preorderedList != null){
for(int j=0;j<childrenCOunt ; j++){
if(children[childIndex]==mChildren[j]){
mLastTouchDownIndex = j ;
break;
}
}
}else{
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true ;
break;
}
上方代码逻辑:首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件。是否能够接受点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,而在上面的恶代码中传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理,从而完成了一轮事件分发。
if(child == null){
handled = super.dispatchTouchEvent(event);
}else{
handled = child.dispatchTouchEvent(event);
}
如果子元素的dipatchTouchEvent返回true,这是我们暂时不用考虑事件在子元素内部时怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环,
newTouchTarget = addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了mFirstTouchTarget的肤质并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素的话)。
其实mFirstTouchTarget真正的赋值过程是在addTouchEvent内部完成的,从下面的addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截测恶略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件。
private TouchTarget addTouchTarget(View child,int pointerIdBits){
TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
target.next = mFirstTouchTargeet;
}
如果遍历所有的子元素后事件都没有被合适的处理,那包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。着两种情况下,ViewGroup会自己处理点击事件。
if(mFirstTouchTarget == null){
handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS)
}
这里第三个参数child为null,他会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。
4.View对点击事件的处理过程
View对点击事件的处理过程稍微简单,注意这里的View不包含ViewGroup。
public boolean dispatchTouchEvent(MotionEvent event){
boolean result = false;
if(onFilterTouchEventForSecurity(event)){
ListeneerInfo li = mListenerInfo;
if(li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTOuchListener.onTouch(this,event)){
result = true;
}
}
if(!result && onTouchEvent(event)){
result = true;
}
return result;
}
View对点击事件的处理过程比较简单,因为View是一个单独的元素,他没有子元素因此无法向下传递事件,所以它只能自己处理事件。View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可以OnTouchListener的优先级高于onTouchEevent,方便在外界处理点击事件。
在分析OnTouchEEveent的实现。先看当View处于不可用状态下点击事件的处理过程。不可用状态下的View照样会消耗点击事件,接管它看起来不可用。
if((viewFlags & ENABLE_MASK) == DISABLED){
if(event.getAction() == MotionEveent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0){
setPressed(false);
}
return (((vieewFlags & CLICKABLEE) == CLICKABLE || (viewFlags & LONG_CLICKABLE)== LONG_CLICKABLE));
}
如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法,这个onTouchEvent的工作机制看起来和OnTouchListener类似。
if(mTouchDelegate != null){
if(mTouchDelegate.onTouchEvent(eevent)){
return true;
}
}
下面看一下onTouchEvent中对点击事件的具体处理
if(((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)){
switch(event.getAction()){
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed){
if(!mHasPerformedLongPress){
removeLongPressCallback();
}
if(!focusTaken){
if(mPerformClick == null){
mPerformClick = new PerformClick();
}
if(!post(mPerformClick)){
performClick();
}
}
}
}
}
从上面的代码来看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEEvent方法返回true,不管它是不是DISABLEE状态,然后就是当ACTION_UP事件发生时,会出发performClick方法,如果View设置了OnClickListener,那么performClick方法回调用它的onClick方法,如下所示。
public boolean performClick(){
final boolean result;
final ListeenerInfo li = mListenerInfo;
if(li != null && li.mOnClickListeener != null){
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
}else{
result =false;
}
sendAccessibilityEvent(Access)
return result;
}
View的LONG_CLICKABLE属性默认认为false,而CLICKABLE属性是否为false和具体的View有关,确切来说是可点击的Viewe其CLICKABLE为true,不可点击的View其CLICKABLE为false,比如Button是可点击的,TextView是不可点击的。通过setClickable和setLongClickable可以分别改变View的恶CLICKABLE和LONG_CLICKABLE属性。另外,setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener则会自动将View的LONG_CLICKABLE设为true,如下所示:
public void setOnClickListener(OnClickListener l){
if(!isClickable()){
setClickable(true);
}
getListenerInfo().mOnClickListener = 1;
}
public void setOnLongClickListenr(OnLongClickListener l){
if(!isLongClickable){
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
31.滑动冲突时如何产生的呢?如何解决滑动冲突呢?
其实在解饿面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。
32.常见的滑动冲突场景
场景1:外部滑动方向和内部滑动方向不一致
场景2:外部滑动方向和内部滑动方向一致
场景3:上面两种情况的嵌套。
解决场景1:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点事件。这个事件我们就可以根据它们的特征来解决滑动冲突,具体来说时:根据滑动时水平滑动还是竖直滑动来判断到底由谁来拦截事件,根据滑动过程中两个点之间的坐标就可以得出到底时水平滑动还是竖直滑动。
解决场景2:无法根据滑动的角度、距离差以及速度差来做判断。可以从业务上规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View来响应View的滑动,根据这种业务上的需求我们也能得到响应的处理规则,有了处理规则同样可以进行下一步处理。
解决场景3:无法直接根据滑动角度、距离差以及速度差来做判断。
33.如果根据坐标来得到滑动的方向?
可以依据滑动路径和水平方向所形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断,某些特殊时候还可以依据水平和竖直方向的速度差来做判断。
34.两种解决滑动冲突的方式:外部拦截法和内部拦截法。
1.外部拦截法:所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。
public boolean onInterceptTouchEvent(MotionEveent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
intercepted = false;
break;
}
case MotionEvent.ACTION_DOWN:{
if(父容器需要当前点击事件){
intercepted = true;
}else{
intercepted = false;
}
break;
}
}
mLastXInterecept = x;
mLastYInterecept = y;
return intercepted;
}
上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不要做修改并且也不能修改。这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,设置因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件无法再次传递给子元素。其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。
考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接受到ACTION_UP事件,这个时候子元素中的onClick事件就无法出发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。
2.内部拦截法是指父容器不拦截任何事件,所有的时间都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截.稍显复杂。伪代码如下,需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotinoEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此类点击事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP:{
break;
}
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
35.为什么父容器不能拦截ACTION_DOWN事件?
因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截法就无法起作用了。
父元素所作的修改如下:
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
36.ViewRoot和DecorView的概念
ViewRoot对应于ViewRootImpl类,它是链接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,代码如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
37.View的绘制流程?
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽度和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制到屏幕上。
performTraversals
ViewGroup View
performMeasure -> measure -> onMeasure measure
performLayout -> layout -> onLayout layout
performDraw -> draw -> onDraw draw
performTraversals会一次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。
measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等同于View最终的宽高,但是特殊情况除外。Layout过程决定了View的四个顶点的坐标和实际的View的宽高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点位置,并可以通过getWidth和getHeight方法来拿到View额最终宽高。Draw过程则决定View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。
DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏。在Activity中通过setContextView所设置的布局文件被加到内容栏中,而内容栏的id是content,因此可以链接为Activity指定布局的方法不叫setView而叫setContentView,因此布局的确加到了id为content的FrameLayout中。通过ViewGroup content = findViewById(R.android.id.content)。通过content.getChaildAt(0)可以获取到设置的View。DecorView是一个FrameLayout,View层的事件都先经过DecorView,然后才传递给我们的View。
38.理解MeasureSpec
MeasureSpec参与了View的mesasure过程,很大成都上决定了一个View的尺寸规格,这个过程还受父容器的影响,因此父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽高。
MeasureSpec代表一个32位int值,高2为代表SpecMode,低30位代表SpecSize,SpecMode是值测量模式,而SpecSize是指在某种测量模式下的规格大小。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static fianl int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode){
if(sUseBrokenMakeMeasureSpec){
return size + mode;
}else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpecc){
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec){
return (measureSpec & ~MODE_MASK);
}
MeasureSpec通过SpecMode 和 SpecSize打包成一个int值来避免过的多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。
UNSPECIFIED: 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
ExACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
39.MeasureSpec和LayoutParams的对应关系
系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应额MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽高。需注意,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定,对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。
对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下代码,它展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着看一下getRootMeasureSpec方法的实现:
private static int getRootMeasureSpec(int windowSize,int rootDimension){
int measureSpec;
switch(rootDimension){
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
default:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
}
return measureSpec;
}
通过上述代码,DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParms中额宽高的参数来划分。
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
固定大小:精确模式,大小为LayoutParams中指定的大小。
对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,看下ViewGroup的measureChildWithMargins方法:
public static int getChildMeasureSpec(int spec, int padding, int chaildDimension){
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize - padding);
int resultSize = 0 ;
int resultMode = 0 ;
switch(specMode){
case MeasureSpec.EXACTLY:
if(childDimension >= 0){
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}else if(childDimensiong == LayoutParams.MATCH_PARENT){
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}else if(childDimension == LayoutParams.WRAP_CONTENT){
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if(childDimension >= 0){
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}else if(childDimensiong == LayoutParams.MATCH_PARENT){
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}else if(childDimension == LayoutParams.WRAP_CONTENT){
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if(childDimension >= 0){
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}else if(childDimensiong == LayoutParams.MATCH_PARENT){
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}else if(childDimension == LayoutParams.WRAP_CONTENT){
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpecc(resultSize, resultMode);
}
它的作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,此参数中的padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,代码如下:
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
40.View的工作流程?
View的工作流程主要是指measure、layout、draw这三大的流程,即测量、布局和绘制,其中measure确定View的测量宽高,layout确定View的最终宽高的四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素在递归去执行这个流程。
1.View的measure过程
view的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这个方法子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,onMeasure方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec, getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)));
}
setMeasuredDimension方法会设置View宽高的测量值,因此需要看getDefaultSize这个方法即可:
public static int getDefaultSize(int size, int measureSpec){
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSPec.getSize(measureSpec);
switch(sspecMode){
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
rersult = specSize;
break;
rerturn result;
}
}
可以看出,getDefaultSize这个方法逻辑很简单,我们只需要看AT_MOST和.EXACTLY这种情况。简单地理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。
View的宽高由specSize决定,所以我们可以得出如下结论,直接继承View的自定义空间需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
41.getSuggestedMinimumWidth大的逻辑?
如果View没有设置背景那么返回android:minWidth这个属性所指定的值,这个值可以为0,如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View的UNSPECIFIED情况下的测量宽高。
42.ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素在递归去执行这个过程。和View不同的时,ViewGroup时一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个较measureChildren的方法
protected void measureChildren(int widthMeasureSpec,int heightMeasureSpec){
final int size = mChildrenCount;
final View[] children = mChildren;
for(int i = 0; i < size ; ++i){
final View child = children[i];
if((child.mViewFilags & VISIBILITY_MASK) != GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
}
从上述代码来看,ViewGroup在measure时,会对每一个子元素进行measure,measureChild这个方法的实现也很好理解,
protected void measureChild(View child,int parentWidthMeasureSpec, int parentHeightMeasureSpec){
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidth-MeasureSpec,mPaddingLeft+mPaddingRight,lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeight - MeasureSpec,mPaddingTop + mPaddingBottom , lp.height);
child.measure(childWidthMeasureSpec , childHeightMeasureSpec);
}
measureChild 的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec的工作已经再上面进行了详细分析。
ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup时一个抽象类,其测量过程的onMeasure方法需要哥哥子类去具体实现,比如LinearLayout、RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做同意的实现呢?那是因为不同的ViewGroup子类有不同的布局特征,这导致他们的测量细节各不相同,比如LinearLayout、RelativeLayout这两者布局特性显然不同,因此ViewGroup无法做统一实现。
43.比如我们想再Activity已启动的时候做一件任务,但是这个一件任务需要获取某个View的宽高,再onCreate或者onResume里面获取这个View的宽高可以么?
实际上onCreate、onStart、onResume中均无法正确得到某个View的宽高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获取到的宽高就是0.
1.Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口地道道焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁的进行onResume和onPause,那么onWindowFousChanged也会被频繁地调用。
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFous){
int widht = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
protected void onStart(){
super.onStart();
view.post(new Runnable(){
public void run(){
int widht = view.getMeasuredWidth();
int height = view.getMeasureHeight();
}
});
}
3.ViewTreeObserver。
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个几口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高一个很好的实际。需要注意的是,伴随着View树的状态改变等。onGlobalLayout会被调用多次。
protected void onStart(){
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
public void onGlobalLyout(){
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidht();
itn height = view.getMeasuredHeight();
}
});
}
4.view.measeure(int widthMeasureSpec, int heightMeasureSpec).
通过手动对View进行measure来得到View的宽高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
match_parent:
直接放弃,无法measure出具体的宽高。原因很简单,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
具体的数值:
比如宽高都是100px,如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30) - 1, MeasureSPec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30) - 1, MeasureSPec.AT_MOST);
44.layout过程
Layout的作用时ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它再onLayout中会遍历所有的子元素并调用其layout方法,再layout方法中onLayout方法会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本省的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法,如下所示:
public void layout(int l, int t, int r ,int b){
if((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0){
onMeasure(mOldWidthMeasureSpec , mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBotytom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l,t,r,b);
if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED == PFLAG_LAYOUT_REQUIRED)){
onLayout(changed, l, t, r, b);
}
}
layout方法的大致流程如下:时首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了,接着会调用onLayout方法,这个方法的用途时父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
protected void onLayout(boolean changed, int l,int t, int r, int b){
if(mOrientation == VERTICAL){
layoutVertical(l, t, r, b);
}else{
layoutHorizontal(l, t, r, b);
}
}
LinearLyout中onLayout的实现逻辑和onMeasure的实现逻辑类似,这里选择layoutVertical继续讲解
void layoutVertical(int left, int top, int right, int bottom){
final int count = getVirtualChildCount();
for(int i = 0; i < count; i++){
final View child = getVirtualChildAt(i);
if(child == null){
childTop += measureNullChild(i);
}else if(child.getVisibility() != GONE){
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutPaarams) child.getLayoutParams();
if(hasDividerBeforeChildAt(i)){
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocation - Offset(child);
i += getChildrenSkipCount(child,i);
}
}
}
这里分析一下layoutVertical的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定相应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置靠下的位置,这刚好符合竖直防线的LinearLayout的特性。至于setChildFrame,它仅仅时调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。setChildFrame方法的实现如下所示。
private void setChildFrame(View child, int left, int top, int width, int height){
child.layout(left, top, width, height);
}
setChildFrame中的width和height实际上就是子元素的测量宽高,从下面的代码可以看出这一点:
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
而在layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几句赋值语句,这样以来子元素的位置就确定了:
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
45.View的测量宽高和最终宽高有什么区别?View的getMeasuredWidth 和 getWidth这两个方法有什么区别?
看一下getWidth 和 getHeight 这两个方法的具体实现:
public final int getWidth(){
return mRight - mLeft;
}
public final int getHeight(){
return mBottom - mTop;
}
从getWidth和getHeight的源码在结合mLeft、mRight、mTop和mBottom这四个变量的复制过程来看,getWidth方法的返回值刚好就是View的测量宽度,而getHeight方法的返回值也刚好就是View的测量高度。经过上述分析,在View的默认实现中,View的测量高度和最终的宽高时相等的,只不过测量宽高形成于View的measure过程,而最终的宽高形成于View的layout过程,即两者复制实际不同,测量宽高的复制时机稍微早一些。可以认为View的测量宽高就等于最终宽高,但是的却存在有些特殊情况下会导致两者不一致。
如果重写View的layout方法,代码如下:
public void layout(int l ,int t, int r, int b){
supre.layout(l, t, r + 100, b + 100);
}
上述代码会导致任何情况下View的最终宽高总是比测量宽高大100px,虽然这样做会导致View显示不正常并且也没有实际意义,但是这证明了测量宽高的确可以不等于最终宽高。另外一种情况是在某些情况下,View需要多次measure才能确定自己的测量宽高,在前几次的从测量过程中,其得到的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相同。
46.draw过程
Draw过程就比较简单了,它的作用是将View会知道屏幕上面。View的绘制过程遵循如下几步:
1.绘制背景background.draw(canvas)
2.绘制自己onDraw
3.绘制children(dispatchDraw)
4.绘制装饰(onDrawScrollBars)
这一点通过draw方法的源码可以明显看出来
public void draw(Canvas canvas){
final int privateFlags = mPrivateFlags ;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PGLAG_DRAWN;
//Step1. draw the backgroud ,if needed
int saveCount;
if(!dirtyOpaque){
drawBackgroud(canvas);
}
final int viewFlags = mViewFLags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if(!verticalEdges && !horizontalEdges){
//Step 3 draw the content
if(!dirtyOpaque) onDraw(canvas);
//Step 4 draw decorations(scrollbars)
onDrawScrollBars(canvas);
if(mOverlay != null && !mOverlay.isEmpty()){
mOverlay.getOverlayView().dispatchDraw(canvas);
}
return;
}
}
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会便利调用所有子元素的draw方法,如此draw时间就一层层地传递了下去。View有一个特殊地方法setWillNotDraw,如下:
public void setWillNotDraw(boolean willNotDraw){
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从setWillNotDraw这个方法地注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启动这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控制继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示的关闭WILL_NOT_DRAW这个标记位。
47.自定义VIew的分类
1.继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则的图形。很显然需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout
这种方式主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像集中View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View
一般时用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding等。
4.继承特定的ViewGroup
这种方法也比较常见,当某种效果看起来很像集中View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。
48.自定义View过程中的一些注意事项
1.让View支持wrap_content
支持因为支持继承View或者ViewGroup的控制,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。
2.如果有必要,让你的View支持padding
这是因为支持继承View的控件,如果不在draw方法中处理padding,那么padding属性时无法起作用的。直接继承自ViewGroup的控制需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
3.尽量不要在View中使用Handler,没必要
这是因为View内部本身就提供了post系列的方法,完全可以替代Hanlder的作用。
4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove 时,View的onDetachedFromWindow方法会被调用,和此方法对应的时onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。
5.View带有华东嵌套情形时,需要处理好滑动冲突
如果有滑动冲突的话,那么要合适的处理华东冲突,否则将会严重影响View的效果。
49.AppWidgetProvider是Android中提供的用于实现桌面小部件的类,其本质是一个广播,即BroadcastReceiver。
50.PendingIntent和Intent的区别是什么?
PendingIntent表示一种处于pending状态的意图,而pending状态表示的是一种待定、等待、即将发生的意思,就是说接下来有一个Intent将在某个待定时刻发生。PendingIntent和Intent的区别在于,PendingIntent是在将来的某个不确定的时刻发生,而Intent是立刻发生。
51.PendingIntent匹配规则?
如果两个PendingIntent他们内部的Intent相同并且requestCode也相同,那么这两个PendingIntent就是相同的。requestCode相同比较好理解,那么什么情况下Intent相同呢?Intent的匹配规则是:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的。需要注意的是Extras不参与Intent的匹配过程,只要Intent之间的ComponentName和intent-filter相同,即使他们的Extras不同,那么这两个Intent样式相同的。
52.PendingIntent中flags参数的含义?
1.FLAG_ONE_SHOT:当前描述的PendingIntent只能被使用一次,然后它就会被自动cancel,如果后续还有相同的PendingIntent,那么他们的send方法就会调用失败。对于通知栏消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续的通知点击后将无法打开。
2.FLAG_NO_CREATE:当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService和getBroadcast方法会直接返回null,即获取PendingIntent失败。这个标记位很少见,它无法单独使用。
3.FLAG_CANCEL_CURRENT:当前描述的PendingIntent如果已经存在,那么他们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息来说,那些被cancel的消息单机后 将无法打开。
4.FLAG_UPDATE_CURRENT:当前描述的PendingIntent如果已经存在,那么他们都会被更新,即他们的Intent中的Extras会被替换成最新的。
53.通知栏和桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通讯。因此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的,而他们运行在系统的SystemServer中,这就和我们的进程构成了跨进成通讯的场景。
54.RemoteViews的apply以及reapply方法来家在活着更新界面的,apply和reApply的却别在与:apply会家在布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。
WindowManager所提供的功能很简单,常用的只有三个方法,即添加View、更行View和删除View,这三个方法定义在ViewManager中,而WindowManager继承了ViewManager
public interfacce ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view,ViewGroup.LayoutParams params);
public void removeView(View view);
}
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,因此此Window并不是实际存在的,它是以View的形式存在。
Window的添加过程
Window的添加过程需要通过WindowManager的addView来实现,WindowManager是一个接口,它的真正实现是WindowManagerImpl类。在WindowManagerImpl中Window的三大操作的实现如下:
public void addView(View view, ViewGroup.LayoutParams params){
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
public void updateViewLayout(View view, ViewGroup.LayoutParams params){
mGlobal.updateViewLayout(view, params);
}
public void remoteView(View view){
mGlobal.removeView(view, false);
}
WindowManagerImpl并没有直接实现Window的三大操作,二十交割WindowManagerGlobal来处理,WindowManagerGlobal以工厂的形式向外提供自己的实力,在WindowManagerGlobal中有以下代码:private final WindowManagerGLobal mGlobal = WindowManagerGlobal。getInstance()。WindowManagerImpl这种工作模式是典型的桥接模式,将所有的操作全部委托给WindowMangerGlobal来实现。WindowManagerGlobal的addView方法主要分为如下几步。
1.检查参数是否合法,如果是子Window那么还需要调整一些布局参数
if(view == null){
throw new IllegalArgumentException(“view must not be null”);
}
if(display == null){
throw new IllegalArgumentException(“display must not be null”);
}
if(!(params instanceof WindowManager.LayoutParams)){
throw new IllegalArgumentException(“Params must be WindowManager.LayoutParams”);
}
final WindowManager.LayoutParams wparams = (WindowManger.LayoutParams)params;
if(parentWindow != null){
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
2.创建ViewRootImpl并将View添加到列表中
在WindowMangerGlobal内部有如下几个列表比较重要:
private final ArrayList
private final ArrayList
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
private final ArraySet
在上面生命中,mViews存储的是所有Window所对应的View,mRoots存储的是所有Window所对应的ViewRootImpol,mParams存储的是素有Window所对应的布局参数,而mDyingViews则存储了那些正在被删除的View对象,或者说是那些已经调用removeView方法但是删除操作还未完成的Window对象。在addView中通过如下方式将Window的乙烯利列对象添加到列表中:
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
3.通过ViewRootImpl来更新界面并完成Window的添加过程
这个步骤由ViewRootImpl的setView方法来完成,View的绘制过程是由ViewRootImpl来完成的。在setView内部会通过requestLayout来完成异步刷新请求。scheduleTraversals实际是View绘制的入口:
public void requestLayout(){
if(!mHandlingLayoutInLayoutRequest){
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}