1、Flutter 之圖像繪製原理canvas
2、Widget、Element、RenderObject性能優化
4、build 流程分析async
案例描述 根據點擊的位置彈一個提示框,例如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 將須要實時更新的圖層隔離成一個單獨的圖層,在重繪時不影響其餘圖層的繪製