Flutter的绘制流程

ops/2025/2/7 14:52:31/
    index += 1;//当_dirtyElements集合中的“脏”节点还未处理完毕时,又添加了新的“脏”节点if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {//根据“脏”节点的深度进行排序_dirtyElements.sort(Element._sort);_dirtyElementsNeedsResorting = false;dirtyCount = _dirtyElements.length;//如果当前节点的深度比新加入的“脏”节点深度要深,则需要将处理坐标指向新加入的“脏”节点while (index > 0 && _dirtyElements[index - 1].dirty) {index -= 1;}}}
} finally {//清除_dirtyElements中所有节点的“脏”状态for (Element element in _dirtyElements) {element._inDirtyList = false;}_dirtyElements.clear();_scheduledFlushDirtyElements = false;_dirtyElementsNeedsResorting = null;Timeline.finishSync();
}

}


\_dirtyElements是一个集合,存储了所有标记为“脏”的节点。在对其中的“脏”节点进行处理时,需要首先对集合中的“脏”节点进行排序,其排序规则如下。● 如果“脏”节点的深度不同,则按照深度进行升序排序● 如果“脏”节点的深度相同,则会将“脏”节点放在集合的右侧,“干净”节点则在在集合的左侧。在排序完成后,就要遍历该集合,对其中的“脏”节点进行处理。在这里调用的是rebuild函数,通过该函数,会重新创建“脏”节点下的所有Widget对象,并根据新的Widget对象来判断是否需要重用Element对象。一般只要不是增删Widget,Element对象都会被重用,从而也就会重用RenderObject对象。由于Widget是一个非常轻量级的数据结构,所以在UI更新时做到了把性能损耗降到最低。如果\_dirtyElements中的“脏”节点还未处理完毕,就又新增了“脏”节点,那么这时候就会重新排序,保证\_dirtyElements集合的左侧永远是“干净”节点,右侧永远是“脏”节点。由于rebuild函数比较重要,这里就重点介绍一下该函数,在rebuild函数中会调用performRebuild函数,该函数是一个抽象函数,在其子类实现,而标记为“脏”的Element都是StatefulElement。所以就来StatefulElement或者其父类中查找performRebuild函数。

abstract class ComponentElement extends Element {

@override
void performRebuild() {
Widget built;
try {
//重新创建新的Widget对象
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
//当构建Widget对象出错时展示的默认页面,可以修改该页面来使异常界面更友好的显示
built = ErrorWidget.builder(_debugReportException(‘building $this’, e, stack));
} finally {
//清除“脏”标记
_dirty = false;
}
try {
//更新子Element对应的Widget对象
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
//当构建Widget对象出错时展示的默认页面
built = ErrorWidget.builder(_debugReportException(‘building $this’, e, stack));
_child = updateChild(null, built, slot);
}
}
}


performRebuild函数做的事很简单,就是创建新的Widget对象来替换旧的对象。上面的build函数调用的就是State类中的build函数,然后再调用Element的updateChild函数,就是更新Element对应的Widget对象。而在updateChild函数中又会调用子Element的update函数,从而调用子Element的performRebuild,然后在调用子Element的updateChild、update函数。以此类推,从而更新其所有子Element的Widget对象。![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fjuejin.cn%2F&pos_id=img-0yR7cvUa-1694154012759)最后就是调用叶子节点的updateRenderObject函数来更新RenderObject。在更新RenderObject对象时,会根据情况来对需要重新布局及重新绘制的RenderObject对象进行标记。然后等待下一次的Vsync信号时来重新布局及绘制UI。##### 2.2. 标记RenderObject:对于RenderObject对象,可以通过markNeedsLayout及markNeedsPaint来标记是否需要重新布局及重新绘制。但在当前阶段只会调用markNeedsLayout来标记需要重新布局的RenderObject对象,在下一阶段才会标记需要重新绘制的RenderObject,所以先来看markNeedsLayout函数。

void markNeedsLayout() {

//判断布局边界是否是是当前RenderObject对象
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
//标记当前RenderObject及其子RenderObject对象需要重新布局
//将当前RenderObject添加到集合中。
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
@protected
void markParentNeedsLayout() {
_needsLayout = true;
final RenderObject parent = this.parent;
if (!_doingThisLayoutWithCallback) {
//调用父类的markNeedsLayout函数
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
assert(parent == this.parent);
}


markNeedsLayout函数的代码实现很简单,就是不断遍历父RenderObject对象,从而找到布局边界的RenderObject对象,并将该RenderObject对象添加到集合\_nodesNeedingLayout中,然后在下一阶段就从该对象开始布局。在这里有个“布局边界”的概念,在Flutter中,可以给任意节点设置布局边界,即当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。在重新构建build函数及标记RenderObject完成后,就进入下一阶段,开始布局。#### 3. 布局阶段:计算Widget的大小及位置的确定。在该阶段,会确定每个组件的大小及位置,相当于Android中的onMeasure+onLayout函数所实现的功能。如果是第一次调用该函数,该阶段就会遍历所有的组件,来确定其大小及位置;否则该阶段就会遍历布局边界内的所有组件,来确定其大小及位置。当上一阶段中的buildOwner.buildScope(renderViewElement)函数执行完毕后,就会调用RendererBinding的drawFrame函数,该函数实现非常简洁。

//绘制帧
void drawFrame() {
//对指定组件及其子组件进行大小测量及位置确定
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame();
pipelineOwner.flushSemantics(); }


其中flushLayout就是进行组件的大小及位置确定,在该函数中会遍历集合\_nodesNeedingLayout并调用集合中每个对象的\_layoutWithoutResize函数。

void flushLayout() {
try {
while (_nodesNeedingLayout.isNotEmpty) {
final List dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = [];
for (RenderObject node in dirtyNodes…sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
//调用RenderObject对象的_layoutWithoutResize函数
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {…}
}


\_layoutWithoutResize函数是私有的,所以不存在重写的问题。那么就直接来看该函数。

void _layoutWithoutResize() {
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {…}
_needsLayout = false;
markNeedsPaint();
}


\_layoutWithoutResize函数很简单,就直接调用了performLayout函数。当然,RenderObject对象的size也不是随便确定的,因为在调用RenderObject的layout函数时,会传递一个继承自Constraints的对象。该对象是一个布局约束,由父传给子,子会根据该对象来决定自己的大小。##### 3.1. 标记RenderObject当大小及位置确定后,就又会对RenderObject进行一次标记,这次跟上一阶段的标记大同小异,但这次是标记可绘制的RenderObject对象,然后在后面对这些对象进行重新绘制。标记可绘制的RenderObject对象是通过markNeedsPaint函数来实现的,代码如下。

void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
//标记需要重新绘制的RenderObject对象
//需要绘制当前图层
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
//没有自己的图层,与父类共用同一图层
final RenderObject parent = this.parent;
//遍历其父RenderObject对象
parent.markNeedsPaint();
} else {
//当是RenderView时,需要自己创建新的图层
if (owner != null)
owner.requestVisualUpdate();
}
}


markNeedsPaint函数中涉及到了一个“重绘边界”的概念。在进入和走出重绘边界时,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响。当然重绘边界也可以在任何节点手动设置,但是一般不需要我们来实现,Flutter提供的控件默认会在需要设置的地方自动设置。#### 4. compositingBits阶段:重绘之前的预处理操作,检查RenderObject是否需要重绘。在组件的大小及位置确定后,就会进入当前阶段。该阶段主要是做一件事,就是将RenderObject树上新增及删除的RenderObject对象标记为“脏”,方便在下一阶段对这些RenderObject对象进行重绘。具体代码实现是在flushCompositingBits函数中,该函数在Layout阶段后立即调用。

void flushCompositingBits() {

//将RenderObject对象按照深度进行排序
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
//将RenderObject对象及其子对象标记为“脏”
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();

}


nodesNeedingCompositingBitsUpdate是一个集合,只有RenderObject对象的\_needsCompositing为true时,才会添加到该集合中。在RenderObject对象创建时,\_needsCompositing的值会根据isRepaintBoundary及alwaysNeedsCompositing来共同判断。

RenderObject() {
//isRepaintBoundary决定当前RenderObject是否与父RenderObject分开绘制,默认为false,其值在当前对象的生命周期内无法修改。也就是判断当前对象是否是绘制边界
//alwaysNeedsCompositing为true表示当前RenderObject会一直重绘,如视频播放,默认为false
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}


然后在向树中添加或者删除RenderObject对象时会调用adoptChild及dropChild函数,而这两个函数都会调用markNeedsCompositingBitsUpdate函数,也就在markNeedsCompositingBitsUpdate函数内完成了将当前对象添加到集合中的操作。

//向树中添加当前节点
@override
void adoptChild(RenderObject child) {
setupParentData(child);
markNeedsLayout();
//将当前对象的_needsCompositingBitsUpdate值标为true
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
//从树中移除当前节点
@override
void dropChild(RenderObject child) {
child._cleanRelayoutBoundary();
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
//将当前对象的_needsCompositingBitsUpdate值标为true
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
//
void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent;
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
//将当前对象或者其父对象添加到_nodesNeedingCompositingBitsUpdate集合中
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}


这样就会在调用flushCompositingBits函数时,就会调用\_updateCompositingBits函数来判断是否将这些对象及子对象标记为“脏”,然后在下一阶段进行绘制。

void _updateCompositingBits() {
//表示已经处理过,
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
//访问其子对象
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
//如果是绘制边界或者需要一直重绘
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing) {
//将当前对象标记为“脏”,
markNeedsPaint();
}
_needsCompositingBitsUpdate = false;
}


#### 5. 绘制阶段:根据Widget大小及位置来绘制UI。通过调用flushPaint函数就可以重绘已经标记的“脏”RenderObject对象及其子对象。

void flushPaint() {
try {
final List dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = [];
//根据节点深度进行排序
for (RenderObject node in dirtyNodes…sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
//当前对象是否与layer进行关联
if (node._layer.attached) {
//在图层上绘制UI
PaintingContext.repaintCompositedChild(node);
} else {
//跳过UI绘制,但当前节点为“脏”的状态不会改变
node._skippedPaintingOnLayer();
}
}
}
} finally {}
}


flushPaint函数中,每次遍历“脏”RenderObject对象时,都会进行一次排序,避免重复绘制。然后在判断当前对象是否与Layer进行关联,如果没有关联,则无法进行绘制,但不会清除“脏”标记。下面来看repaintCompositedChild函数的实现。

static void repaintCompositedChild(RenderObject child, { bool
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}

static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
//拿到Layer对象
OffsetLayer childLayer = child._layer;
if (childLayer == null) {
//创建新的Layer对象
child._layer = childLayer = OffsetLayer();
} else {
//移除Layer对象的后继节点
childLayer.removeAllChildren();
}
//创建context对象
childContext ??= PaintingContext(child._layer, child.paintBounds);
//调用paint函数开始绘制
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}


##### 5.1. Layer在该函数中主要是对Layer对象的处理,然后调用\_paintWithContext函数,在\_paintWithContext函数中就会调用paint这个函数,从而实现UI的绘制。至此,就完成了UI的绘制,下面再来看一个被忽略的对象——Layer。Layer是“图层”意思,在Flutter中是最容易被忽略但又无比重要的一个类。它非常贴近底层,可以很容易的看到调用Native方法。Layer跟其他三棵树一样,也是一棵树,有“脏”状态的标记、更新等操作。不同的是,Layer是一个双链表结构,在每个Layer对象中都会指向其前置节点与后置节点(叶子Layer的后置节点为null)。

abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
//返回父节点
@override
ContainerLayer get parent => super.parent;

//当前节点状态,为true表示当前节点是“脏”数据。需要重绘
bool _needsAddToScene = true;

//将当前节点标记为“脏”
@protected
@visibleForTesting
void markNeedsAddToScene() {
// Already marked. Short-circuit.
if (_needsAddToScene) {
return;
}
_needsAddToScene = true;
}

@protected
bool get alwaysNeedsAddToScene => false;

//这个是一个非常重要的东西,主要用于节点数据的缓存。存储当前节点的渲染数据,如果当前节点不需要更新,就直接拿存储的数据使用。
@protected
ui.EngineLayer get engineLayer => _engineLayer;

//更改当前节点的数据
@protected
set engineLayer(ui.EngineLayer value) {
_engineLayer = value;
if (parent != null && !parent.alwaysNeedsAddToScene) {
//将父节点标记需要更新的状态
parent.markNeedsAddToScene();
}
}
}
ui.EngineLayer _engineLayer;

//更新当前节点状态,如果_needsAddToScene为true,则将当前节点标记为“脏”
@protected
@visibleForTesting
void updateSubtreeNeedsAddToScene() {
_needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
}

//指向后置节点
Layer get nextSibling => _nextSibling;
Layer _nextSibling;

//指向前置节点
Layer get previousSibling => _previousSibling;
Layer _previousSibling;

//将子节点从Layer树中移除
@override
void dropChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene();
}
super.dropChild(child);
}
//将当前节点添加到Layer树中
@override
void adoptChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene();
}
super.adoptChild(child);
}

//将当前节点从Layer树中移除
@mustCallSuper
void remove() {
parent?._removeChild(this);
}

void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);

void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
//使用当前节点的缓存的数据
if (!_needsAddToScene && _engineLayer != null) {
builder.addRetained(_engineLayer);
return;
}
addToScene(builder);
//将当前节点标记为“干净”的
_needsAddToScene = false;
}
}


##### 5.2. Layer节点的添加previousSibling与nextSibling分别是Layer的前置节点与后置节点,当向Layer树中添加Layer节点时,也会将当前Layer设置为父节点的后置节点,父节点设置为当前节点的前置节点。这样,就形成了一颗树。

class ContainerLayer extends Layer {

//将当前节点及其链表上的所有子节点都加入到Layer树中
@override
void attach(Object owner) {
super.attach(owner);
Layer child = firstChild;
while (child != null) {
child.attach(owner);
child = child.nextSibling;
}
}

//将当前节点及其链表上的所有子节点都从Layer树中移除
@override
void detach() {
super.detach();
Layer child = firstChild;
while (child != null) {
child.detach();
child = child.nextSibling;
}
}
//将child添加到链表中
void append(Layer child) {
adoptChild(child);
child._previousSibling = lastChild;
if (lastChild != null)
lastChild._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
}

}


在上述的append函数中就将子节点添加到Layer树并加入到双链表中。在adoptChild函数中最终会调用attach函数,从而完成Layer树的添加。##### 5.3. Layer的状态更新

class ContainerLayer extends Layer {

//更新Layer节点的状态。
@override
void updateSubtreeNeedsAddToScene() {
super.updateSubtreeNeedsAddToScene();
Layer child = firstChild;
while (child != null) {
child.updateSubtreeNeedsAddToScene();
_needsAddToScene = _needsAddToScene || child._needsAddToScene;
child = child.nextSibling;
}
}

}


updateSubtreeNeedsAddToScene函数就是更新Layer的状态,可以发现,在更新当前Layer的状态时,也会更新其所有子Layer的状态。#### 6. compositing阶段:将UI数据发送给GPU处理。该阶段主要是将更新后的数据传递给GPU。这时候调用的是compositeFrame函数,该函数很简单,就是调用了一个Native函数。

void compositeFrame() {
Timeline.startSync(‘Compositing’, arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
//更新后数据交给GPU处理
_window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}


#### 7. semantics阶段:与平台的辅助功能相关。在向GPU发送数据后,Flutter还会调用flushSemantics函数。该函数与系统的辅助功能相关,一般情况下是不做任何处理。#### 8. finalization阶段:主要是从Element树中移除无用的Element对象及处理绘制结束回调。在该阶段,主要是将Element对象从树中移除及处理添加在\_postFrameCallbacks集合中的回调函数。由于该回调函数是在绘制结束时调用,所以在该回调函数中,context已经创建成功。#### 其他:flushLayout触发布局,将RenderObject树的dirty节点通过调用performLayout方法进行逐一布局,我们先看一下RenderPadding中的实现

@override
void performLayout() {
_resolve();//解析padding参数
if (child == null) {//如果没有child,直接将constraints与padding综合计算得出自己的size
size = constraints.constrain(Size(
_resolvedPadding.left + _resolvedPadding.right,
_resolvedPadding.top + _resolvedPadding.bottom
));
return;
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//将padding减去,生成新的约束innerConstraints
child.layout(innerConstraints, parentUsesSize: true);//用新的约束去布局child
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//设置childParentData的offset值
size = constraints.constrain(Size(//将constraints与padding以及child的sieze综合计算得出自己的size
_resolvedPadding.left + child.size.width + _resolvedPadding.right,
_resolvedPadding.top + child.size.height + _resolvedPadding.bottom
));
}


可以看到RenderPadding中的布局分两种情况。如果没有child,那么就直接拿parent传过来的约束以及padding来确定自己的大小;否则就先去布局child,让后再拿parent传过来的约束和padding以及child的size来确定自己的大小。RenderPadding是典型的单child的RenderBox,我们看一下多个child的RenderBox。例如RenderFlow

@override
void performLayout() {
size = _getSize(constraints);//直接先确定自己的size
int i = 0;
_randomAccessChildren.clear();
RenderBox child = firstChild;
while (child != null) {//遍历孩子
_randomAccessChildren.add(child);
final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//获取child的约束,此方法为抽象
child.layout(innerConstraints, parentUsesSize: true);//布局孩子
final FlowParentData childParentData = child.parentData;
childParentData.offset = Offset.zero;
child = childParentData.nextSibling;
i += 1;
}
}


可以看到RenderFlow的size直接就根据约束来确定了,并没去有先布局孩子,所以RenderFlow的size不依赖与孩子,后面依旧是对每一个child依次进行布局。还有一种比较典型的树尖类型的RenderBox,LeafRenderObjectWidget子类创建的RenderObject对象都是,他们没有孩子,他们才是最终需要渲染的对象,例如

@override
void performLayout() {
size = _sizeForConstraints(constraints);
}


非常简单就通过约束确定自己的大小就结束了。所以performLayout过程就是两点,确定自己的大小以及布局孩子。我们上面提到的都是RenderBox的子类,这些RenderObject约束都是通过BoxConstraints来完成,但是RenderSliver的子类的约束是通过SliverConstraints来完成,虽然他们对child的约束方式不同,但他们在布局过程需要执行的操作都是一致的。#### 绘制布局完成了,PipelineOwner就通过flushPaint来进行绘制

void flushPaint() {
try {
final List dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = [];
// 对dirty nodes列表进行排序,最深的在第一位
for (RenderObject node in dirtyNodes…sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {}
}


PaintingContext.repaintCompositedChild(node)会调用到child.\_paintWithContext(childContext, Offset.zero)方法,进而调用到child的paint方法,我们来看一下第一次绘制的情况,dirty的node就应该是RenderView,跟进RenderView的paint方法

@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);//直接绘制child
}


自己没有什么绘制的内容,直接绘制child,再看一下RenderShiftedBox

@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);//直接绘制child
}
}


好像没有绘制内容就直接递归的进行绘制child,那找一个有绘制内容的吧,我们看看RenderDecoratedBox

@override
void paint(PaintingContext context, Offset offset) {
_painter ??= _decoration.createBoxPainter(markNeedsPaint);//获取painter画笔
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) {//画背景
_painter.paint(context.canvas, offset, filledConfiguration);//绘制过程,具体细节再painter中
if (decoration.isComplex)
context.setIsComplexHint();
}
super.paint(context, offset);//画child,里面直接调用了paintChild
if (position == DecorationPosition.foreground) {//画前景
_painter.paint(context.canvas, offset, filledConfiguration);
if (decoration.isComplex)
context.setIsComplexHint();
}
}


如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,看一下RenderImage

@override
void paint(PaintingContext context, Offset offset) {
if (_image == null)
return;
_resolve();
paintImage(//直接绘制Image,具体细节再此方法中
canvas: context.canvas,
rect: offset & size,
image: _image,
scale: _scale,
colorFilter: _colorFilter,
fit: _fit,
alignment: _resolvedAlignment,
centerSlice: _centerSlice,
repeat: _repeat,
flipHorizontally: _flipHorizontally,


http://www.ppmy.cn/ops/156459.html

相关文章

git使用指南(保姆贴)

训练软件 learnGitBranching&#xff1a;Git 命令可视化学习网站。通过游戏关卡挑战&#xff0c;逐步深入的学习 Git&#xff0c;需要git入门的人&#xff0c;不适合纯小白。Oh My Git&#xff1a;开源的git训练游戏。https://www.w3ccoo.com/git/index.html 存储结构 工作区…

机器学习day8

自定义数据集 &#xff0c;使用朴素贝叶斯对其进行分类 代码 import numpy as np import matplotlib.pyplot as pltclass1_points np.array([[2.1, 2.2], [2.4, 2.5], [2.2, 2.0], [2.0, 2.1], [2.3, 2.3], [2.6, 2.4], [2.5, 2.1]]) class2_points np.array([[4.0, 3.5], …

配置@别名路径,把@/ 解析为 src/

路径解析配置 webpack 安装 craco npm i -D craco/craco 项目根目录下创建文件 craco.config.js &#xff0c;内容如下 const path require(path) module.exports {webpack: {// 配置别名alias: {// 约定&#xff1a; 使用 表示src文件所在路径: path.resolve(__dirname,src)…

不同数据库与 WebGL 集成

一、引言 在当今数字化时代&#xff0c;数据可视化是一个关键需求&#xff0c;而 WebGL&#xff08;Web Graphics Library&#xff09;为在网页上实现高性能 3D 图形渲染提供了强大的工具。然而&#xff0c;WebGL 本身无法直接与数据库进行交互&#xff0c;为了将数据库中的数…

Ubuntu安装OpenSSF Scorecard

OpenSSF Scorecard 是用于评估开源项目安全状况的工具。在 Ubuntu 18.04.6 LTS 系统上安装 OpenSSF Scorecard&#xff0c;可按以下步骤进行&#xff1a; 1. 安装依赖软件 在安装 Scorecard 之前&#xff0c;需要确保系统已经安装了一些必要的依赖项&#xff0c;包括 git、cu…

绿联NAS安装cpolar内网穿透工具实现无公网IP远程访问教程

文章目录 前言1. 开启ssh服务2. ssh连接3. 安装cpolar内网穿透4. 配置绿联NAS公网地址 前言 本文主要介绍如何在绿联NAS中使用ssh远程连接后&#xff0c;使用一行代码快速安装cpolar内网穿透工具&#xff0c;轻松实现随时随地远程访问本地内网中的绿联NAS&#xff0c;无需公网…

IEC60825测试流程与报告测试什么内容IEC60825怎么判定是否合格呢?

IEC60825就是激光产品的测试标准&#xff0c;国际标准的测试报告&#xff0c;有CNAS资质的哦 IEC60825-12014版本 激光模块的测试比较简单&#xff0c;基本的是激光等级符合要求。 激光模块做IEC60825需要的样品每个型号2PCS带一个驱动电源 IEC60825-12014关联的关键词&…

DeepSeek-R1 本地电脑部署 Windows系统 【轻松简易】

本文分享在自己的本地电脑部署 DeepSeek&#xff0c;而且轻松简易&#xff0c;快速上手。 这里借助Ollama工具&#xff0c;在Windows系统中进行大模型部署~ 1、安装Ollama 来到官网地址&#xff1a;Download Ollama on macOS 点击“Download for Windows”下载安装包&#x…