先整理一下Activity A start ActivityB 的一个brief 流程 如下
对应的 Wms Ams ActivityThread 的流程:
setContentView 开始,我们看发生了什么
- activity 的setContentView 实际上调用的是Activity 依赖的 mWindow对象的setContentView方法 Window是一个抽象类,在目前来说仅有一个实现类– PhoneWindow。
- PhoneWindow 干了啥。
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystallized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
关键看 mLayoutInflater.inflate(layoutResID, mContentParent);
这不和我们普通动态添加一个view到一个viewGroup 一样么?顺带看一下layoutinflater 是怎么把一个xml 生成一个个的view对象的。
- 通过Resources对象拿到布局xml文件,同时ResourceImpl去解析出xmlResourceParser 实例XmlResourceParser parser = res.getLayout(resource) 这里面ResourceImpl解析各个xml块 解析的时候里面还有一个缓存机制, 如果已经解析过了,则第二次再次inflat这个布局就直接从缓存中去而不用读取文件了,要知道这可是在主线程啊。。。
- 传递 parser 给LayoutInflater 对象 View
这里就是循环读取parser 并new ViewGroup 或者View 添加了到相应的ViewGroup之下,代码比较长,不贴了,用了一堆装饰模式。 - View 对应的类有了。怎么去渲染到屏幕上?
app开发知道。setContentView 后我们的布局文件会渲染在标题栏下,那标题栏是啥?我们没有写相关代码,它又是如何创建的呢?我先用uiautomatorviewer(在sdk tools/bin目录下)查看一下布局结构如下。
这个id为decor_content_parent 的ViewGroup 对应的就是我们的整个app的布局,包括标题栏(不包括顶部状态栏),其实最外层根节点FramLayout在activity 的window中对应的就是一个mDecodeview 它继承自FrameLayout,同时 PhoneWindow里面还有一个DecorContentParent mDecorContentParent ,它就是这个id为 decor_content_parent 的ViewGroup,(1)View 对应的就是statusBar (我们的状态栏,因此理论上可以反射到这个View 然后给其设置动态背景颜色,当然也可以直接利用api啦) - Window设置完成之后。
Window设置完成之后,就要把创建的这个窗口告诉WmS。 Activity准备好后通知Ams,Ams 处理完条件判断,最终调用Activity的makeVisible()
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }
这里的ViewManager 是LocalWindowManager,如下设置的时候mWindowManager 就是createLocalWindowManager 虽然和WindowmanagerImpl 都是实现WindowManager,但是前者做了一些权限拦截处理。
//window.java /** * Set the window manager for use by this Window to, for example, * display panels. This is <em>not</em> used for displaying the * Window itself -- that must be done by the client. * * @param wm The window manager for adding new windows. */ public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) { mAppToken = appToken; mAppName = appName; mHardwareAccelerated = hardwareAccelerated || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false); if (wm == null) { wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); } mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); }
View绘制
invalidate(), 请求View的重绘制,首先绘制根视图,然后再是其下面的子视图,会根据View 下面的mPrivateFlags 属性进行判断是否需要绘制,如果视图大小为0 也不会去重绘.
performTraversals(),根据之前状态判断是否需要measure,layout 和draw。
- measure
measure本质是把相对值转化为具体值的过程,从RootView的host.measure开始的
view对应的原型为:public final void measure(int widthMeasureSpec, int heightMeasureSpec)
因此,是不可被子类覆盖,只能覆盖onMeasure,子视图可以setMeasureDimension() 设置任意大小的布局。match_parent 对应的MeasureSpec.EXACTLY(父窗口会限制子类的大小) wrap_contet 对应MeasureSpec.AT_MOST
- layout
setLayout 中 setFrame 然后回调onLayout 函数如果继承自viewGroup 则必须实现onLayout方法。因为ViewGroup中的OnLayout是抽象方法,
setFram中如果位置有变化,则会调用invalidate 并把DRAWN 写入mPrivateFlags。 onLayout主要是用于ViewGroup 对子视图进行分配位置。
LinearLayout 有两种布局VerticalLayout和HorizentalLayout - draw
还是从ViewRoot 的performTraversals函数开始,调用draw(),再调用mView.draw(),mView就是窗口的根视图,Activity 就是DecordView/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
上面是View.draw(),注释。解释了draw很明白了 第四步会调用dispatchDraw 方法,viewGroup实现了这个方法。绘制完一个步骤会回调onDraw(canvas) 方法,这个在子类中可以重载。
canvas从哪里来的?ViewRoot 从surface中获取。(显卡,图形加速支持的surface 或者cpu内存模拟的Surface)
重绘View的时机:invalidate(改变selected 状态,setVisibility),requestLayout(setVisible),requestFocus(下一步,搜索等)
点击事件分发
WmS 判断当前用户在和哪个窗口交互,则将事件派发到相应的窗口,按键消息keyEvent的传递的路径是:
Activity ->mWindow ->DecordView ->FrameLayout->ViewGroup->mFocused.dispatchEventKey()
这样就开始遍历其下面的子视图。
对于触摸消息 motionEvent,则和上述相反,先由子视图处理,没有胎儿期(Wms不会预先处理).
TouchEvent:
- ViewGroup可以拦截事件传递到子View,拦截 MotionEvent.ACTION_DOWN 即可,这样,只有这个ViewGroup才能响应onTouchEvent事件,子view被拦截
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.d(TAG, "onInterceptTouchEvent: " + ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: return true; } return super.onInterceptTouchEvent(ev); }
- 子View 可以不让ViewGroup响应。也是通过消化 MotionEvent.ACTION_DOWN。返回true 父group则不再回调onTouchEvent
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "-----onTouchEvent: "+event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: return true; } return super.onTouchEvent(event); }
事件传递最后才到达Activity的onTouchEvent 中。
- dispatchTouchEvent
对于View,这个方法 调用super.dispatchEvent方法才能触发onTouchEvent 回调(如果这个是viewgroup的话回调的是onInterceptTouchEvent)。retrun true 事件将不会向下层(物理视觉上的)传递,但也不调用onTouchEvent。 false 则会向这个View的底层(视觉上)传递,就是那种点击穿透。
对于ViewGroup 如果disptatchTouchEvent返回super.dispatchTouchEvent 会调用onInterceptTouchEvent ,如果返回false 则直接调用父容器(ViewGroup)的onTouchEvent 方法。不再向下传递. 如果返回True 则事件被消费(消失)
添加一个View 到其它app之上如何做?
类似于以前那些一键清理等工具。既然所有的View 都是Wm 管理,而它提供了一个addView方法。其中参数LayoutParams type 如果是2000 以上是系统窗口类型,如果是1000以上是子窗口类型,如果是小于100 则是应用程需窗口类型。
设置不同的类型 对应需要的权限不一样,比如我们设置了TYPE_APPLICATION_OVERLAY 则需要显示在其它窗口之上的权限:”android.permission.SYSTEM_ALERT_WINDOW”
final TextView view = new TextView(context); view.setText(R.string.app_name); view.setBackgroundColor(context.getResources().getColor(R.color.colorAccent)); final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams(); int LAYOUT_FLAG; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE; } wmParams.type=LAYOUT_FLAG; //这里是关键,你也可以试试2003 wmParams.format=1; wmParams.flags=40; wmParams.width=400; wmParams.height=400; wmParams.verticalMargin=-0.40f;//在屏幕上的百分比默认是在中间这里往上移动了40%也就在顶部了它不包括电量栏 wm.addView(view,wmParams); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { wm.removeView(view); } });
如果想让它能够随着手指拖动,只需监听view的OnTouchListener,并处理好MotionEvent.ACTION_DOWN 和MotionEvent.ACTION_MOVE 在move的同时更新wm.updateViewLayout(view, wmParams);