Activity启动以及View的绘制

先整理一下Activity A start ActivityB 的一个brief 流程 如下

对应的 Wms Ams ActivityThread 的流程:

setContentView 开始,我们看发生了什么

  1. activity 的setContentView 实际上调用的是Activity 依赖的 mWindow对象的setContentView方法 Window是一个抽象类,在目前来说仅有一个实现类– PhoneWindow。
  2. 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对象的。

  1. 通过Resources对象拿到布局xml文件,同时ResourceImpl去解析出xmlResourceParser 实例XmlResourceParser parser = res.getLayout(resource) 这里面ResourceImpl解析各个xml块 解析的时候里面还有一个缓存机制, 如果已经解析过了,则第二次再次inflat这个布局就直接从缓存中去而不用读取文件了,要知道这可是在主线程啊。。。
  2. 传递 parser 给LayoutInflater 对象 View
    这里就是循环读取parser 并new ViewGroup 或者View 添加了到相应的ViewGroup之下,代码比较长,不贴了,用了一堆装饰模式。
  3. 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啦)
  4. 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。

  1. 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

  2. layout

    setLayout 中 setFrame 然后回调onLayout 函数如果继承自viewGroup 则必须实现onLayout方法。因为ViewGroup中的OnLayout是抽象方法,
    setFram中如果位置有变化,则会调用invalidate 并把DRAWN 写入mPrivateFlags。 onLayout主要是用于ViewGroup 对子视图进行分配位置。
    LinearLayout 有两种布局VerticalLayout和HorizentalLayout
  3. 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:

  1. 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);
    }
  2. 子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 中。

  3. 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);

Leave a Reply

Your email address will not be published. Required fields are marked *