View的绘制流程

2021-09-18  本文已影响0人  壹元伍角叁分

1、ActivityThread.handleResumeActivity()解析

ActivityThread.handleResumeActivity()
 --> final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
     --> r.activity.performResume(r.startsNotResumed, reason);
         --> mInstrumentation.callActivityOnResume(this);@activity
     //获取到PhoneWindow,PhoneWindow是在attach()中创建的。
 --> r.window = r.activity.getWindow();
     //获取到DecorView。DecorView是在setContent()中创建的。
 --> View decor = r.window.getDecorView();
     //ViewManager也是在activity.attach()中创建,wm实际也就是WindowManagerImpl
 --> ViewManager wm = a.getWindowManager();
 --> WindowManager.LayoutParams l = r.window.getAttributes();
     //将DecorView添加到window上的
 --> wm.addView(decor, l);
     --> mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());@WindowManagerImpl
         //ViewRootImpl
         --> root = new ViewRootImpl(view.getContext(), display);@WindowManagerGlobal
         //将三个参数进行保存
         -->  mViews.add(view); //DecorView
         -->  mRoots.add(root); //ViewRootImpl
         -->  mParams.add(wparams); //WindowManager.LayoutParams
         //将DecorView添加到ViewRootImpl中去
         --> root.setView(view, wparams, panelParentView, userId);

重要的三个类的作用:WindowManagerImpl、WindowManagerGlobal、ViewRootImpl
WindowManagerImpl:确定 View 属于哪个屏幕,哪个父窗口
WindowManagerGlobal:管理整个进程,所有的窗口信息。每个进程都有一个对应的WindowManagerGlobal
ViewRootImpl:是WindowManagerGlobal的实际操作者。只操作自己的窗口,和wms交互都是ViewRootImpl来操作

2、ViewRootImpl.setView()解析

ViewRootImpl.setView();
    //请求遍历
--> requestLayout();
    --> scheduleTraversals();
        --> mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            --> final class TraversalRunnable implements Runnable {
                    @Override
                    public void run() {
                       doTraversal();
                       //绘制view流程
                       --> performTraversals();
                           //1、预测量
                           --> windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
                               //只有在WRAP_CONTENT情况才会进行预测量
                               --> if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                                       //父控件给你一个值,设置baseSize
                                       res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
                                       if (baseSize != 0 && desiredWindowWidth > baseSize) {
                                           ...
                                           //第一次进行测量
                                           performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                                           ...
                                           if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                                                //如果父控件给你的值,满意的话,那就预测量结束
                                                goodMeasure = true;
                                            } else {
                                                //如果不满意,就改变baseSize大小
                                                baseSize = (baseSize+desiredWindowWidth)/2;
                                                //然后进行第二次测量
                                                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                                                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                                                    //满意,就结束
                                                    goodMeasure = true;
                                                }
                                            }
                                        }
                                   }
                               --> if (!goodMeasure) {
                                      //如果还不满意,直接给自己的最大值,
                                      childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                                      childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                                      //然后第三次测量
                                      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                                      //如果我给子view的大小,我的父控件不允许,那就还需要再测量
                                      if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                                          //true表示还需要测量
                                          windowSizeMayChange = true;
                                       }                                     
                                   }
                           //2、布局窗口,了解下即可
                           --> relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                               --> mWindowSession.relayout
                                   //最后还是调用的是WMS的relayoutWindow
                                   --> int res = mService.relayoutWindow@Session
                           //3、控制树测量
                           --> performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                               --> mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                                   --> onMeasure(widthMeasureSpec, heightMeasureSpec);
                                       --> setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
                                           //标志位。如果我们重写onMeasure(),没有调用setMeasuredDimension()这个方法,下面会抛一个异常
                                           --> mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
                                   -->  if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                                           throw new IllegalStateException("View with id " + getId() + ": "
                                           + getClass().getName() + "#onMeasure() did not set the"
                                           + " measured dimension by calling"
                                           + " setMeasuredDimension()");
                                         }
                           //4、布局
                           --> performLayout(lp, mWidth, mHeight);
                               --> host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                                   --> boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
                                       //这里进行赋值。所以getHeight() = mBottom - mTop,要在layout之后获取才有值。
                                       --> mLeft = left;
                                           mTop = top;
                                           mRight = right;
                                           mBottom = bottom;
                                       //view没有实现,具体的容器才会实现这个方法。
                                   --> onLayout(changed, l, t, r, b);
                                       //再交由子view去layout
                                       -->child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);

                           //5、绘制
                           --> performDraw();
                               --> boolean canUseAsync = draw(fullRedrawNeeded);
                                   //这个键盘输入时,被软键盘顶上去的距离
                                   --> scrollToRectOrFocus(null, false);
                                   //下面就是绘制了,有两种绘制方式:硬件加速绘制、软件绘制
                                   --> if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
                                           //如果开了硬件加速,并且支持硬件加速
                                           mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
                                       } else {
                                           //软件绘制
                                           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty, surfaceInsets)) {
                                               return false;
                                           }
                                              //滚动画布,就是上面说的被软键盘顶上去的距离
                                              --> canvas.translate(-xoff, -yoff);
                                              --> mView.draw(canvas);
                                                  //绘制背景色
                                                  --> drawBackground(canvas);
                                                  //如果横竖向都没有渐变色
                                                  --> if (!verticalEdges && !horizontalEdges) {
                                                          // Step 3, draw the content
                                                          onDraw(canvas);
                                                          // Step 4, draw the children
                                                          dispatchDraw(canvas);
                                                          drawAutofilledHighlight(canvas);
                                                          // Overlay is part of the content and draws beneath Foreground
                                                          if (mOverlay != null && !mOverlay.isEmpty()) {
                                                              mOverlay.getOverlayView().dispatchDraw(canvas);
                                                          }
                                                          // 绘制前景色
                                                          onDrawForeground(canvas);
                                                          // Step 7, draw the default focus highlight
                                                          drawDefaultFocusHighlight(canvas);
                                                          return;
                                                       }
                                       }
                    }
                }
    //将窗口添加到WMS上面  WindowManagerService
--> res = mWindowSession.addToDisplayAsUser()
    //事件处理
--> mInputEventReceiver = new WindowInputEventReceiver(inputChannel,Looper.myLooper());
    //将ViewRootImpl设置为view的父容器,ViewRootImpl就是控件树的根部
--> view.assignParent(this);
    --> if (mParent == null) {
            mParent = parent;
        } else if (parent == null) {
            mParent = null;
        } else {
            throw new RuntimeException("view " + this + " being added, but"
                    + " it already has a parent");
        }

view在绘制的时候,需要去管理自己的padding
父容器在分配空间时,给子view分配的大小是子view的宽高+子view设置的margin值。

3、ViewRootImpl 构造方法

    // 拿到创建它的线程,MainThread --- 默认
--> mThread = Thread.currentThread(); 
    // 脏区域:收集哪些区域需要修改
--> mDirty = new Rect(); 
    // 窗口的位置和尺寸
--> mmWinFrame = new Rect();
    // 保存当前窗口的一些信息
--> mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);
        // 参数mWindowSession,相当于是对wms的一个封装,相当于代理
    --> mWindowSession = WindowManagerGlobal.getWindowSession()
        --> IWindowManager windowManager = getWindowManagerService();
        --> sWindowSession = windowManager.openSession(
                new IWindowSessionCallback.Stub() {
                    @Override
                    public void onAnimatorScaleChanged(float scale) {
                         ValueAnimator.setDurationScale(scale);
                    }
            });
            --> return new Session(this, callback);

4、总结

View绘制流程图.jpg

layoutRequested?什么时候是true呢?调用requestLayout()为true;invalidate()走的是false

面试题:

UI 刷新只能在主线程进行吗?

不是,哪个线程创建了ViewRootImpl就在哪个线程创建ui。
原因看代码。

两种刷新方式:

view.requestLayout();
//一层层往上调用,最终调到ViewRootImpl.requestLayout();
--> mParent.requestLayout();@View
    --> checkThread();@ViewRootImpl
        //如果当前线程不等于mThread,则抛出异常。mThread是在ViewRootImpl的构造方法中赋值。
        --> if (mThread != Thread.currentThread()) {
               throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
            }
view.invalidate();
--> invalidate(true);
    --> invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
        //p就是viewParent
        --> p.invalidateChild(this, damage);
            //一层层往上调用,最终调到ViewRootImpl.requestLayout();
            --> parent = parent.invalidateChildInParent(location, dirty);
                //同样是需要去检测当前线程
                --> checkThread();@ViewRootImpl

如何实现在子线程刷新Ui?

  1. 在ViewRootImpl 创建之前调用

  2. 在需要刷新Ui的子线程 创建ViewRootImpl
    1.1 AndroidManifest.xml中添加权限

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    

    1.2 6.0之后的版本上还需要用户主动的允许权限申请。而为了让系统弹出提示用户允许权限申请操作的界面

    if (Build.VERSION.SDK_INT >= 23) {
        if (!canDrawOverlays(this)) {
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivityForResult(intent, 1);
        } else {
            childThreadChangeUI()
        }
    }
    

    1.3 子线程创建悬浮窗

    private fun childThreadChangeUI() {
        object : Thread() {
            @RequiresApi(api = Build.VERSION_CODES.R)
            override fun run() {
                Looper.prepare()
                val wm = applicationContext.getSystemService(WINDOW_SERVICE) as WindowManager
                val view: View = View.inflate(this@Main2Activity, R.layout.item, null)
                val tv: TextView = view.findViewById(R.id.tv)
                val params = WindowManager.LayoutParams()
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
                // 设置不拦截焦点
                params.flags =
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                params.width = (60 * resources.displayMetrics.density).toInt()
                params.height = (60 * resources.displayMetrics.density).toInt()
                params.gravity = Gravity.LEFT or Gravity.TOP // 且设置坐标系 左上角
                params.format = PixelFormat.TRANSPARENT
                val width = wm.defaultDisplay.width
                val height = wm.defaultDisplay.height
                params.y = height / 2 - params.height / 2
                wm.addView(view, params)
                view.setOnTouchListener(object : View.OnTouchListener {
                    private var y = 0
                    private var x = 0
                    override fun onTouch(v: View?, event: MotionEvent): Boolean {
                        when (event.action) {
                            MotionEvent.ACTION_DOWN -> {
                                x = event.rawX.toInt()
                                y = event.rawY.toInt()
                            }
                            MotionEvent.ACTION_MOVE -> {
                                val minX = (event.rawX - x).toInt()
                                val minY = (event.rawY - y).toInt()
                                params.x =
                                    (width - params.width).coerceAtMost(0.coerceAtLeast(minX + params.x))
                                params.y =
                                    (height - params.height).coerceAtMost(0.coerceAtLeast(minY + params.y))
                                wm.updateViewLayout(view, params)
                                x = event.rawX.toInt()
                                y = event.rawY.toInt()
                            }
                            MotionEvent.ACTION_UP -> if (params.x > 0 && params.x < width - params.width) {
                                val x = params.x
                                if (x > (width - params.width) / 2) {
                                    params.x = width - params.width
                                } else {
                                    params.x = 0
                                }
                                wm.updateViewLayout(view, params)
                            } else if (params.x == 0 || params.x == width - params.width) {
                                Toast.makeText(this@Main2Activity, "被点击了", Toast.LENGTH_SHORT)
                                    .show()
                                tv.text = "更改了item的TextView值"
                            }
                        }
                        return true
                    }
                })
                Looper.loop()
            }
        }.start()
    }
    
上一篇 下一篇

猜你喜欢

热点阅读