9、Flutter 小實踐

1、Flutter 之圖像繪製原理canvas

2、Widget、Element、RenderObject性能優化

3、Flutter UI 更新流程bash

4、build 流程分析async

5、layout 流程分析ide

6、Paint 繪製(1)函數

7、Paint 繪製(2)工具

8、composite 流程分析佈局

一、自定義佈局

案例描述 根據點擊的位置彈一個提示框,例如post

提示框的位置主要有四種狀況:性能

以上圖的的圓點(即屏幕的中間點)爲參考位置

水平方向

顯示在參考物的左邊:參考物的中間點位置位於圓點的右側,即提示框顯示在參考物的左邊

顯示在參考物的右邊:參考物的中間點位置位於圓點的左側,即提示框顯示在參考物的右邊

垂直方向

顯示在參考物的下方:參考物的中間點位置位於圓點的上方,即提示框顯示在參考物的下方

顯示在參考物的上方:參考物的中間點位置位於圓點的下方,即提示框顯示在參考物的上方

自定義提示框佈局步驟 (1)點擊事件觸發時獲取參考物的位置

showTip(BuildContext context) async 
  //獲取點擊源
    final RenderBox box = customPopupKey.currentContext.findRenderObject();
  
    final Offset target = box.localToGlobal(box.size.center(Offset.zero));
    final RenderBox overlay = Overlay.of(context).context.findRenderObject();
  }
複製代碼

(2)判斷 提示框的位置,居左,居右,居上,居下

PopupDirection popupDirection; 

  if (preferVertical) {
    // 是不是垂直方向顯示
    if (target.dy > overlay.size.center(Offset.zero).dy) {
      popupDirection = PopupDirection.up;
    } else {
      popupDirection = PopupDirection.down;
    }
  } else {
    // 水平方向顯示
    if (target.dx < overlay.size.center(Offset.zero).dx) {
      popupDirection = PopupDirection.right;
    } else {
      popupDirection = PopupDirection.left;
    }
}
複製代碼

(3)重寫 performLayout 函數 在performLayout 函數中返回對應節點的大小設置,返回該 renderObejct 的offset, 即位置信息, 在該例子中使用了 flutter 提供的 CustomSingleChildLayout 進行自定義佈局

delegate: _CustomSingleChildDelegate(
      target: target,
      targetBoxSize: box.size,
      offsetDistance: offsetDistance,
      preferVertical: preferVertical,
      popUpDirection: popupDirection),
  child: Container(
    padding: contentPadding,
    decoration: ShapeDecoration(
      color: bgColor,
      shape: CustomShapeBorder(
          popupDirection: popupDirection,
          targetCenter: target,
          borderRadius: _defaultBorderRadius,
          arrowBaseWidth: arrowBaseWidth,
          arrowTipDistance: arrowTipDistance,
          borderColor: borderColor,
          borderWidth: borderWidth),
    ),
    child: Text(
      message,
      style: TextStyle(
          fontSize: Adapt.px(textSize),
          color: textColor,
          decoration: TextDecoration.none),
    ),
  ),
)
複製代碼

(4)CustomSingleChildLayout 對應的 renderObject 重寫了 performLayout方法

void performLayout() {
  size = _getSize(constraints);
  if (child != null) {
    final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints);
    assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
    child.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child.size);
  }
}
複製代碼

其設置節點的 offset 是經過調用 delegate.getPositionForChild 方法,這個delegate 是由調用方自定義的,如 demo 中傳入的 _CustomSingleChildDelegate,在這個類總,咱們重寫了 getPositionForChild 方法,根據本身的需求完成節點位置的佈局

(5)_CustomSingleChildDelegate

class _CustomSingleChildDelegate extends SingleChildLayoutDelegate {

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return customPositionDependentBox(
      size: size,
      childSize: childSize,
      target: target,
      offsetDistance: offsetDistance,
      preferVertical: preferVertical,
    );
  }

  Offset customPositionDependentBox({
    @required Size size,
    @required Size childSize,
    @required Offset target,
    @required bool preferVertical,
    double offsetDistance = 0.0,
    double margin = 30.0,
  }) {
    // VERTICAL DIRECTION
    double x;
    double y;

    // 在點擊位置的垂直方向顯示
    if (preferVertical) {
      final bool fitsBelow = popUpDirection == PopupDirection.down;

      if (fitsBelow) {
        // 顯示在底部
        y = min(target.dy + 5, size.height - margin);
      } else {
        // 顯示在上方
        y = max(target.dy - childSize.height - 5, margin);
      }

      // 水平方向處理
      if (size.width - margin * 2.0 < childSize.width) {
        x = (size.width - childSize.width) / 2.0;
      } else {
        final double normalizedTargetX =
            target.dx.clamp(margin, size.width - margin);
        final double edge = margin + childSize.width / 2.0;
        if (normalizedTargetX < edge) {
          x = margin;
        } else if (normalizedTargetX > size.width - edge) {
          x = size.width - margin - childSize.width;
        } else {
          x = normalizedTargetX - childSize.width / 2.0;
        }
      }
    } else {
      // 在觸發源的水平方向顯示處理
      final bool fitsLeft = popUpDirection == PopupDirection.left;
      if (fitsLeft) {
        // 左邊
        x = min(target.dx - childSize.width - targetBoxSize.width / 2 - 10,
            size.width - margin);
      } else {
        // 右邊
        x = max(target.dx + targetBoxSize.width / 2 + 10, margin);
      }
      // 水平顯示時垂直方向的處理
      if (size.height - margin * 2.0 < childSize.height) {
        y = (size.height - childSize.height) / 2.0;
      } else {
        final double normalizedTargetY =
            target.dy.clamp(margin, size.height - margin);
        final double edge = margin + childSize.height / 2.0;
        if (normalizedTargetY < edge) {
          y = margin;
        } else if (normalizedTargetY > size.height - edge) {
          y = size.height - margin - childSize.height;
        } else {
          y = normalizedTargetY - childSize.height / 2.0;
        }
      }
    }
    return Offset(x, y);
  }
}
複製代碼

二、自定義繪製

在上述例子中,咱們完成了自定義佈局,可是該例子的提示框是帶有一個三角形,並且三角形的尖角方向可能隨着提示框的位置而不一樣,多是向上,向下,也有多是向左,向右,這個三角形是能夠根據需求自定義繪製的。

(1) 經過從新描繪提示框的邊框實現

Container(
  padding: contentPadding,
  decoration: ShapeDecoration(
    color: bgColor,
    shape: CustomShapeBorder(
        popupDirection: popupDirection,
        targetCenter: target,
        borderRadius: _defaultBorderRadius,
        arrowBaseWidth: arrowBaseWidth,
        arrowTipDistance: arrowTipDistance,
        borderColor: borderColor,
        borderWidth: borderWidth),
  ),
  child: Text(
    message,
    style: TextStyle(
        fontSize: Adapt.px(textSize),
        color: textColor,
        decoration: TextDecoration.none),
  ),
)
複製代碼

(2) 自定義提示框的邊框

/**
 * 繪製提示框邊框,提示框尖角
 */
class CustomShapeBorder extends ShapeBorder {
  final Offset targetCenter;
  final double arrowBaseWidth;
  final double arrowTipDistance;
  final double borderRadius;
  final Color borderColor;
  final double borderWidth;
  final PopupDirection popupDirection;

  CustomShapeBorder(
      {this.popupDirection,
      this.targetCenter,
      this.borderRadius,
      this.arrowBaseWidth,
      this.arrowTipDistance,
      this.borderColor,
      this.borderWidth});

  @override
  EdgeInsetsGeometry get dimensions => new EdgeInsets.all(10.0);

  @override
  Path getInnerPath(Rect rect, {TextDirection textDirection}) {
    return new Path()
      ..fillType = PathFillType.evenOdd
      ..addPath(getOuterPath(rect), Offset.zero);
  }

  @override
  //繪製邊框
  Path getOuterPath(Rect rect, {TextDirection textDirection}) {
    double topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius;

    Path _getLeftTopPath(Rect rect) {
      return new Path()
        ..moveTo(rect.left, rect.bottom - bottomLeftRadius)
        ..lineTo(rect.left, rect.top + topLeftRadius)
        ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top), //繪製圓角
            radius: new Radius.circular(topLeftRadius))
        ..lineTo(rect.right - topRightRadius, rect.top)
        ..arcToPoint(Offset(rect.right, rect.top + topRightRadius), //繪製圓角
            radius: new Radius.circular(topRightRadius),
            clockwise: true);
    }

    Path _getBottomRightPath(Rect rect) {
      return new Path()
        ..moveTo(rect.left + bottomLeftRadius, rect.bottom)
        ..lineTo(rect.right - bottomRightRadius, rect.bottom)
        ..arcToPoint(Offset(rect.right, rect.bottom - bottomRightRadius),
            radius: new Radius.circular(bottomRightRadius), clockwise: false)
        ..lineTo(rect.right, rect.top + topRightRadius)
        ..arcToPoint(Offset(rect.right - topRightRadius, rect.top),
            radius: new Radius.circular(topRightRadius), clockwise: false);
    }

    topLeftRadius = borderRadius;
    topRightRadius = borderRadius;
    bottomLeftRadius = borderRadius;
    bottomRightRadius = borderRadius;

    switch (popupDirection) {
      //

      case PopupDirection.down:
        return _getBottomRightPath(rect)
          ..lineTo(
              min(
                  max(targetCenter.dx + arrowBaseWidth / 2,
                      rect.left + borderRadius + arrowBaseWidth),
                  rect.right - topRightRadius),
              rect.top)
          ..lineTo(targetCenter.dx, rect.top - arrowTipDistance) // 向下箭頭
          ..lineTo(
              max(
                  min(targetCenter.dx - arrowBaseWidth / 2,
                      rect.right - topLeftRadius - arrowBaseWidth),
                  rect.left + topLeftRadius),
              rect.top) //  // 向下箭頭

          ..lineTo(rect.left + topLeftRadius, rect.top)
          ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius),
              radius: new Radius.circular(topLeftRadius), clockwise: false)
          ..lineTo(rect.left, rect.bottom - bottomLeftRadius)
          ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom),
              radius: new Radius.circular(bottomLeftRadius), clockwise: false);

      case PopupDirection.up:
        return _getLeftTopPath(rect)
          ..lineTo(rect.right, rect.bottom - bottomRightRadius)
          ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom),
              radius: new Radius.circular(bottomRightRadius), clockwise: true)
          ..lineTo(
              min(
                  max(targetCenter.dx + arrowBaseWidth / 2,
                      rect.left + bottomLeftRadius + arrowBaseWidth),
                  rect.right - bottomRightRadius),
              rect.bottom)

          // 向上箭頭
          ..lineTo(targetCenter.dx, rect.bottom + arrowTipDistance)
          ..lineTo(
              max(
                  min(targetCenter.dx - arrowBaseWidth / 2,
                      rect.right - bottomRightRadius - arrowBaseWidth),
                  rect.left + bottomLeftRadius),
              rect.bottom)
          ..lineTo(rect.left + bottomLeftRadius, rect.bottom)
          ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius),
              radius: new Radius.circular(bottomLeftRadius), clockwise: true)
          ..lineTo(rect.left, rect.top + topLeftRadius)
          ..arcToPoint(Offset(rect.left + topLeftRadius, rect.top),
              radius: new Radius.circular(topLeftRadius), clockwise: true);

      case PopupDirection.left:
        return _getLeftTopPath(rect)
          ..lineTo(
              rect.right,
              max(
                  min(targetCenter.dy - arrowBaseWidth / 2,
                      rect.bottom - bottomRightRadius - arrowBaseWidth),
                  rect.top + topRightRadius))
          ..lineTo(rect.right + arrowTipDistance, targetCenter.dy) // 向左箭頭
          ..lineTo(
              rect.right,
              min(targetCenter.dy + arrowBaseWidth / 2,
                  rect.bottom - bottomRightRadius))
          ..lineTo(rect.right, rect.bottom - borderRadius)
          ..arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom),
              radius: new Radius.circular(bottomRightRadius), clockwise: true)
          ..lineTo(rect.left + bottomLeftRadius, rect.bottom)
          ..arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius),
              radius: new Radius.circular(bottomLeftRadius), clockwise: true);

      case PopupDirection.right:
        return _getBottomRightPath(rect)
          ..lineTo(rect.left + topLeftRadius, rect.top)
          ..arcToPoint(Offset(rect.left, rect.top + topLeftRadius),
              radius: new Radius.circular(topLeftRadius), clockwise: false)
          ..lineTo(
              rect.left,
              max(
                  min(targetCenter.dy - arrowBaseWidth / 2,
                      rect.bottom - bottomLeftRadius - arrowBaseWidth),
                  rect.top + topLeftRadius))

          // 向右箭頭
          ..lineTo(rect.left - arrowTipDistance, targetCenter.dy)
          ..lineTo(
              rect.left,
              min(targetCenter.dy + arrowBaseWidth / 2,
                  rect.bottom - bottomLeftRadius))
          ..lineTo(rect.left, rect.bottom - bottomLeftRadius)
          ..arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom),
              radius: new Radius.circular(bottomLeftRadius), clockwise: false);

      default:
        throw AssertionError(popupDirection);
    }
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
    Paint paint = new Paint()
      ..color = borderColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = borderWidth;

    canvas.drawPath(getOuterPath(rect), paint);
  }

  @override
  ShapeBorder scale(double t) {
    return new CustomShapeBorder(
        popupDirection: this.popupDirection,
        targetCenter: this.targetCenter,
        borderRadius: this.borderRadius,
        arrowBaseWidth: this.arrowBaseWidth,
        arrowTipDistance: this.arrowTipDistance,
        borderColor: this.borderColor,
        borderWidth: this.borderWidth);
  }
}
複製代碼

Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > CustomSingleChildLayout

三、性能優化

(1) devTool 工具

工欲善其事必先利其器,能夠藉助 devTool 打開 timeline 查看相關的繪製渲染情

或者在 Android Studio 能夠調出 Flutter Inspector

(2) 在main方法中設置如下屬性值

void main() {
  debugProfileBuildsEnabled = true; // 查看須要重繪的widget
  debugProfilePaintsEnabled = true; // 查看須要重繪的 renderObject
  debugPaintLayerBordersEnabled = true; // 查看須要重繪的 layer信息
  debugRepaintRainbowEnabled = true;
  runApp(MyApp());
}
複製代碼

(3) 減小 rebuild 範圍

class _MyHomePageState extends State<MyHomePage> {
  String name = '';
  int count = 0;
  @override
  void initState() {
    super.initState();
    Timer.periodic(
        Duration(milliseconds: 1000),
            (timer) => {
          setState(() {
            count++;
          })
        });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
      child: Row(
        children: <Widget>[
          Container(
            child: Row(
              children: <Widget>[Text('哈哈哈1'), Text('哈哈哈3')],
            ),
          ),
          Text('哈哈哈2${count}')
        ],
      ),
    ));
  }
}
複製代碼

優化前:

widget 每次 rebuild 都是 從根節點 Scaffold 開始遍歷,但實際在這個demo 中只會實時更新 一個 text 文本

優化後: 將須要實時更新的 widget 抽離成一個組件

class TextCom extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _TextComState();
  }
}

class _TextComState extends State<TextCom> {
  int count = 0;
  @override
  void initState() {
    super.initState();
    Timer.periodic(
        Duration(milliseconds: 1000),
        (timer) => {
              setState(() {
                count++;
              })
            });
  }

  @override
  Widget build(BuildContext context) {
    return Text('哈哈哈2${count}');
  }
}
複製代碼

從新查看 reBuild 範圍,發現縮小到這個 TextWidget 組件,以下圖中的 TextCom

(2) RepaintBoundary 將須要實時更新的圖層隔離成一個單獨的圖層,在重繪時不影響其餘圖層的繪製

相關文章
相關標籤/搜索