mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Negative constraints never make sense, so catch those too. Make RenderObject.layout's isNormalized assert use the newer more fancy debug version of isNormalized.
536 lines
19 KiB
Dart
536 lines
19 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 'package:flutter/rendering.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
|
|
const double kTwoPi = 2 * math.PI;
|
|
|
|
class SectorConstraints extends Constraints {
|
|
const SectorConstraints({
|
|
this.minDeltaRadius: 0.0,
|
|
this.maxDeltaRadius: double.INFINITY,
|
|
this.minDeltaTheta: 0.0,
|
|
this.maxDeltaTheta: kTwoPi
|
|
});
|
|
|
|
const SectorConstraints.tight({ double deltaRadius: 0.0, double deltaTheta: 0.0 })
|
|
: minDeltaRadius = deltaRadius,
|
|
maxDeltaRadius = deltaRadius,
|
|
minDeltaTheta = deltaTheta,
|
|
maxDeltaTheta = deltaTheta;
|
|
|
|
final double minDeltaRadius;
|
|
final double maxDeltaRadius;
|
|
final double minDeltaTheta;
|
|
final double maxDeltaTheta;
|
|
|
|
double constrainDeltaRadius(double deltaRadius) {
|
|
return deltaRadius.clamp(minDeltaRadius, maxDeltaRadius);
|
|
}
|
|
|
|
double constrainDeltaTheta(double deltaTheta) {
|
|
return deltaTheta.clamp(minDeltaTheta, maxDeltaTheta);
|
|
}
|
|
|
|
bool get isTight => minDeltaTheta >= maxDeltaTheta && minDeltaTheta >= maxDeltaTheta;
|
|
|
|
bool get isNormalized => minDeltaRadius <= maxDeltaRadius && minDeltaTheta <= maxDeltaTheta;
|
|
bool get debugAssertIsNormalized {
|
|
assert(isNormalized);
|
|
return isNormalized;
|
|
}
|
|
}
|
|
|
|
class SectorDimensions {
|
|
const SectorDimensions({ this.deltaRadius: 0.0, this.deltaTheta: 0.0 });
|
|
|
|
factory SectorDimensions.withConstraints(
|
|
SectorConstraints constraints,
|
|
{ double deltaRadius: 0.0, double deltaTheta: 0.0 }
|
|
) {
|
|
return new SectorDimensions(
|
|
deltaRadius: constraints.constrainDeltaRadius(deltaRadius),
|
|
deltaTheta: constraints.constrainDeltaTheta(deltaTheta)
|
|
);
|
|
}
|
|
|
|
final double deltaRadius;
|
|
final double deltaTheta;
|
|
}
|
|
|
|
class SectorParentData extends ParentData {
|
|
double radius = 0.0;
|
|
double theta = 0.0;
|
|
}
|
|
|
|
abstract class RenderSector extends RenderObject {
|
|
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! SectorParentData)
|
|
child.parentData = new SectorParentData();
|
|
}
|
|
|
|
// RenderSectors always use SectorParentData subclasses, as they need to be
|
|
// able to read their position information for painting and hit testing.
|
|
SectorParentData get parentData => super.parentData;
|
|
|
|
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
|
|
return new SectorDimensions.withConstraints(constraints);
|
|
}
|
|
|
|
SectorConstraints get constraints => super.constraints;
|
|
void debugAssertDoesMeetConstraints() {
|
|
assert(constraints != null);
|
|
assert(deltaRadius != null);
|
|
assert(deltaRadius < double.INFINITY);
|
|
assert(deltaTheta != null);
|
|
assert(deltaTheta < double.INFINITY);
|
|
assert(constraints.minDeltaRadius <= deltaRadius);
|
|
assert(deltaRadius <= math.max(constraints.minDeltaRadius, constraints.maxDeltaRadius));
|
|
assert(constraints.minDeltaTheta <= deltaTheta);
|
|
assert(deltaTheta <= math.max(constraints.minDeltaTheta, constraints.maxDeltaTheta));
|
|
}
|
|
void performResize() {
|
|
// default behavior for subclasses that have sizedByParent = true
|
|
deltaRadius = constraints.constrainDeltaRadius(0.0);
|
|
deltaTheta = constraints.constrainDeltaTheta(0.0);
|
|
}
|
|
void performLayout() {
|
|
// descendants have to either override performLayout() to set both
|
|
// the dimensions and lay out children, or, set sizedByParent to
|
|
// true so that performResize()'s logic above does its thing.
|
|
assert(sizedByParent);
|
|
}
|
|
|
|
Rect get paintBounds => new Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius);
|
|
Rect get semanticBounds => new Rect.fromLTWH(-deltaRadius, -deltaRadius, 2.0 * deltaRadius, 2.0 * deltaRadius);
|
|
|
|
bool hitTest(HitTestResult result, { double radius, double theta }) {
|
|
if (radius < parentData.radius || radius >= parentData.radius + deltaRadius ||
|
|
theta < parentData.theta || theta >= parentData.theta + deltaTheta)
|
|
return false;
|
|
hitTestChildren(result, radius: radius, theta: theta);
|
|
result.add(new HitTestEntry(this));
|
|
return true;
|
|
}
|
|
void hitTestChildren(HitTestResult result, { double radius, double theta }) { }
|
|
|
|
double deltaRadius;
|
|
double deltaTheta;
|
|
}
|
|
|
|
abstract class RenderDecoratedSector extends RenderSector {
|
|
|
|
RenderDecoratedSector(BoxDecoration decoration) : _decoration = decoration;
|
|
|
|
BoxDecoration _decoration;
|
|
BoxDecoration get decoration => _decoration;
|
|
void set decoration (BoxDecoration value) {
|
|
if (value == _decoration)
|
|
return;
|
|
_decoration = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
// offset must point to the center of the circle
|
|
void paint(PaintingContext context, Offset offset) {
|
|
assert(deltaRadius != null);
|
|
assert(deltaTheta != null);
|
|
assert(parentData is SectorParentData);
|
|
|
|
if (_decoration == null)
|
|
return;
|
|
|
|
if (_decoration.backgroundColor != null) {
|
|
final Canvas canvas = context.canvas;
|
|
Paint paint = new Paint()..color = _decoration.backgroundColor;
|
|
Path path = new Path();
|
|
double outerRadius = (parentData.radius + deltaRadius);
|
|
Rect outerBounds = new Rect.fromLTRB(offset.dx-outerRadius, offset.dy-outerRadius, offset.dx+outerRadius, offset.dy+outerRadius);
|
|
path.arcTo(outerBounds, parentData.theta, deltaTheta, true);
|
|
double innerRadius = parentData.radius;
|
|
Rect innerBounds = new Rect.fromLTRB(offset.dx-innerRadius, offset.dy-innerRadius, offset.dx+innerRadius, offset.dy+innerRadius);
|
|
path.arcTo(innerBounds, parentData.theta + deltaTheta, -deltaTheta, false);
|
|
path.close();
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class SectorChildListParentData extends SectorParentData with ContainerParentDataMixin<RenderSector> { }
|
|
|
|
class RenderSectorWithChildren extends RenderDecoratedSector with ContainerRenderObjectMixin<RenderSector, SectorChildListParentData> {
|
|
RenderSectorWithChildren(BoxDecoration decoration) : super(decoration);
|
|
|
|
void hitTestChildren(HitTestResult result, { double radius, double theta }) {
|
|
RenderSector child = lastChild;
|
|
while (child != null) {
|
|
if (child.hitTest(result, radius: radius, theta: theta))
|
|
return;
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.previousSibling;
|
|
}
|
|
}
|
|
|
|
void visitChildren(RenderObjectVisitor visitor) {
|
|
RenderSector child = lastChild;
|
|
while (child != null) {
|
|
visitor(child);
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.previousSibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
class RenderSectorRing extends RenderSectorWithChildren {
|
|
// lays out RenderSector children in a ring
|
|
|
|
RenderSectorRing({
|
|
BoxDecoration decoration,
|
|
double deltaRadius: double.INFINITY,
|
|
double padding: 0.0
|
|
}) : _padding = padding, _desiredDeltaRadius = deltaRadius, super(decoration);
|
|
|
|
double _desiredDeltaRadius;
|
|
double get desiredDeltaRadius => _desiredDeltaRadius;
|
|
void set desiredDeltaRadius(double value) {
|
|
assert(value != null);
|
|
if (_desiredDeltaRadius != value) {
|
|
_desiredDeltaRadius = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
double _padding;
|
|
double get padding => _padding;
|
|
void set padding(double value) {
|
|
// TODO(ianh): avoid code duplication
|
|
assert(value != null);
|
|
if (_padding != value) {
|
|
_padding = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
void setupParentData(RenderObject child) {
|
|
// TODO(ianh): avoid code duplication
|
|
if (child.parentData is! SectorChildListParentData)
|
|
child.parentData = new SectorChildListParentData();
|
|
}
|
|
|
|
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
|
|
double outerDeltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
|
|
double innerDeltaRadius = outerDeltaRadius - padding * 2.0;
|
|
double childRadius = radius + padding;
|
|
double paddingTheta = math.atan(padding / (radius + outerDeltaRadius));
|
|
double innerTheta = paddingTheta; // increments with each child
|
|
double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
SectorConstraints innerConstraints = new SectorConstraints(
|
|
maxDeltaRadius: innerDeltaRadius,
|
|
maxDeltaTheta: remainingDeltaTheta
|
|
);
|
|
SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius);
|
|
innerTheta += childDimensions.deltaTheta;
|
|
remainingDeltaTheta -= childDimensions.deltaTheta;
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
if (child != null) {
|
|
innerTheta += paddingTheta;
|
|
remainingDeltaTheta -= paddingTheta;
|
|
}
|
|
}
|
|
return new SectorDimensions.withConstraints(constraints,
|
|
deltaRadius: outerDeltaRadius,
|
|
deltaTheta: innerTheta);
|
|
}
|
|
|
|
void performLayout() {
|
|
assert(this.parentData is SectorParentData);
|
|
deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
|
|
assert(deltaRadius < double.INFINITY);
|
|
double innerDeltaRadius = deltaRadius - padding * 2.0;
|
|
double childRadius = this.parentData.radius + padding;
|
|
double paddingTheta = math.atan(padding / (this.parentData.radius + deltaRadius));
|
|
double innerTheta = paddingTheta; // increments with each child
|
|
double remainingDeltaTheta = constraints.maxDeltaTheta - (innerTheta + paddingTheta);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
SectorConstraints innerConstraints = new SectorConstraints(
|
|
maxDeltaRadius: innerDeltaRadius,
|
|
maxDeltaTheta: remainingDeltaTheta
|
|
);
|
|
assert(child.parentData is SectorParentData);
|
|
child.parentData.theta = innerTheta;
|
|
child.parentData.radius = childRadius;
|
|
child.layout(innerConstraints, parentUsesSize: true);
|
|
innerTheta += child.deltaTheta;
|
|
remainingDeltaTheta -= child.deltaTheta;
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
if (child != null) {
|
|
innerTheta += paddingTheta;
|
|
remainingDeltaTheta -= paddingTheta;
|
|
}
|
|
}
|
|
deltaTheta = innerTheta;
|
|
}
|
|
|
|
// offset must point to the center of our circle
|
|
// each sector then knows how to paint itself at its location
|
|
void paint(PaintingContext context, Offset offset) {
|
|
// TODO(ianh): avoid code duplication
|
|
super.paint(context, offset);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
context.paintChild(child, offset);
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class RenderSectorSlice extends RenderSectorWithChildren {
|
|
// lays out RenderSector children in a stack
|
|
|
|
RenderSectorSlice({
|
|
BoxDecoration decoration,
|
|
double deltaTheta: kTwoPi,
|
|
double padding: 0.0
|
|
}) : _padding = padding, _desiredDeltaTheta = deltaTheta, super(decoration);
|
|
|
|
double _desiredDeltaTheta;
|
|
double get desiredDeltaTheta => _desiredDeltaTheta;
|
|
void set desiredDeltaTheta(double value) {
|
|
assert(value != null);
|
|
if (_desiredDeltaTheta != value) {
|
|
_desiredDeltaTheta = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
double _padding;
|
|
double get padding => _padding;
|
|
void set padding(double value) {
|
|
// TODO(ianh): avoid code duplication
|
|
assert(value != null);
|
|
if (_padding != value) {
|
|
_padding = value;
|
|
markNeedsLayout();
|
|
}
|
|
}
|
|
|
|
void setupParentData(RenderObject child) {
|
|
// TODO(ianh): avoid code duplication
|
|
if (child.parentData is! SectorChildListParentData)
|
|
child.parentData = new SectorChildListParentData();
|
|
}
|
|
|
|
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
|
|
assert(this.parentData is SectorParentData);
|
|
double paddingTheta = math.atan(padding / this.parentData.radius);
|
|
double outerDeltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
|
|
double innerDeltaTheta = outerDeltaTheta - paddingTheta * 2.0;
|
|
double childRadius = this.parentData.radius + padding;
|
|
double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
SectorConstraints innerConstraints = new SectorConstraints(
|
|
maxDeltaRadius: remainingDeltaRadius,
|
|
maxDeltaTheta: innerDeltaTheta
|
|
);
|
|
SectorDimensions childDimensions = child.getIntrinsicDimensions(innerConstraints, childRadius);
|
|
childRadius += childDimensions.deltaRadius;
|
|
remainingDeltaRadius -= childDimensions.deltaRadius;
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
childRadius += padding;
|
|
remainingDeltaRadius -= padding;
|
|
}
|
|
return new SectorDimensions.withConstraints(constraints,
|
|
deltaRadius: childRadius - this.parentData.radius,
|
|
deltaTheta: outerDeltaTheta);
|
|
}
|
|
|
|
void performLayout() {
|
|
assert(this.parentData is SectorParentData);
|
|
deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
|
|
assert(deltaTheta <= kTwoPi);
|
|
double paddingTheta = math.atan(padding / this.parentData.radius);
|
|
double innerTheta = this.parentData.theta + paddingTheta;
|
|
double innerDeltaTheta = deltaTheta - paddingTheta * 2.0;
|
|
double childRadius = this.parentData.radius + padding;
|
|
double remainingDeltaRadius = constraints.maxDeltaRadius - (padding * 2.0);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
SectorConstraints innerConstraints = new SectorConstraints(
|
|
maxDeltaRadius: remainingDeltaRadius,
|
|
maxDeltaTheta: innerDeltaTheta
|
|
);
|
|
child.parentData.theta = innerTheta;
|
|
child.parentData.radius = childRadius;
|
|
child.layout(innerConstraints, parentUsesSize: true);
|
|
childRadius += child.deltaRadius;
|
|
remainingDeltaRadius -= child.deltaRadius;
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
childRadius += padding;
|
|
remainingDeltaRadius -= padding;
|
|
}
|
|
deltaRadius = childRadius - this.parentData.radius;
|
|
}
|
|
|
|
// offset must point to the center of our circle
|
|
// each sector then knows how to paint itself at its location
|
|
void paint(PaintingContext context, Offset offset) {
|
|
// TODO(ianh): avoid code duplication
|
|
super.paint(context, offset);
|
|
RenderSector child = firstChild;
|
|
while (child != null) {
|
|
assert(child.parentData is SectorChildListParentData);
|
|
context.paintChild(child, offset);
|
|
final SectorChildListParentData childParentData = child.parentData;
|
|
child = childParentData.nextSibling;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChildMixin<RenderSector> {
|
|
|
|
RenderBoxToRenderSectorAdapter({ double innerRadius: 0.0, RenderSector child }) :
|
|
_innerRadius = innerRadius {
|
|
this.child = child;
|
|
}
|
|
|
|
double _innerRadius;
|
|
double get innerRadius => _innerRadius;
|
|
void set innerRadius(double value) {
|
|
_innerRadius = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! SectorParentData)
|
|
child.parentData = new SectorParentData();
|
|
}
|
|
|
|
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
|
if (child == null)
|
|
return super.getMinIntrinsicWidth(constraints);
|
|
return getIntrinsicDimensions(constraints).width;
|
|
}
|
|
|
|
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
|
if (child == null)
|
|
return super.getMaxIntrinsicWidth(constraints);
|
|
return getIntrinsicDimensions(constraints).width;
|
|
}
|
|
|
|
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
|
if (child == null)
|
|
return super.getMinIntrinsicHeight(constraints);
|
|
return getIntrinsicDimensions(constraints).height;
|
|
}
|
|
|
|
double getMaxIntrinsicHeight(BoxConstraints constraints) {
|
|
if (child == null)
|
|
return super.getMaxIntrinsicHeight(constraints);
|
|
return getIntrinsicDimensions(constraints).height;
|
|
}
|
|
|
|
Size getIntrinsicDimensions(BoxConstraints constraints) {
|
|
assert(child is RenderSector);
|
|
assert(child.parentData is SectorParentData);
|
|
assert(constraints.maxWidth < double.INFINITY || constraints.maxHeight < double.INFINITY);
|
|
double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius;
|
|
SectorDimensions childDimensions = child.getIntrinsicDimensions(new SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), innerRadius);
|
|
double dimension = (innerRadius + childDimensions.deltaRadius) * 2.0;
|
|
return constraints.constrain(new Size(dimension, dimension));
|
|
}
|
|
|
|
void performLayout() {
|
|
if (child == null) {
|
|
size = constraints.constrain(Size.zero);
|
|
} else {
|
|
assert(child is RenderSector);
|
|
assert(constraints.maxWidth < double.INFINITY || constraints.maxHeight < double.INFINITY);
|
|
double maxChildDeltaRadius = math.min(constraints.maxWidth, constraints.maxHeight) / 2.0 - innerRadius;
|
|
assert(child.parentData is SectorParentData);
|
|
child.parentData.radius = innerRadius;
|
|
child.parentData.theta = 0.0;
|
|
child.layout(new SectorConstraints(maxDeltaRadius: maxChildDeltaRadius), parentUsesSize: true);
|
|
double dimension = (innerRadius + child.deltaRadius) * 2.0;
|
|
size = constraints.constrain(new Size(dimension, dimension));
|
|
}
|
|
}
|
|
|
|
void paint(PaintingContext context, Offset offset) {
|
|
super.paint(context, offset);
|
|
if (child != null) {
|
|
Rect bounds = offset & size;
|
|
// we move the offset to the center of the circle for the RenderSectors
|
|
context.paintChild(child, bounds.center.toOffset());
|
|
}
|
|
}
|
|
|
|
bool hitTest(HitTestResult result, { Point position }) {
|
|
if (child == null)
|
|
return false;
|
|
double x = position.x;
|
|
double y = position.y;
|
|
// translate to our origin
|
|
x -= size.width/2.0;
|
|
y -= size.height/2.0;
|
|
// convert to radius/theta
|
|
double radius = math.sqrt(x*x+y*y);
|
|
double theta = (math.atan2(x, -y) - math.PI/2.0) % kTwoPi;
|
|
if (radius < innerRadius)
|
|
return false;
|
|
if (radius >= innerRadius + child.deltaRadius)
|
|
return false;
|
|
if (theta > child.deltaTheta)
|
|
return false;
|
|
child.hitTest(result, radius: radius, theta: theta);
|
|
result.add(new BoxHitTestEntry(this, position));
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
class RenderSolidColor extends RenderDecoratedSector {
|
|
RenderSolidColor(Color backgroundColor, {
|
|
this.desiredDeltaRadius: double.INFINITY,
|
|
this.desiredDeltaTheta: kTwoPi
|
|
}) : this.backgroundColor = backgroundColor,
|
|
super(new BoxDecoration(backgroundColor: backgroundColor));
|
|
|
|
double desiredDeltaRadius;
|
|
double desiredDeltaTheta;
|
|
final Color backgroundColor;
|
|
|
|
SectorDimensions getIntrinsicDimensions(SectorConstraints constraints, double radius) {
|
|
return new SectorDimensions.withConstraints(constraints, deltaTheta: desiredDeltaTheta);
|
|
}
|
|
|
|
void performLayout() {
|
|
deltaRadius = constraints.constrainDeltaRadius(desiredDeltaRadius);
|
|
deltaTheta = constraints.constrainDeltaTheta(desiredDeltaTheta);
|
|
}
|
|
|
|
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
|
if (event is PointerDownEvent) {
|
|
decoration = new BoxDecoration(backgroundColor: const Color(0xFFFF0000));
|
|
} else if (event is PointerUpEvent) {
|
|
decoration = new BoxDecoration(backgroundColor: backgroundColor);
|
|
}
|
|
}
|
|
}
|