mirror of
https://github.com/flutter/flutter.git
synced 2026-02-15 15:23:32 +08:00
Previously, if you used a layout callback, you could not have a parent that did parentUsesSize, or if you did, you had to be marked sizedByParent. With this patch, we allow the parent to depend on your layout, even if you modify your child list during your layout using a layout callback, by checking that the parent is still actively being laid out in this scenario.
1055 lines
37 KiB
Dart
1055 lines
37 KiB
Dart
// Copyright 2015 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:sky' as sky;
|
|
import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path;
|
|
|
|
import 'package:sky/base/debug.dart';
|
|
import 'package:sky/base/hit_test.dart';
|
|
import 'package:sky/base/node.dart';
|
|
import 'package:sky/base/scheduler.dart' as scheduler;
|
|
import 'package:sky/rendering/layer.dart';
|
|
import 'package:vector_math/vector_math.dart';
|
|
|
|
export 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path;
|
|
export 'package:sky/base/hit_test.dart' show EventDisposition, HitTestTarget, HitTestEntry, HitTestResult;
|
|
|
|
class ParentData {
|
|
void detach() {
|
|
detachSiblings();
|
|
}
|
|
void detachSiblings() { } // workaround for lack of inter-class mixins in Dart
|
|
void merge(ParentData other) {
|
|
// override this in subclasses to merge in data from other into this
|
|
assert(other.runtimeType == this.runtimeType);
|
|
}
|
|
String toString() => '<none>';
|
|
}
|
|
|
|
class PaintingCanvas extends sky.Canvas {
|
|
PaintingCanvas(sky.PictureRecorder recorder, Rect bounds) : super(recorder, bounds);
|
|
// TODO(ianh): Just use sky.Canvas everywhere instead
|
|
}
|
|
|
|
class PaintingContext {
|
|
|
|
// A PaintingContext wraps a canvas, so that the canvas can be
|
|
// hot-swapped whenever we need to start a new layer.
|
|
|
|
// Don't keep a reference to the PaintingContext.canvas, since it
|
|
// can change dynamically after any call to this object's methods.
|
|
|
|
PaintingContext.withOffset(Offset offset, Rect paintBounds) {
|
|
_containerLayer = new ContainerLayer(offset: offset);
|
|
_startRecording(paintBounds);
|
|
}
|
|
|
|
PaintingContext.withLayer(ContainerLayer containerLayer, Rect paintBounds) {
|
|
_containerLayer = containerLayer;
|
|
_startRecording(paintBounds);
|
|
}
|
|
|
|
PaintingContext.forTesting(this._canvas);
|
|
|
|
ContainerLayer _containerLayer;
|
|
ContainerLayer get containerLayer => _containerLayer;
|
|
|
|
PictureLayer _currentLayer;
|
|
sky.PictureRecorder _recorder;
|
|
PaintingCanvas _canvas;
|
|
PaintingCanvas get canvas => _canvas; // Paint on this.
|
|
|
|
void _startRecording(Rect paintBounds) {
|
|
assert(_currentLayer == null);
|
|
assert(_recorder == null);
|
|
assert(_canvas == null);
|
|
_currentLayer = new PictureLayer(paintBounds: paintBounds);
|
|
_recorder = new sky.PictureRecorder();
|
|
_canvas = new PaintingCanvas(_recorder, paintBounds);
|
|
_containerLayer.add(_currentLayer);
|
|
}
|
|
|
|
void endRecording() {
|
|
assert(_currentLayer != null);
|
|
assert(_recorder != null);
|
|
assert(_canvas != null);
|
|
_currentLayer.picture = _recorder.endRecording();
|
|
_currentLayer = null;
|
|
_recorder = null;
|
|
_canvas = null;
|
|
}
|
|
|
|
bool debugCanPaintChild(RenderObject child) {
|
|
// You need to use layers if you are applying transforms, clips,
|
|
// or similar, to a child. To do so, use the paintChildWith*()
|
|
// methods below.
|
|
// (commented out for now because we haven't ported everything yet)
|
|
assert(canvas.getSaveCount() == 1 || !child.needsCompositing);
|
|
return true;
|
|
}
|
|
|
|
void paintChild(RenderObject child, Point childPosition) {
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.hasLayer) {
|
|
insertChild(child, childOffset);
|
|
} else {
|
|
compositeChild(child, childOffset: childOffset, parentLayer: _containerLayer);
|
|
}
|
|
}
|
|
|
|
// Below we have various variants of the paintChild() method, which
|
|
// do additional work, such as clipping or transforming, at the same
|
|
// time as painting the children.
|
|
|
|
// If none of the descendants require compositing, then these don't
|
|
// need to use a new layer, because at no point will any of the
|
|
// children introduce a new layer of their own. In that case, we
|
|
// just use regular canvas commands to do the work.
|
|
|
|
// If at least one of the descendants requires compositing, though,
|
|
// we introduce a new layer to do the work, so that when the
|
|
// children are split into a new layer, the work (e.g. clip) is not
|
|
// lost, as it would if we didn't introduce a new layer.
|
|
|
|
static final Paint _disableAntialias = new Paint()..isAntiAlias = false;
|
|
|
|
void paintChildWithClipRect(RenderObject child, Point childPosition, Rect clipRect) {
|
|
// clipRect is in the parent's coordinate space
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.save();
|
|
canvas.clipRect(clipRect);
|
|
insertChild(child, childOffset);
|
|
canvas.restore();
|
|
} else {
|
|
ClipRectLayer clipLayer = new ClipRectLayer(offset: childOffset, clipRect: clipRect);
|
|
_containerLayer.add(clipLayer);
|
|
compositeChild(child, parentLayer: clipLayer);
|
|
}
|
|
}
|
|
|
|
void paintChildWithClipRRect(RenderObject child, Point childPosition, Rect bounds, sky.RRect clipRRect) {
|
|
// clipRRect is in the parent's coordinate space
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.saveLayer(bounds, _disableAntialias);
|
|
canvas.clipRRect(clipRRect);
|
|
insertChild(child, childOffset);
|
|
canvas.restore();
|
|
} else {
|
|
ClipRRectLayer clipLayer = new ClipRRectLayer(offset: childOffset, bounds: bounds, clipRRect: clipRRect);
|
|
_containerLayer.add(clipLayer);
|
|
compositeChild(child, parentLayer: clipLayer);
|
|
}
|
|
}
|
|
|
|
void paintChildWithClipPath(RenderObject child, Point childPosition, Rect bounds, Path clipPath) {
|
|
// bounds and clipPath are in the parent's coordinate space
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.saveLayer(bounds, _disableAntialias);
|
|
canvas.clipPath(clipPath);
|
|
canvas.translate(childOffset.dx, childOffset.dy);
|
|
insertChild(child, Offset.zero);
|
|
canvas.restore();
|
|
} else {
|
|
ClipPathLayer clipLayer = new ClipPathLayer(offset: childOffset, bounds: bounds, clipPath: clipPath);
|
|
_containerLayer.add(clipLayer);
|
|
compositeChild(child, parentLayer: clipLayer);
|
|
}
|
|
}
|
|
|
|
void paintChildWithTransform(RenderObject child, Point childPosition, Matrix4 transform) {
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.save();
|
|
canvas.translate(childOffset.dx, childOffset.dy);
|
|
canvas.concat(transform.storage);
|
|
insertChild(child, Offset.zero);
|
|
canvas.restore();
|
|
} else {
|
|
TransformLayer transformLayer = new TransformLayer(offset: childOffset, transform: transform);
|
|
_containerLayer.add(transformLayer);
|
|
compositeChild(child, parentLayer: transformLayer);
|
|
}
|
|
}
|
|
|
|
static Paint _getPaintForAlpha(int alpha) {
|
|
return new Paint()
|
|
..color = new Color.fromARGB(alpha, 0, 0, 0)
|
|
..setTransferMode(sky.TransferMode.srcOver)
|
|
..isAntiAlias = false;
|
|
}
|
|
|
|
void paintChildWithOpacity(RenderObject child,
|
|
Point childPosition,
|
|
Rect bounds,
|
|
int alpha) {
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.saveLayer(bounds, _getPaintForAlpha(alpha));
|
|
canvas.translate(childOffset.dx, childOffset.dy);
|
|
insertChild(child, Offset.zero);
|
|
canvas.restore();
|
|
} else {
|
|
OpacityLayer paintLayer = new OpacityLayer(
|
|
offset: childOffset,
|
|
bounds: bounds,
|
|
alpha: alpha);
|
|
_containerLayer.add(paintLayer);
|
|
compositeChild(child, parentLayer: paintLayer);
|
|
}
|
|
}
|
|
|
|
static Paint _getPaintForColorFilter(Color color, sky.TransferMode transferMode) {
|
|
return new Paint()
|
|
..setColorFilter(new sky.ColorFilter.mode(color, transferMode))
|
|
..isAntiAlias = false;
|
|
}
|
|
|
|
void paintChildWithColorFilter(RenderObject child,
|
|
Point childPosition,
|
|
Rect bounds,
|
|
Color color,
|
|
sky.TransferMode transferMode) {
|
|
assert(debugCanPaintChild(child));
|
|
final Offset childOffset = childPosition.toOffset();
|
|
if (!child.needsCompositing) {
|
|
canvas.saveLayer(bounds, _getPaintForColorFilter(color, transferMode));
|
|
canvas.translate(childOffset.dx, childOffset.dy);
|
|
insertChild(child, Offset.zero);
|
|
canvas.restore();
|
|
} else {
|
|
ColorFilterLayer paintLayer = new ColorFilterLayer(
|
|
offset: childOffset,
|
|
bounds: bounds,
|
|
color: color,
|
|
transferMode: transferMode);
|
|
_containerLayer.add(paintLayer);
|
|
compositeChild(child, parentLayer: paintLayer);
|
|
}
|
|
}
|
|
|
|
// do not call directly
|
|
void insertChild(RenderObject child, Offset offset) {
|
|
child._paintWithContext(this, offset);
|
|
}
|
|
|
|
// do not call directly
|
|
void compositeChild(RenderObject child, { Offset childOffset: Offset.zero, ContainerLayer parentLayer }) {
|
|
// This ends the current layer and starts a new layer for the
|
|
// remainder of our rendering. It also creates a new layer for the
|
|
// child, and inserts that layer into the given parentLayer, which
|
|
// must either be our current layer's parent layer, or at least
|
|
// must have our current layer's parent layer as an ancestor.
|
|
final PictureLayer originalLayer = _currentLayer;
|
|
assert(() {
|
|
assert(parentLayer != null);
|
|
assert(originalLayer != null);
|
|
assert(originalLayer.parent != null);
|
|
ContainerLayer ancestor = parentLayer;
|
|
while (ancestor != null && ancestor != originalLayer.parent)
|
|
ancestor = ancestor.parent;
|
|
assert(ancestor == originalLayer.parent);
|
|
assert(originalLayer.parent == _containerLayer);
|
|
return true;
|
|
});
|
|
|
|
// End our current layer.
|
|
endRecording();
|
|
|
|
// Create a layer for our child, and paint the child into it.
|
|
if (child.needsPaint || !child.hasLayer) {
|
|
PaintingContext newContext = new PaintingContext.withOffset(childOffset, child.paintBounds);
|
|
child._layer = newContext.containerLayer;
|
|
child._paintWithContext(newContext, Offset.zero);
|
|
newContext.endRecording();
|
|
} else {
|
|
assert(child._layer != null);
|
|
child._layer.detach();
|
|
child._layer.offset = childOffset;
|
|
}
|
|
parentLayer.add(child._layer);
|
|
|
|
// Start a new layer for anything that remains of our own paint.
|
|
_startRecording(originalLayer.paintBounds);
|
|
}
|
|
|
|
}
|
|
|
|
abstract class Constraints {
|
|
const Constraints();
|
|
bool get isTight;
|
|
}
|
|
|
|
typedef void RenderObjectVisitor(RenderObject child);
|
|
typedef void LayoutCallback(Constraints constraints);
|
|
typedef double DimensionCallback(Constraints constraints);
|
|
|
|
abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|
|
|
// LAYOUT
|
|
|
|
// parentData is only for use by the RenderObject that actually lays this
|
|
// node out, and any other nodes who happen to know exactly what
|
|
// kind of node that is.
|
|
dynamic parentData; // TODO(ianh): change the type of this back to ParentData once the analyzer is cleverer
|
|
void setupParentData(RenderObject child) {
|
|
// override this to setup .parentData correctly for your class
|
|
assert(debugCanPerformMutations);
|
|
if (child.parentData is! ParentData)
|
|
child.parentData = new ParentData();
|
|
}
|
|
|
|
void adoptChild(RenderObject child) { // only for use by subclasses
|
|
// call this whenever you decide a node is a child
|
|
assert(debugCanPerformMutations);
|
|
assert(child != null);
|
|
setupParentData(child);
|
|
super.adoptChild(child);
|
|
markNeedsLayout();
|
|
markNeedsCompositingBitsUpdate();
|
|
}
|
|
void dropChild(RenderObject child) { // only for use by subclasses
|
|
assert(debugCanPerformMutations);
|
|
assert(child != null);
|
|
assert(child.parentData != null);
|
|
child._cleanRelayoutSubtreeRoot();
|
|
child.parentData.detach();
|
|
super.dropChild(child);
|
|
markNeedsLayout();
|
|
markNeedsCompositingBitsUpdate();
|
|
}
|
|
|
|
// Override in subclasses with children and call the visitor for each child.
|
|
void visitChildren(RenderObjectVisitor visitor) { }
|
|
|
|
dynamic debugExceptionContext = '';
|
|
static dynamic _debugLastException;
|
|
bool _debugReportException(dynamic exception, String method) {
|
|
if (!inDebugBuild) {
|
|
print('Uncaught exception in ${method}():\n$exception');
|
|
return false;
|
|
}
|
|
if (!identical(exception, _debugLastException)) {
|
|
print('-- EXCEPTION --');
|
|
print('An exception was raised during ${method}().');
|
|
'The following RenderObject was being processed when the exception was fired:\n${this}'.split('\n').forEach(print);
|
|
if (debugExceptionContext != '')
|
|
'The RenderObject had the following exception context:\n${debugExceptionContext}'.split('\n').forEach(print);
|
|
_debugLastException = exception;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool _debugDoingLayout = false;
|
|
static bool get debugDoingLayout => _debugDoingLayout;
|
|
bool _debugDoingThisResize = false;
|
|
bool get debugDoingThisResize => _debugDoingThisResize;
|
|
bool _debugDoingThisLayout = false;
|
|
bool get debugDoingThisLayout => _debugDoingThisLayout;
|
|
static RenderObject _debugActiveLayout = null;
|
|
static RenderObject get debugActiveLayout => _debugActiveLayout;
|
|
bool _debugMutationsLocked = false;
|
|
bool _debugCanParentUseSize;
|
|
bool get debugCanParentUseSize => _debugCanParentUseSize;
|
|
bool get debugCanPerformMutations {
|
|
RenderObject node = this;
|
|
while (true) {
|
|
if (node._doingThisLayoutWithCallback)
|
|
return true;
|
|
if (node._debugMutationsLocked)
|
|
return false;
|
|
if (node.parent is! RenderObject)
|
|
return true;
|
|
node = node.parent;
|
|
}
|
|
}
|
|
|
|
static List<RenderObject> _nodesNeedingLayout = new List<RenderObject>();
|
|
bool _needsLayout = true;
|
|
bool get needsLayout => _needsLayout;
|
|
RenderObject _relayoutSubtreeRoot;
|
|
bool _doingThisLayoutWithCallback = false;
|
|
Constraints _constraints;
|
|
Constraints get constraints => _constraints;
|
|
bool debugDoesMeetConstraints(); // override this in a subclass to verify that your state matches the constraints object
|
|
bool debugAncestorsAlreadyMarkedNeedsLayout() {
|
|
if (_relayoutSubtreeRoot == null)
|
|
return true; // we haven't yet done layout even once, so there's nothing for us to do
|
|
RenderObject node = this;
|
|
while (node != _relayoutSubtreeRoot) {
|
|
assert(node._relayoutSubtreeRoot == _relayoutSubtreeRoot);
|
|
assert(node.parent != null);
|
|
node = node.parent as RenderObject;
|
|
if ((!node._needsLayout) && (!node._debugDoingThisLayout))
|
|
return false;
|
|
}
|
|
assert(node._relayoutSubtreeRoot == node);
|
|
return true;
|
|
}
|
|
void markNeedsLayout() {
|
|
assert(debugCanPerformMutations);
|
|
if (_needsLayout) {
|
|
assert(debugAncestorsAlreadyMarkedNeedsLayout());
|
|
return;
|
|
}
|
|
_needsLayout = true;
|
|
assert(_relayoutSubtreeRoot != null);
|
|
if (_relayoutSubtreeRoot != this) {
|
|
final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer
|
|
assert(parent is RenderObject);
|
|
if (!_doingThisLayoutWithCallback) {
|
|
parent.markNeedsLayout();
|
|
} else {
|
|
assert(parent._debugDoingThisLayout);
|
|
}
|
|
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
|
|
} else {
|
|
_nodesNeedingLayout.add(this);
|
|
scheduler.ensureVisualUpdate();
|
|
}
|
|
}
|
|
void _cleanRelayoutSubtreeRoot() {
|
|
if (_relayoutSubtreeRoot != this) {
|
|
_relayoutSubtreeRoot = null;
|
|
_needsLayout = true;
|
|
visitChildren((RenderObject child) {
|
|
child._cleanRelayoutSubtreeRoot();
|
|
});
|
|
}
|
|
}
|
|
void scheduleInitialLayout() {
|
|
assert(attached);
|
|
assert(parent is! RenderObject);
|
|
assert(!_debugDoingLayout);
|
|
assert(_relayoutSubtreeRoot == null);
|
|
_relayoutSubtreeRoot = this;
|
|
assert(() {
|
|
_debugCanParentUseSize = false;
|
|
return true;
|
|
});
|
|
_nodesNeedingLayout.add(this);
|
|
}
|
|
static void flushLayout() {
|
|
sky.tracing.begin('RenderObject.flushLayout');
|
|
_debugDoingLayout = true;
|
|
try {
|
|
// TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themeselves
|
|
while(_nodesNeedingLayout.isNotEmpty) {
|
|
List<RenderObject> dirtyNodes = _nodesNeedingLayout;
|
|
_nodesNeedingLayout = new List<RenderObject>();
|
|
dirtyNodes..sort((a, b) => a.depth - b.depth)..forEach((node) {
|
|
if (node._needsLayout && node.attached)
|
|
node.layoutWithoutResize();
|
|
});
|
|
}
|
|
} finally {
|
|
_debugDoingLayout = false;
|
|
sky.tracing.end('RenderObject.flushLayout');
|
|
}
|
|
}
|
|
void layoutWithoutResize() {
|
|
try {
|
|
assert(_relayoutSubtreeRoot == this);
|
|
RenderObject debugPreviousActiveLayout;
|
|
assert(!_debugMutationsLocked);
|
|
assert(!_doingThisLayoutWithCallback);
|
|
assert(_debugCanParentUseSize != null);
|
|
assert(() {
|
|
_debugMutationsLocked = true;
|
|
_debugDoingThisLayout = true;
|
|
debugPreviousActiveLayout = _debugActiveLayout;
|
|
_debugActiveLayout = this;
|
|
return true;
|
|
});
|
|
performLayout();
|
|
assert(() {
|
|
_debugActiveLayout = debugPreviousActiveLayout;
|
|
_debugDoingThisLayout = false;
|
|
_debugMutationsLocked = false;
|
|
return true;
|
|
});
|
|
} catch (e) {
|
|
if (_debugReportException(e, 'layoutWithoutResize'))
|
|
rethrow;
|
|
}
|
|
_needsLayout = false;
|
|
markNeedsPaint();
|
|
}
|
|
void layout(Constraints constraints, { bool parentUsesSize: false }) {
|
|
final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer
|
|
RenderObject relayoutSubtreeRoot;
|
|
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject)
|
|
relayoutSubtreeRoot = this;
|
|
else
|
|
relayoutSubtreeRoot = parent._relayoutSubtreeRoot;
|
|
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
|
|
if (!needsLayout && constraints == _constraints && relayoutSubtreeRoot == _relayoutSubtreeRoot)
|
|
return;
|
|
_constraints = constraints;
|
|
_relayoutSubtreeRoot = relayoutSubtreeRoot;
|
|
assert(!_debugMutationsLocked);
|
|
assert(!_doingThisLayoutWithCallback);
|
|
assert(() {
|
|
_debugMutationsLocked = true;
|
|
_debugCanParentUseSize = parentUsesSize;
|
|
return true;
|
|
});
|
|
if (sizedByParent) {
|
|
assert(() { _debugDoingThisResize = true; return true; });
|
|
performResize();
|
|
assert(() { _debugDoingThisResize = false; return true; });
|
|
}
|
|
RenderObject debugPreviousActiveLayout;
|
|
assert(() {
|
|
_debugDoingThisLayout = true;
|
|
debugPreviousActiveLayout = _debugActiveLayout;
|
|
_debugActiveLayout = this;
|
|
return true;
|
|
});
|
|
try {
|
|
performLayout();
|
|
assert(() {
|
|
_debugActiveLayout = debugPreviousActiveLayout;
|
|
_debugDoingThisLayout = false;
|
|
_debugMutationsLocked = false;
|
|
return true;
|
|
});
|
|
assert(debugDoesMeetConstraints());
|
|
} catch (e) {
|
|
if (_debugReportException(e, 'layout'))
|
|
rethrow;
|
|
}
|
|
_needsLayout = false;
|
|
markNeedsPaint();
|
|
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
|
|
}
|
|
bool get sizedByParent => false; // return true if the constraints are the only input to the sizing algorithm (in particular, child nodes have no impact)
|
|
void performResize(); // set the local dimensions, using only the constraints (only called if sizedByParent is true)
|
|
void performLayout();
|
|
// Override this to perform relayout without your parent's
|
|
// involvement.
|
|
//
|
|
// This is called during layout. If sizedByParent is true, then
|
|
// performLayout() should not change your dimensions, only do that
|
|
// in performResize(). If sizedByParent is false, then set both
|
|
// your dimensions and do your children's layout here.
|
|
//
|
|
// When calling layout() on your children, pass in
|
|
// "parentUsesSize: true" if your size or layout is dependent on
|
|
// your child's size or intrinsic dimensions.
|
|
void invokeLayoutCallback(LayoutCallback callback) {
|
|
assert(_debugMutationsLocked);
|
|
assert(_debugDoingThisLayout);
|
|
assert(!_doingThisLayoutWithCallback);
|
|
_doingThisLayoutWithCallback = true;
|
|
try {
|
|
callback(constraints);
|
|
} finally {
|
|
_doingThisLayoutWithCallback = false;
|
|
}
|
|
}
|
|
|
|
// when the parent has rotated (e.g. when the screen has been turned
|
|
// 90 degrees), immediately prior to layout() being called for the
|
|
// new dimensions, rotate() is called with the old and new angles.
|
|
// The next time paint() is called, the coordinate space will have
|
|
// been rotated N quarter-turns clockwise, where:
|
|
// N = newAngle-oldAngle
|
|
// ...but the rendering is expected to remain the same, pixel for
|
|
// pixel, on the output device. Then, the layout() method or
|
|
// equivalent will be invoked.
|
|
|
|
void rotate({
|
|
int oldAngle, // 0..3
|
|
int newAngle, // 0..3
|
|
Duration time
|
|
}) { }
|
|
|
|
|
|
// PAINTING
|
|
|
|
static bool _debugDoingPaint = false;
|
|
static bool get debugDoingPaint => _debugDoingPaint;
|
|
static void set debugDoingPaint(bool value) {
|
|
_debugDoingPaint = value;
|
|
}
|
|
bool _debugDoingThisPaint = false;
|
|
bool get debugDoingThisPaint => _debugDoingThisPaint;
|
|
static RenderObject _debugActivePaint = null;
|
|
static RenderObject get debugActivePaint => _debugActivePaint;
|
|
|
|
static List<RenderObject> _nodesNeedingPaint = new List<RenderObject>();
|
|
|
|
// Override this in subclasses to indicate that instances of your
|
|
// class need to have their own Layer. For example, videos.
|
|
bool get hasLayer => false;
|
|
|
|
ContainerLayer _layer;
|
|
ContainerLayer get layer {
|
|
assert(hasLayer);
|
|
assert(!_needsPaint);
|
|
return _layer;
|
|
}
|
|
|
|
// When the subtree is mutated, we need to recompute our
|
|
// "needsCompositing" bit, and our ancestors need to do the
|
|
// same (in case ours changed). adoptChild() and dropChild() thus
|
|
// call markNeedsCompositingBitsUpdate().
|
|
bool _needsCompositingBitsUpdate = true;
|
|
void markNeedsCompositingBitsUpdate() {
|
|
if (_needsCompositingBitsUpdate)
|
|
return;
|
|
_needsCompositingBitsUpdate = true;
|
|
final AbstractNode parent = this.parent; // TODO(ianh): remove the once the analyzer is cleverer
|
|
if (parent is RenderObject)
|
|
parent.markNeedsCompositingBitsUpdate();
|
|
}
|
|
bool _needsCompositing = false;
|
|
bool get needsCompositing {
|
|
// needsCompositing is true if either we have a layer or one of our descendants has a layer
|
|
assert(!_needsCompositingBitsUpdate); // make sure we don't use this bit when it is dirty
|
|
return _needsCompositing;
|
|
}
|
|
void updateCompositingBits() {
|
|
if (!_needsCompositingBitsUpdate)
|
|
return;
|
|
bool didHaveCompositedDescendant = _needsCompositing;
|
|
visitChildren((RenderObject child) {
|
|
child.updateCompositingBits();
|
|
if (child.needsCompositing)
|
|
_needsCompositing = true;
|
|
});
|
|
if (hasLayer)
|
|
_needsCompositing = true;
|
|
if (didHaveCompositedDescendant != _needsCompositing)
|
|
markNeedsPaint();
|
|
_needsCompositingBitsUpdate = false;
|
|
}
|
|
|
|
bool _needsPaint = true;
|
|
bool get needsPaint => _needsPaint;
|
|
void markNeedsPaint() {
|
|
assert(!debugDoingPaint);
|
|
if (!attached) return; // Don't try painting things that aren't in the hierarchy
|
|
if (_needsPaint) return;
|
|
if (hasLayer) {
|
|
// If we always have our own layer, then we can just repaint
|
|
// ourselves without involving any other nodes.
|
|
assert(_layer != null);
|
|
_needsPaint = true;
|
|
_nodesNeedingPaint.add(this);
|
|
scheduler.ensureVisualUpdate();
|
|
} else if (parent is RenderObject) {
|
|
// We don't have our own layer; one of our ancestors will take
|
|
// care of updating the layer we're in and when they do that
|
|
// we'll get our paint() method called.
|
|
assert(_layer == null);
|
|
(parent as RenderObject).markNeedsPaint(); // TODO(ianh): remove the cast once the analyzer is cleverer
|
|
} else {
|
|
// If we're the root of the render tree (probably a RenderView),
|
|
// then we have to paint ourselves, since nobody else can paint
|
|
// us. We don't add ourselves to _nodesNeedingPaint in this
|
|
// case, because the root is always told to paint regardless.
|
|
_needsPaint = true;
|
|
scheduler.ensureVisualUpdate();
|
|
}
|
|
}
|
|
static void flushPaint() {
|
|
sky.tracing.begin('RenderObject.flushPaint');
|
|
_debugDoingPaint = true;
|
|
try {
|
|
List<RenderObject> dirtyNodes = _nodesNeedingPaint;
|
|
_nodesNeedingPaint = new List<RenderObject>();
|
|
// Sort the dirty nodes in reverse order (deepest first).
|
|
for (RenderObject node in dirtyNodes..sort((a, b) => b.depth - a.depth)) {
|
|
assert(node._needsPaint);
|
|
if (node.attached)
|
|
node._repaint();
|
|
};
|
|
assert(_nodesNeedingPaint.length == 0);
|
|
} finally {
|
|
_debugDoingPaint = false;
|
|
sky.tracing.end('RenderObject.flushPaint');
|
|
}
|
|
}
|
|
void scheduleInitialPaint(ContainerLayer rootLayer) {
|
|
assert(attached);
|
|
assert(parent is! RenderObject);
|
|
assert(!_debugDoingPaint);
|
|
assert(hasLayer);
|
|
assert(_layer == null);
|
|
_layer = rootLayer;
|
|
assert(_needsPaint);
|
|
_nodesNeedingPaint.add(this);
|
|
}
|
|
void _repaint() {
|
|
assert(hasLayer);
|
|
assert(_layer != null);
|
|
_layer.removeAllChildren();
|
|
PaintingContext context = new PaintingContext.withLayer(_layer, paintBounds);
|
|
_layer = context._containerLayer;
|
|
try {
|
|
_paintWithContext(context, Offset.zero);
|
|
context.endRecording();
|
|
} catch (e) {
|
|
if (_debugReportException(e, '_repaint'))
|
|
rethrow;
|
|
}
|
|
}
|
|
void _paintWithContext(PaintingContext context, Offset offset) {
|
|
assert(!_debugDoingThisPaint);
|
|
assert(!_needsLayout);
|
|
assert(!_needsCompositingBitsUpdate);
|
|
RenderObject debugLastActivePaint;
|
|
assert(() {
|
|
_debugDoingThisPaint = true;
|
|
debugLastActivePaint = _debugActivePaint;
|
|
_debugActivePaint = this;
|
|
debugPaint(context, offset);
|
|
if (debugPaintBoundsEnabled) {
|
|
context.canvas.save();
|
|
context.canvas.clipRect(paintBounds.shift(offset));
|
|
}
|
|
assert(!hasLayer || _layer != null);
|
|
return true;
|
|
});
|
|
_needsPaint = false;
|
|
paint(context, offset);
|
|
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
|
|
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
|
|
assert(() {
|
|
if (debugPaintBoundsEnabled)
|
|
context.canvas.restore();
|
|
_debugActivePaint = debugLastActivePaint;
|
|
_debugDoingThisPaint = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
Rect get paintBounds;
|
|
void debugPaint(PaintingContext context, Offset offset) { }
|
|
void paint(PaintingContext context, Offset offset) { }
|
|
|
|
void applyPaintTransform(Matrix4 transform) { }
|
|
|
|
|
|
// EVENTS
|
|
|
|
EventDisposition handleEvent(sky.Event event, HitTestEntry entry) {
|
|
// override this if you have a client, to hand it to the client
|
|
// override this if you want to do anything with the event
|
|
return EventDisposition.ignored;
|
|
}
|
|
|
|
|
|
// HIT TESTING
|
|
|
|
// RenderObject subclasses are expected to have a method like the
|
|
// following (with the signature being whatever passes for coordinates
|
|
// for this particular class):
|
|
// bool hitTest(HitTestResult result, { Point position }) {
|
|
// // If (x,y) is not inside this node, then return false. (You
|
|
// // can assume that the given coordinate is inside your
|
|
// // dimensions. You only need to check this if you're an
|
|
// // irregular shape, e.g. if you have a hole.)
|
|
// // Otherwise:
|
|
// // For each child that intersects x,y, in z-order starting from the top,
|
|
// // call hitTest() for that child, passing it /result/, and the coordinates
|
|
// // converted to the child's coordinate origin, and stop at the first child
|
|
// // that returns true.
|
|
// // Then, add yourself to /result/, and return true.
|
|
// }
|
|
// You must not add yourself to /result/ if you return false.
|
|
|
|
|
|
String toString([String prefix = '']) {
|
|
RenderObject debugPreviousActiveLayout = _debugActiveLayout;
|
|
_debugActiveLayout = null;
|
|
String header = toStringName();
|
|
prefix += ' ';
|
|
String result = '${header}\n${debugDescribeSettings(prefix)}${debugDescribeChildren(prefix)}';
|
|
_debugActiveLayout = debugPreviousActiveLayout;
|
|
return result;
|
|
}
|
|
String toStringName() {
|
|
String header = '${runtimeType}';
|
|
if (_relayoutSubtreeRoot != null && _relayoutSubtreeRoot != this) {
|
|
int count = 1;
|
|
RenderObject target = parent;
|
|
while (target != null && target != _relayoutSubtreeRoot) {
|
|
target = target.parent as RenderObject;
|
|
count += 1;
|
|
}
|
|
header += ' relayoutSubtreeRoot=up$count';
|
|
}
|
|
if (_needsLayout)
|
|
header += ' NEEDS-LAYOUT';
|
|
if (!attached)
|
|
header += ' DETACHED';
|
|
return header;
|
|
}
|
|
String debugDescribeSettings(String prefix) => '${prefix}parentData: ${parentData}\n${prefix}constraints: ${constraints}\n';
|
|
String debugDescribeChildren(String prefix) => '';
|
|
|
|
}
|
|
|
|
double clamp({ double min: 0.0, double value: 0.0, double max: double.INFINITY }) {
|
|
assert(min != null);
|
|
assert(value != null);
|
|
assert(max != null);
|
|
return math.max(min, math.min(max, value));
|
|
}
|
|
|
|
|
|
// GENERIC MIXIN FOR RENDER NODES WITH ONE CHILD
|
|
|
|
abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implements RenderObject {
|
|
ChildType _child;
|
|
ChildType get child => _child;
|
|
void set child (ChildType value) {
|
|
if (_child != null)
|
|
dropChild(_child);
|
|
_child = value;
|
|
if (_child != null)
|
|
adoptChild(_child);
|
|
}
|
|
void attachChildren() {
|
|
if (_child != null)
|
|
_child.attach();
|
|
}
|
|
void detachChildren() {
|
|
if (_child != null)
|
|
_child.detach();
|
|
}
|
|
void visitChildren(RenderObjectVisitor visitor) {
|
|
if (_child != null)
|
|
visitor(_child);
|
|
}
|
|
String debugDescribeChildren(String prefix) {
|
|
if (child != null)
|
|
return '${prefix}child: ${child.toString(prefix)}';
|
|
return '';
|
|
}
|
|
}
|
|
|
|
|
|
// GENERIC MIXIN FOR RENDER NODES WITH A LIST OF CHILDREN
|
|
|
|
abstract class ContainerParentDataMixin<ChildType extends RenderObject> {
|
|
ChildType previousSibling;
|
|
ChildType nextSibling;
|
|
void detachSiblings() {
|
|
if (previousSibling != null) {
|
|
assert(previousSibling.parentData is ContainerParentDataMixin<ChildType>);
|
|
assert(previousSibling != this);
|
|
assert(previousSibling.parentData.nextSibling == this);
|
|
previousSibling.parentData.nextSibling = nextSibling;
|
|
}
|
|
if (nextSibling != null) {
|
|
assert(nextSibling.parentData is ContainerParentDataMixin<ChildType>);
|
|
assert(nextSibling != this);
|
|
assert(nextSibling.parentData.previousSibling == this);
|
|
nextSibling.parentData.previousSibling = previousSibling;
|
|
}
|
|
previousSibling = null;
|
|
nextSibling = null;
|
|
}
|
|
}
|
|
|
|
abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> implements RenderObject {
|
|
|
|
bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType equals }) {
|
|
assert(child.parentData is ParentDataType);
|
|
while (child.parentData.previousSibling != null) {
|
|
assert(child.parentData.previousSibling != child);
|
|
child = child.parentData.previousSibling;
|
|
assert(child.parentData is ParentDataType);
|
|
}
|
|
return child == equals;
|
|
}
|
|
bool _debugUltimateNextSiblingOf(ChildType child, { ChildType equals }) {
|
|
assert(child.parentData is ParentDataType);
|
|
while (child.parentData.nextSibling != null) {
|
|
assert(child.parentData.nextSibling != child);
|
|
child = child.parentData.nextSibling;
|
|
assert(child.parentData is ParentDataType);
|
|
}
|
|
return child == equals;
|
|
}
|
|
|
|
int _childCount = 0;
|
|
int get childCount => _childCount;
|
|
|
|
ChildType _firstChild;
|
|
ChildType _lastChild;
|
|
void _addToChildList(ChildType child, { ChildType before }) {
|
|
assert(child.parentData is ParentDataType);
|
|
assert(child.parentData.nextSibling == null);
|
|
assert(child.parentData.previousSibling == null);
|
|
_childCount += 1;
|
|
assert(_childCount > 0);
|
|
if (before == null) {
|
|
// append at the end (_lastChild)
|
|
child.parentData.previousSibling = _lastChild;
|
|
if (_lastChild != null) {
|
|
assert(_lastChild.parentData is ParentDataType);
|
|
_lastChild.parentData.nextSibling = child;
|
|
}
|
|
_lastChild = child;
|
|
if (_firstChild == null)
|
|
_firstChild = child;
|
|
} else {
|
|
assert(_firstChild != null);
|
|
assert(_lastChild != null);
|
|
assert(_debugUltimatePreviousSiblingOf(before, equals: _firstChild));
|
|
assert(_debugUltimateNextSiblingOf(before, equals: _lastChild));
|
|
assert(before.parentData is ParentDataType);
|
|
if (before.parentData.previousSibling == null) {
|
|
// insert at the start (_firstChild); we'll end up with two or more children
|
|
assert(before == _firstChild);
|
|
child.parentData.nextSibling = before;
|
|
before.parentData.previousSibling = child;
|
|
_firstChild = child;
|
|
} else {
|
|
// insert in the middle; we'll end up with three or more children
|
|
// set up links from child to siblings
|
|
child.parentData.previousSibling = before.parentData.previousSibling;
|
|
child.parentData.nextSibling = before;
|
|
// set up links from siblings to child
|
|
assert(child.parentData.previousSibling.parentData is ParentDataType);
|
|
assert(child.parentData.nextSibling.parentData is ParentDataType);
|
|
child.parentData.previousSibling.parentData.nextSibling = child;
|
|
child.parentData.nextSibling.parentData.previousSibling = child;
|
|
assert(before.parentData.previousSibling == child);
|
|
}
|
|
}
|
|
}
|
|
void add(ChildType child, { ChildType before }) {
|
|
assert(child != this);
|
|
assert(before != this);
|
|
assert(child != before);
|
|
assert(child != _firstChild);
|
|
assert(child != _lastChild);
|
|
adoptChild(child);
|
|
_addToChildList(child, before: before);
|
|
}
|
|
void addAll(List<ChildType> children) {
|
|
if (children != null)
|
|
for (ChildType child in children)
|
|
add(child);
|
|
}
|
|
void _removeFromChildList(ChildType child) {
|
|
assert(child.parentData is ParentDataType);
|
|
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
|
|
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
|
|
assert(_childCount >= 0);
|
|
if (child.parentData.previousSibling == null) {
|
|
assert(_firstChild == child);
|
|
_firstChild = child.parentData.nextSibling;
|
|
} else {
|
|
assert(child.parentData.previousSibling.parentData is ParentDataType);
|
|
child.parentData.previousSibling.parentData.nextSibling = child.parentData.nextSibling;
|
|
}
|
|
if (child.parentData.nextSibling == null) {
|
|
assert(_lastChild == child);
|
|
_lastChild = child.parentData.previousSibling;
|
|
} else {
|
|
assert(child.parentData.nextSibling.parentData is ParentDataType);
|
|
child.parentData.nextSibling.parentData.previousSibling = child.parentData.previousSibling;
|
|
}
|
|
child.parentData.previousSibling = null;
|
|
child.parentData.nextSibling = null;
|
|
_childCount -= 1;
|
|
}
|
|
void remove(ChildType child) {
|
|
_removeFromChildList(child);
|
|
dropChild(child);
|
|
}
|
|
void removeAll() {
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
assert(child.parentData is ParentDataType);
|
|
ChildType next = child.parentData.nextSibling;
|
|
child.parentData.previousSibling = null;
|
|
child.parentData.nextSibling = null;
|
|
dropChild(child);
|
|
child = next;
|
|
}
|
|
_firstChild = null;
|
|
_lastChild = null;
|
|
_childCount = 0;
|
|
}
|
|
void move(ChildType child, { ChildType before }) {
|
|
assert(child != this);
|
|
assert(before != this);
|
|
assert(child != before);
|
|
assert(child.parent == this);
|
|
assert(child.parentData is ParentDataType);
|
|
if (child.parentData.nextSibling == before)
|
|
return;
|
|
_removeFromChildList(child);
|
|
_addToChildList(child, before: before);
|
|
}
|
|
void redepthChildren() {
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
redepthChild(child);
|
|
assert(child.parentData is ParentDataType);
|
|
child = child.parentData.nextSibling;
|
|
}
|
|
}
|
|
void attachChildren() {
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
child.attach();
|
|
assert(child.parentData is ParentDataType);
|
|
child = child.parentData.nextSibling;
|
|
}
|
|
}
|
|
void detachChildren() {
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
child.detach();
|
|
assert(child.parentData is ParentDataType);
|
|
child = child.parentData.nextSibling;
|
|
}
|
|
}
|
|
void visitChildren(RenderObjectVisitor visitor) {
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
visitor(child);
|
|
assert(child.parentData is ParentDataType);
|
|
child = child.parentData.nextSibling;
|
|
}
|
|
}
|
|
|
|
ChildType get firstChild => _firstChild;
|
|
ChildType get lastChild => _lastChild;
|
|
ChildType childAfter(ChildType child) {
|
|
assert(child.parentData is ParentDataType);
|
|
return child.parentData.nextSibling;
|
|
}
|
|
|
|
String debugDescribeChildren(String prefix) {
|
|
String result = '';
|
|
int count = 1;
|
|
ChildType child = _firstChild;
|
|
while (child != null) {
|
|
result += '${prefix}child ${count}: ${child.toString(prefix)}';
|
|
count += 1;
|
|
child = child.parentData.nextSibling;
|
|
}
|
|
return result;
|
|
}
|
|
}
|