diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart index 65247d731a2..f4c026b57b0 100644 --- a/packages/flutter/lib/src/painting/basic_types.dart +++ b/packages/flutter/lib/src/painting/basic_types.dart @@ -52,3 +52,41 @@ export 'dart:ui' show // - window, WindowPadding // These are generally wrapped by other APIs so we always refer to them directly // as ui.* to avoid making them seem like high-level APIs. + +/// The description of the difference between two objects, in the context of how +/// it will affect the rendering. +/// +/// Used by [TextSpan.compareTo] and [TextStyle.compareTo]. +/// +/// The values in this enum are ordered such that they are in increasing order +/// of cost. A value with index N implies all the values with index less than N. +/// For example, [layout] (index 3) implies [paint] (2). +enum RenderComparison { + /// The two objects are identical (meaning deeply equal, not necessarily + /// [identical]). + identical, + + /// The two objects are identical for the purpose of layout, but may be different + /// in other ways. + /// + /// For example, maybe some event handlers changed. + metadata, + + /// The two objects are different but only in ways that affect paint, not layout. + /// + /// For example, only the color is changed. + /// + /// [RenderObject.markNeedsPaint] would be necessary to handle this kind of + /// change in a render object. + paint, + + /// The two objects are different in ways that affect layout (and therefore paint). + /// + /// For example, the size is changed. + /// + /// This is the most drastic level of change possible. + /// + /// [RenderObject.markNeedsLayout] would be necessary to handle this kind of + /// change in a render object. + layout, +} diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart index a7f068d7d80..feb610e4a2e 100644 --- a/packages/flutter/lib/src/painting/text_span.dart +++ b/packages/flutter/lib/src/painting/text_span.dart @@ -317,6 +317,39 @@ class TextSpan { return true; } + /// Describe the difference between this text span and another, in terms of + /// how much damage it will make to the rendering. The comparison is deep. + /// + /// See also: + /// + /// * [TextStyle.compareTo], which does the same thing for [TextStyle]s. + RenderComparison compareTo(TextSpan other) { + if (identical(this, other)) + return RenderComparison.identical; + if (other.text != text || + children?.length != other.children?.length || + (style == null) != (other.style == null)) + return RenderComparison.layout; + RenderComparison result = recognizer == other.recognizer ? RenderComparison.identical : RenderComparison.metadata; + if (style != null) { + final RenderComparison candidate = style.compareTo(other.style); + if (candidate.index > result.index) + result = candidate; + if (result == RenderComparison.layout) + return result; + } + if (children != null) { + for (int index = 0; index < children.length; index += 1) { + final RenderComparison candidate = children[index].compareTo(other.children[index]); + if (candidate.index > result.index) + result = candidate; + if (result == RenderComparison.layout) + return result; + } + } + return result; + } + @override bool operator ==(dynamic other) { if (identical(this, other)) diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index c7595b8fc15..f2605523726 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -387,6 +387,33 @@ class TextStyle { ); } + /// Describe the difference between this style and another, in terms of how + /// much damage it will make to the rendering. + /// + /// See also: + /// + /// * [TextSpan.compareTo], which does the same thing for entire [TextSpan]s. + RenderComparison compareTo(TextStyle other) { + if (identical(this, other)) + return RenderComparison.identical; + if (inherit != other.inherit || + fontFamily != other.fontFamily || + fontSize != other.fontSize || + fontWeight != other.fontWeight || + fontStyle != other.fontStyle || + letterSpacing != other.letterSpacing || + wordSpacing != other.wordSpacing || + textBaseline != other.textBaseline || + height != other.height) + return RenderComparison.layout; + if (color != other.color || + decoration != other.decoration || + decorationColor != other.decorationColor || + decorationStyle != other.decorationStyle) + return RenderComparison.paint; + return RenderComparison.identical; + } + @override bool operator ==(dynamic other) { if (identical(this, other)) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index d720c2d956b..d84ca71e431 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -1559,6 +1559,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// to condition their runtime behavior on whether they are dirty or not, /// since they should only be marked dirty immediately prior to being laid /// out and painted. + /// + /// It is intended to be used by tests and asserts. bool get debugNeedsLayout { bool result; assert(() { @@ -2133,6 +2135,28 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { _needsCompositingBitsUpdate = false; } + /// Whether this render object's paint information is dirty. + /// + /// This is only set in debug mode. In general, render objects should not need + /// to condition their runtime behavior on whether they are dirty or not, + /// since they should only be marked dirty immediately prior to being laid + /// out and painted. + /// + /// It is intended to be used by tests and asserts. + /// + /// It is possible (and indeed, quite common) for [debugNeedsPaint] to be + /// false and [debugNeedsLayout] to be true. The render object will still be + /// repainted in the next frame when this is the case, because the + /// [markNeedsPaint] method is implicitly called by the framework after a + /// render object is laid out, prior to the paint phase. + bool get debugNeedsPaint { + bool result; + assert(() { + result = _needsPaint; + return true; + }); + return result; + } bool _needsPaint = true; /// Mark this render object as having changed its visual appearance. @@ -2145,6 +2169,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// /// This mechanism batches the painting work so that multiple sequential /// writes are coalesced, removing redundant computation. + /// + /// Once [markNeedsPaint] has been called on a render object, + /// [debugNeedsPaint] returns true for that render object until just after + /// the pipeline owner has called [paint] on the render object. void markNeedsPaint() { assert(owner == null || !owner.debugDoingPaint); if (_needsPaint) diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 51ef993009e..667929c55d9 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -64,11 +64,20 @@ class RenderParagraph extends RenderBox { TextSpan get text => _textPainter.text; set text(TextSpan value) { assert(value != null); - if (_textPainter.text == value) - return; - _textPainter.text = value; - _overflowShader = null; - markNeedsLayout(); + switch (_textPainter.text.compareTo(value)) { + case RenderComparison.identical: + case RenderComparison.metadata: + return; + case RenderComparison.paint: + _textPainter.text = value; + markNeedsPaint(); + break; + case RenderComparison.layout: + _textPainter.text = value; + _overflowShader = null; + markNeedsLayout(); + break; + } } /// How the text should be aligned horizontally. diff --git a/packages/flutter/test/rendering/paragraph_test.dart b/packages/flutter/test/rendering/paragraph_test.dart index e90d7f28d55..ed01b1e7d23 100644 --- a/packages/flutter/test/rendering/paragraph_test.dart +++ b/packages/flutter/test/rendering/paragraph_test.dart @@ -177,6 +177,36 @@ void main() { expect(paragraph.size.height, 30.0); }); + test('changing color does not do layout', () { + final RenderParagraph paragraph = new RenderParagraph( + const TextSpan( + text: 'Hello', + style: const TextStyle(color: const Color(0xFF000000)), + ), + ); + layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0), phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + paragraph.text = const TextSpan( + text: 'Hello World', + style: const TextStyle(color: const Color(0xFF000000)), + ); + expect(paragraph.debugNeedsLayout, isTrue); + expect(paragraph.debugNeedsPaint, isFalse); + pumpFrame(phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + paragraph.text = const TextSpan( + text: 'Hello World', + style: const TextStyle(color: const Color(0xFFFFFFFF)), + ); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isTrue); + pumpFrame(phase: EnginePhase.paint); + expect(paragraph.debugNeedsLayout, isFalse); + expect(paragraph.debugNeedsPaint, isFalse); + }); + test('toStringDeep', () { final RenderParagraph paragraph = new RenderParagraph( const TextSpan(text: _kText),