美文网首页
View的绘制流程

View的绘制流程

作者: 壹元伍角叁分 | 来源:发表于2021-09-18 21:53 被阅读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()
      }
      

    相关文章

      网友评论

          本文标题:View的绘制流程

          本文链接:https://www.haomeiwen.com/subject/rxgygltx.html