View的繪制流程

1.自定義View

自定義View可以分為三個流程:測量、布局、繪制 分別對應(yīng)著onMeasure、onLayout、onDraw方法。

自定義View可以分為兩種類型:
1.自定義ViewGroup :主要是onMeasure()、onLayout()測量和布局方法。
2.自定義View:主要是onMeasure()、onDarw()測量和繪制方法。

  • 在ViewGroup和View中都使用了測量onMeasure()方法,接下來通過一個寫一個自定義一個瀑布流ViewGroup學習onMeasure()是怎么測量的。

2.onMeasure()測量


2.1 MeasureSpec

  • 首先要搞清楚,我們?yōu)槭裁匆獪y量?
    因為在我們的xml布局文件中,我們設(shè)置width和height時會使用match_parent或warp_content來設(shè)置,測量的方法就是要將之變成具體的值,如250dp等。

  • MeasureSpec的基本知識
    每一個ViewGroup和View都會有MeasureSpec。MeasureSpec有32位字節(jié)組成,前兩位:放三個模式,后30位:放控件的大小。
    每個子View的MeasureSpec:由父ViewGroup的MeasureSpec和子view的LayoutParams確定。在getChildMeasureSpec()方法中可以查看。
    MeasureSpec:exactly、at_most、unspecified
    LayoutParams: 100dp、match_parent、warp_content

以下是為每個子View確定MeasureSpec的方法代碼:

如果父View的MeasureSpec模式是exactly,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式exactly,大小為父view的默認大小;子View是warp_content時,模式是at_most,大小為父view的默認大小。

如果父View的MeasureSpec模式是at_most,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式at_most,大小為父view的默認大小;子View是warp_content時,模式是at_most,大小為父view的默認大小。

如果父View的MeasureSpec模式是upspecified,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式upspecified,大小為父view的默認大小;子View是warp_content時,模式是upspecified,大小為父view的默認大小。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      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) {
      // Parent has imposed an exact size on us
      case MeasureSpec.EXACTLY:
          if (childDimension >= 0) {
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size. So be it.
              resultSize = size;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;

      // Parent has imposed a maximum size on us
      case MeasureSpec.AT_MOST:
          if (childDimension >= 0) {
              // Child wants a specific size... so be it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size, but our size is not fixed.
              // Constrain child to not be bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;

      // Parent asked to see how big we want to be
      case MeasureSpec.UNSPECIFIED:
          if (childDimension >= 0) {
              // Child wants a specific size... let him have it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size... find out how big it should
              // be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size.... find out how
              // big it should be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          }
          break;
      }
      //noinspection ResourceType
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }

2.2 onMeasure()測量的通用流程:
不同ViewGroup由于子View的顯示布局樣式不同,所以代碼都可能不一樣。但是它們在測量時會有一個基本的通用流程:

  1. 遍歷子View為其設(shè)置MeasureSpec并調(diào)用子view的measure()方法:這一步通過getChildMeasureSpec()方法,也可以使用measureChildMargins()方法完成。
  2. 確定自定義view的大?。和ㄟ^自身的measureSpec的模式和子view的所需大小,確定自定義view的大小。如viewGroup本身的模式是exactly則不需要理會子view所需的大小。這種模式關(guān)系來確定大小的關(guān)系可以自己定義,也可以有resolveSizeAndState()方法來確定。最后調(diào)用setMeasureDimension()來確定自定義view的大小。

onMeasure()通用流程的代碼:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      //怎么測孩子呢?
      for (i in 0 until childCount) {
          val childView = getChildAt(i)
          //獲得子View的LayoutParams來確定子View的measureSpec
          //可用替代measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
          var childLP = childView.layoutParams
          //讓ViewGroup的MeasureSpec和子View的LayoutParams,來確定子view的MeasureSpec
          val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
          val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
          childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
          }

      //自己定義不同模式的大小,也可以調(diào)用resolveSizeAndState()方法定義大小。
      val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
      val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
      setMeasuredDimension(realWidth, realHeight)
}

2.3 舉例瀑布流onMeasure()的測量:
布局分析:子View得換行:我們要先根據(jù)ViewGroup里MeasureSpec拿到默認的大小(一般都是上一個父View的最大值),然后跟子View所使用的寬度,如果比ViewGroup的默認大小要大,則換行。每一行的view都需要記錄下來,以便在布局中確定位置。同時每一行的最大高度也需要記錄下來,以便在布局中確定位置。同時也把每一行的最大寬度記錄下來。下面是瀑布流布局的OnMeasure()方法代碼

private val mVerticalSpacing = 0
  private val mHorizontalSpacing = 0
  private val allLines: MutableList<List<View?>> = ArrayList()
  private val lineHeights: MutableList<Int> = ArrayList()
  private var lineViews: MutableList<View> = ArrayList()

  fun clearList(){
      allLines.clear()
      lineHeights.clear()
      lineViews.clear()
  }

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      clearList()
      val childCount = childCount
      //拿到ViewGroup的默認的寬高
      val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
      val selHeight = MeasureSpec.getSize(heightMeasureSpec)
      var parentNeedHeight = 0
      var parentNeedWidth = 0
      var lineWidthUsed = 0
      var lineHeight = 0 //一行的高度

      //怎么測孩子呢?
      for (i in 0 until childCount) {
          val childView = getChildAt(i)
          //獲得子View的LayoutParams來確定子View的measureSpec
          //measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
          var childLP = childView.layoutParams
          //讓ViewGroup的MeasureSpec和子View的LayoutParams,來確定子view的MeasureSpec
          val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
          val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
          childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
          //獲取子view的測量寬高
          val childMeasureWidth = childView.measuredWidth
          val childMeasureHeight = childView.measuredHeight
          //這個時候需要換行
          if (lineWidthUsed + childMeasureWidth > selfWidth) {
              allLines.add(lineViews)
              lineHeights.add(lineHeight)
              parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed) + mHorizontalSpacing
              parentNeedHeight = parentNeedHeight + lineHeight
              lineViews = ArrayList()
              lineWidthUsed = 0
              lineHeight = 0
          }
          //記錄每一行的view
          lineViews.add(childView)
          lineWidthUsed = lineWidthUsed + childMeasureWidth  //每行已經(jīng)添加的寬度值
          lineHeight = Math.max(lineHeight, childMeasureHeight)
          if (i == childCount - 1) {
              allLines.add(lineViews)
              lineHeights.add(lineHeight)
              parentNeedWidth = Math.max(parentNeedHeight, lineWidthUsed)
              parentNeedHeight = parentNeedHeight + lineHeight
          }

      }
      //再測量自己,確定自己得大小
      val widthMode = MeasureSpec.getMode(widthMeasureSpec)
      val heightMode = MeasureSpec.getMode(heightMeasureSpec)
      val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
      val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
      setMeasuredDimension(realWidth, realHeight)
  }

3. onLayout()布局方法:

通過上面的onMeasure()方法,已經(jīng)確定了ViewGroup的大小。這時通過調(diào)用子view的.layout(l,t,r,b)來確定每個子view的位置。


3.1 getLeft()、getX()、getRawX()的區(qū)別:
getLeft():是控件左邊到手機屏幕坐標系的左邊的位置。
getX():是手勢點擊的點,到所屬控件的里面左邊位置。
getRawX():是手勢點擊的點,到手機屏幕的左邊位置。


3.2 getWidth()和getMeasureWidth()的區(qū)別
getWidth()和getHeight()是在onLayout()方法執(zhí)行完才有效。
getMeasureWidth()和getMeasureHeight()在onMeasure()就有效。


3.3 例子瀑布流的onLayout()分析:
布局分析:在上面的onMeasure()方法中,我們已經(jīng)記錄了每行都有哪些view,因此我們只要計算每個子view的左上坐標,然后通過view.getMeasureWidth和view.getMeasureHeight的被測量過子view的寬高以此來確定四個點的位置。以下是代碼。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
      //分為幾行
      val lineCount = allLines.size
      var curL = paddingLeft
      var curT = paddingTop
      for (i in 0 until lineCount) {
          val lineView = allLines[i]
          val lineHeight = lineHeights[i]
          for (j in lineView.indices) {
              val view = lineView[j]
              val left = curL
              val top = curT
              val right = left + view!!.measuredWidth
              val bottom = top + view!!.measuredHeight
              view.layout(left, top, right, bottom)
              curL = right + mHorizontalSpacing
          }
          curT = curT + lineHeight + mVerticalSpacing
          curL = paddingLeft
      }
  }

寫在最后:

這里寫的一個FlowLayout只是用作學習自定義ViewGroup如何測量和布局的一個簡單例子,由于時間關(guān)系只寫出一個大概。不過現(xiàn)在我們要用到流布局一般會使用recyclerView+FlexboxLayoutManager來寫既方便又快速,這里有篇文章可以參考:RecyclerView之使用FlexboxLayoutManager

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容