Flutter框架分析(四)-RenderObject

1. 前言

Flutter中,RenderObject的主要職責是佈局和繪製。經過上篇文章介紹的Element TreeFlutter Framework會生成一棵RenderObject Tree. 其主要功能以下:html

  • 佈局,從RenderBox開始,對RenderObject Tree從上至下進行佈局。
  • 繪製,經過Canvas對象,RenderObject能夠繪製自身以及其在RenderObject Tree中的子節點。
  • 點擊測試,RenderObject從上至下傳遞點擊事件,並經過其位置和behavior來控制是否響應點擊事件。

RenderObject Tree是底層的佈局和繪製系統。大多數Flutter開發者並不須要直接和RenderObject Tree交互,而是使用Widget,而後Flutter Framework會自動構建RenderObject Tree
RenderObject擁有一個parent和一個ParentData插槽(Slot),所謂插槽,就是指預留的一個接口或位置,這個接口和位置是由其它對象來接入或佔據的,這個接口或位置在軟件中一般用預留變量來表示,而ParentData正是一個預留變量,它正是由parent來賦值的,parent一般會經過子RenderObjectParentData存儲一些和子元素相關的數據,如在Stack佈局中,RenderStack就會將子元素的偏移數據存儲在子元素的ParentData中(具體能夠查看Positioned實現)。ParentData相關原理會在《Flutter框架分析(六)- Parent Data》一文中詳述,感興趣的讀者能夠閱讀該文。git

2. RenderObject分類

image.png

如上圖所示,RenderObject主要分爲四類:markdown

  • RenderView

RenderView是整個RenderObject Tree的根節點,表明了整個輸出界面。架構

  • RenderAbstractViewport

RenderAbstractViewport是一類接口,此類接口爲只展現其部份內容的RenderObject設計。app

  • RenderSliver

RenderSliver是全部實現了滑動效果的RenderObject基類,其經常使用子類有RenderSliverSingleBoxAdapter等。框架

  • RenderBox

RenderBox是一個採用2D笛卡爾座標系的RenderObject的基類,通常的RenderOBject都是繼承自RenderBox,例如RenderStack等,它也是通常自定義RenderObject的基類。ide

3. 核心流程

RenderObject主要負責佈局,繪製,及命中測試,下面會對這幾個核心流程分別進行講解。函數

  • 佈局

佈局對應的函數是layout,該函數主要做用是經過上級節點傳過來的ConstraintsparentUsesSize等控制參數,對本節點和其子節點進行佈局。Constraints是對於節點佈局的約束,其原則是,Constraints向下,Sizes向上,父節點設置本節點的位置。即:oop

  1. 一個Widget從它的父節點獲取Constraints,並將其傳遞給子節點。
  2. Widget對其子節點進行佈局。
  3. 最終,該節點告訴其父節點它的Sizes

在接下來的文章中,咱們將對該流程進行詳細介紹,當前咱們只須要記住該原則。佈局

當本節點的佈局依賴於其子節點的佈局時,parentUsesSize的值是true,此時,子節點被標記爲須要佈局時,本節點也將被標記爲須要佈局。這樣當下一幀繪製時本節點和子節點都將被從新佈局。反之,若是parentUsesSize的值是false,子節點被從新佈局時不須要通知本節點。

RenderObject的子類不該該直接重寫RenderObjectlayout函數,而是重寫performResizeperformLayout函數,這兩個函數纔是真正負責具體佈局的函數。

RenderObject中layout函數的源碼以下:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
//1. 根據relayoutBoundary判斷是否須要從新佈局
  RenderObject relayoutBoundary;
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }
  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
    return;
  }
  _constraints = constraints;
//2. 更新子節點的relayout boundary 
  if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
    // The local relayout boundary has changed, must notify children in case
    // they also need updating. Otherwise, they will be confused about what
    // their actual relayout boundary is later.
    visitChildren(_cleanChildRelayoutBoundary);
  }
  _relayoutBoundary = relayoutBoundary;
//3. 從新計算大小,從新佈局
  if (sizedByParent) {
    try {
      performResize();
    } catch (e, stack) {
      _debugReportException('performResize', e, stack);
    }
  }
  try {
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  _needsLayout = false;
  markNeedsPaint();
}
複製代碼

從源碼能夠看到,relayoutBoundarylayout函數中一個重要參數。當一個組件的大小被改變時,其parent的大小可能也會被影響,所以須要通知其父節點。若是這樣迭代上去,須要通知整棵RenderObject Tree從新佈局,必然會影響佈局效率。所以,Flutter經過relayoutBoundaryRenderObject Tree分段,若是遇到了relayoutBoundary,則不去通知其父節點從新佈局,由於其大小不會影響父節點的大小。這樣就只須要對RenderObject Tree中的一段從新佈局,提升了佈局效率。關於relayoutBoundary將在以後的文章中詳細講解,目前只須要了解relayoutBoundary會將RenderObject Tree分段,提升佈局效率。

  • 繪製

繪製對應的函數是paint,其主要做用是將本RenderObject和子RenderObject繪製在Canvas上。RenderObject的子類應該重寫這個函數,在該函數中添加繪製的邏輯。

RenderObject的子類RenderFlexpaint函數源碼以下:

void paint(PaintingContext context, Offset offset) {
//1. 未溢出,直接繪製
  if (!_hasOverflow) {
    defaultPaint(context, offset);
    return;
  }

//2. 空的,不須要繪製
  // There's no point in drawing the children if we're empty.
  if (size.isEmpty)
    return;

//3. 根據clipBehavior判斷是否須要對溢出邊界部分進行裁剪
  if (clipBehavior == Clip.none) {
    defaultPaint(context, offset);
  } else {
    // We have overflow and the clipBehavior isn't none. Clip it.
    context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint, clipBehavior: clipBehavior);
  }

//4. 繪製溢出錯誤提示
  assert(() {
    // Only set this if it's null to save work. It gets reset to null if the
    // _direction changes.
    final List<DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[
      ErrorDescription(
        'The overflowing $runtimeType has an orientation of $_direction.'
      ),
      ErrorDescription(
        'The edge of the $runtimeType that is overflowing has been marked '
        'in the rendering with a yellow and black striped pattern. This is '
        'usually caused by the contents being too big for the $runtimeType.'
      ),
      ErrorHint(
        'Consider applying a flex factor (e.g. using an Expanded widget) to '
        'force the children of the $runtimeType to fit within the available '
        'space instead of being sized to their natural size.'
      ),
      ErrorHint(
        'This is considered an error condition because it indicates that there '
        'is content that cannot be seen. If the content is legitimately bigger '
        'than the available space, consider clipping it with a ClipRect widget '
        'before putting it in the flex, or using a scrollable container rather '
        'than a Flex, like a ListView.'
      ),
    ];

    // Simulate a child rect that overflows by the right amount. This child
    // rect is never used for drawing, just for determining the overflow
    // location and amount.
    Rect overflowChildRect;
    switch (_direction) {
      case Axis.horizontal:
        overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
        break;
      case Axis.vertical:
        overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
        break;
    }
    paintOverflowIndicator(context, offset, Offset.zero & size, overflowChildRect, overflowHints: debugOverflowHints);
    return true;
  }());
}
複製代碼

這部分代碼邏輯爲,先判斷是否溢出,沒有溢出則調用defaultPaint完成繪製,再看是否爲空,size是空的話直接返回,最後繪製溢出信息。

其中defaultPaint的源碼以下:

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData as ParentDataType;
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}
複製代碼

可見defaultPaint會調用paintChild繪製子節點,而若是子節點還有子節點,則paintChild最終又會調用到其paint而後調用到defaultPaint,從而造成循環遞歸調用,繪製整棵RenderObject Tree

  • 命中測試

命中測試是爲了判斷某個組件是否須要響應一個點擊事件,其入口是RenderObject Tree的根節點RenderViewhitTest函數。下面是該函數的源碼:

bool hitTest(HitTestResult result, { Offset position }) {
  if (child != null)
    child.hitTest(BoxHitTestResult.wrap(result), position: position);
  result.add(HitTestEntry(this));
  return true;
}
複製代碼

RenderView的構造函數能夠看出,childRenderBox類,所以咱們再看RenderBoxhitTest函數。

bool hitTest(BoxHitTestResult result, { @required Offset position }) {
  if (_size.contains(position)) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}
複製代碼

代碼邏輯很簡單,若是點擊事件位置處於RenderObject以內,若是在其內,而且hitTestSelf或者hitTestChildren返回true,則表示該RenderObject經過了命中測試,須要響應事件,此時須要將被點擊的RenderObject加入BoxHitTestResult列表,同時點擊事件再也不向下傳遞。不然認爲沒有經過命中測試,事件繼續向下傳遞。其中,hitTestSelf函數表示本節點是否經過命中測試,hitTestChildren表示子節點是否經過命中測試。

4. 核心函數

RenderObject的核心函數有不少,難以一一列舉,在覈心流程中已經詳細講解了RenderObject三個核心函數。爲了便於理解各個核心函數的做用,這裏將RenderObject的核心函數和Android View的核心函數進行比較。如下是比較的表格。

做用 Flutter RenderObject Android View
繪製 paint() draw()/onDraw()
佈局 performLayout()/layout() measure()/onMeasure(), layout()/onLayout()
佈局約束 Constraints MeasureSpec
佈局協議1 performLayout() 的 Constraints 參數表示父節點對子節點的佈局限制 measure() 的兩個參數表示父節點對子節點的佈局限制
佈局協議2 performLayout() 應調用各子節點的 layout() onLayout() 應調用各子節點的 layout()
佈局參數 parentData mLayoutParams
請求佈局 markNeedsLayout() requestLayout()
請求繪製 markNeedsPaint() invalidate()
添加 child adoptChild() addView()
移除 child dropChild() removeView()
關聯到窗口/樹 attach() onAttachedToWindow()
從窗口/樹取消關聯 detach() onDetachedFromWindow()
獲取 parent parent getParent()
觸摸事件 hitTest() onTouch()
用戶輸入事件 handleEvent() onKey()
旋轉事件 rotate() onConfigurationChanged()

可見,RenderObjectAndroid View有不少函數是對應起來的,RenderObject相對於將Android View中的佈局渲染等功能單獨拆了出來,簡化了View的邏輯。

5. 小結

本文主要介紹了RenderObject相關知識,重點介紹了其分類,核心流程,和核心函數。重點以下:

  • RenderObject主要負責繪製,佈局,命中測試等。
  • RenderObject佈局的原則是,Constraints向下,Sizes向上,父節點設置本節點的位置。
  • RenderView是整個RenderObject Tree的根節點,其child是一個RenderBox類型的RenderObject

6. 參考文檔

Flutter實戰
Flutter RenderObject 淺析

7. 相關文章

Flutter框架分析(一)--架構總覽
Flutter框架分析(二)-- Widget
Flutter框架分析(三)-- Element
Flutter框架分析(五)-Widget,Element,RenderObject樹
Flutter框架分析(六)-Constraint
Flutter框架分析(七)-relayoutBoundary
Flutter框架分析(八)-Platform Channel
Flutter框架分析- Parent Data
Flutter框架分析 -InheritedWidget

相關文章
相關標籤/搜索