| // Copyright 2014 The Flutter 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 'package:flutter_test/flutter_test.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| @immutable |
| class Pair<T> { |
| const Pair(this.first, this.second); |
| final T? first; |
| final T second; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is Pair<T> && other.first == first && other.second == second; |
| } |
| |
| @override |
| int get hashCode => hashValues(first, second); |
| |
| @override |
| String toString() => '($first,$second)'; |
| } |
| |
| /// Widget that will layout one child in the top half of this widget's size |
| /// and the other child in the bottom half. It will swap which child is on top |
| /// and which is on bottom every time the widget is rendered. |
| abstract class Swapper extends RenderObjectWidget { |
| const Swapper({ this.stable, this.swapper }); |
| |
| final Widget? stable; |
| final Widget? swapper; |
| |
| @override |
| SwapperElement createElement(); |
| |
| @override |
| RenderObject createRenderObject(BuildContext context) => RenderSwapper(); |
| } |
| |
| class SwapperWithProperOverrides extends Swapper { |
| const SwapperWithProperOverrides({ |
| Widget? stable, |
| Widget? swapper, |
| }) : super(stable: stable, swapper: swapper); |
| |
| @override |
| SwapperElement createElement() => SwapperElementWithProperOverrides(this); |
| } |
| |
| class SwapperWithNoOverrides extends Swapper { |
| const SwapperWithNoOverrides({ |
| Widget? stable, |
| Widget? swapper, |
| }) : super(stable: stable, swapper: swapper); |
| |
| @override |
| SwapperElement createElement() => SwapperElementWithNoOverrides(this); |
| } |
| |
| class SwapperWithDeprecatedOverrides extends Swapper { |
| const SwapperWithDeprecatedOverrides({ |
| Widget? stable, |
| Widget? swapper, |
| }) : super(stable: stable, swapper: swapper); |
| |
| @override |
| SwapperElement createElement() => SwapperElementWithDeprecatedOverrides(this); |
| } |
| |
| abstract class SwapperElement extends RenderObjectElement { |
| SwapperElement(Swapper widget) : super(widget); |
| |
| Element? stable; |
| Element? swapper; |
| bool swapperIsOnTop = true; |
| List<dynamic> insertSlots = <dynamic>[]; |
| List<Pair<dynamic>> moveSlots = <Pair<dynamic>>[]; |
| List<dynamic> removeSlots = <dynamic>[]; |
| |
| @override |
| Swapper get widget => super.widget as Swapper; |
| |
| @override |
| RenderSwapper get renderObject => super.renderObject as RenderSwapper; |
| |
| @override |
| void visitChildren(ElementVisitor visitor) { |
| if (stable != null) |
| visitor(stable!); |
| if (swapper != null) |
| visitor(swapper!); |
| } |
| |
| @override |
| void update(Swapper newWidget) { |
| super.update(newWidget); |
| _updateChildren(newWidget); |
| } |
| |
| @override |
| void mount(Element? parent, dynamic newSlot) { |
| super.mount(parent, newSlot); |
| _updateChildren(widget); |
| } |
| |
| void _updateChildren(Swapper widget) { |
| stable = updateChild(stable, widget.stable, 'stable'); |
| swapper = updateChild(swapper, widget.swapper, swapperIsOnTop); |
| swapperIsOnTop = !swapperIsOnTop; |
| } |
| } |
| |
| class SwapperElementWithProperOverrides extends SwapperElement { |
| SwapperElementWithProperOverrides(Swapper widget) : super(widget); |
| |
| @override |
| void insertRenderObjectChild(RenderBox child, dynamic slot) { |
| insertSlots.add(slot); |
| assert(child != null); |
| if (slot == 'stable') |
| renderObject.stable = child; |
| else |
| renderObject.setSwapper(child, slot as bool); |
| } |
| |
| @override |
| void moveRenderObjectChild(RenderBox child, bool oldIsOnTop, bool newIsOnTop) { |
| moveSlots.add(Pair<bool>(oldIsOnTop, newIsOnTop)); |
| assert(oldIsOnTop == !newIsOnTop); |
| renderObject.setSwapper(child, newIsOnTop); |
| } |
| |
| @override |
| void removeRenderObjectChild(RenderBox child, dynamic slot) { |
| removeSlots.add(slot); |
| if (slot == 'stable') |
| renderObject.stable = null; |
| else |
| renderObject.setSwapper(null, slot as bool); |
| } |
| } |
| |
| class SwapperElementWithNoOverrides extends SwapperElement { |
| SwapperElementWithNoOverrides(Swapper widget) : super(widget); |
| } |
| |
| class SwapperElementWithDeprecatedOverrides extends SwapperElement { |
| SwapperElementWithDeprecatedOverrides(Swapper widget) : super(widget); |
| |
| @override |
| // ignore: must_call_super |
| void insertChildRenderObject(RenderBox child, dynamic slot) { |
| insertSlots.add(slot); |
| assert(child != null); |
| if (slot == 'stable') |
| renderObject.stable = child; |
| else |
| renderObject.setSwapper(child, slot as bool); |
| } |
| |
| @override |
| // ignore: must_call_super |
| void moveChildRenderObject(RenderBox child, bool isOnTop) { |
| moveSlots.add(Pair<bool>(null, isOnTop)); |
| renderObject.setSwapper(child, isOnTop); |
| } |
| |
| @override |
| // ignore: must_call_super |
| void removeChildRenderObject(RenderBox child) { |
| removeSlots.add(null); |
| if (child == renderObject._stable) |
| renderObject.stable = null; |
| else |
| renderObject.setSwapper(null, swapperIsOnTop); |
| } |
| } |
| |
| class RenderSwapper extends RenderBox { |
| RenderBox? _stable; |
| RenderBox? get stable => _stable; |
| set stable(RenderBox? child) { |
| if (child == _stable) |
| return; |
| if (_stable != null) |
| dropChild(_stable!); |
| _stable = child; |
| if (child != null) |
| adoptChild(child); |
| } |
| |
| bool? _swapperIsOnTop; |
| RenderBox? _swapper; |
| RenderBox? get swapper => _swapper; |
| void setSwapper(RenderBox? child, bool isOnTop) { |
| if (isOnTop != _swapperIsOnTop) { |
| _swapperIsOnTop = isOnTop; |
| markNeedsLayout(); |
| } |
| if (child == _swapper) |
| return; |
| if (_swapper != null) |
| dropChild(_swapper!); |
| _swapper = child; |
| if (child != null) |
| adoptChild(child); |
| } |
| |
| @override |
| void visitChildren(RenderObjectVisitor visitor) { |
| if (_stable != null) |
| visitor(_stable!); |
| if (_swapper != null) |
| visitor(_swapper!); |
| } |
| |
| @override |
| void attach(PipelineOwner owner) { |
| super.attach(owner); |
| visitChildren((RenderObject child) => child.attach(owner)); |
| } |
| |
| @override |
| void detach() { |
| super.detach(); |
| visitChildren((RenderObject child) => child.detach()); |
| } |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| return constraints.biggest; |
| } |
| |
| @override |
| void performLayout() { |
| assert(constraints.hasBoundedWidth); |
| assert(constraints.hasTightHeight); |
| size = constraints.biggest; |
| const Offset topOffset = Offset.zero; |
| final Offset bottomOffset = Offset(0, size.height / 2); |
| final BoxConstraints childConstraints = constraints.copyWith( |
| minHeight: constraints.minHeight / 2, |
| maxHeight: constraints.maxHeight / 2, |
| ); |
| if (_stable != null) { |
| final BoxParentData stableParentData = _stable!.parentData! as BoxParentData; |
| _stable!.layout(childConstraints); |
| stableParentData.offset = _swapperIsOnTop! ? bottomOffset : topOffset; |
| } |
| if (_swapper != null) { |
| final BoxParentData swapperParentData = _swapper!.parentData! as BoxParentData; |
| _swapper!.layout(childConstraints); |
| swapperParentData.offset = _swapperIsOnTop! ? topOffset : bottomOffset; |
| } |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| visitChildren((RenderObject child) { |
| final BoxParentData childParentData = child.parentData! as BoxParentData; |
| context.paintChild(child, offset + childParentData.offset); |
| }); |
| } |
| |
| @override |
| void redepthChildren() { |
| visitChildren((RenderObject child) => redepthChild(child)); |
| } |
| } |
| |
| BoxParentData parentDataFor(RenderObject renderObject) => renderObject.parentData! as BoxParentData; |
| |
| void main() { |
| testWidgets('RenderObjectElement *RenderObjectChild methods get called with correct arguments', (WidgetTester tester) async { |
| const Key redKey = ValueKey<String>('red'); |
| const Key blueKey = ValueKey<String>('blue'); |
| Widget widget() { |
| return SwapperWithProperOverrides( |
| stable: ColoredBox( |
| key: redKey, |
| color: Color(nonconst(0xffff0000)), |
| ), |
| swapper: ColoredBox( |
| key: blueKey, |
| color: Color(nonconst(0xff0000ff)), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(widget()); |
| final SwapperElement swapper = tester.element<SwapperElement>(find.byType(SwapperWithProperOverrides)); |
| final RenderBox redBox = tester.renderObject<RenderBox>(find.byKey(redKey)); |
| final RenderBox blueBox = tester.renderObject<RenderBox>(find.byKey(blueKey)); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.insertSlots, contains('stable')); |
| expect(swapper.insertSlots, contains(true)); |
| expect(swapper.moveSlots, isEmpty); |
| expect(swapper.removeSlots, isEmpty); |
| expect(parentDataFor(redBox).offset, const Offset(0, 300)); |
| expect(parentDataFor(blueBox).offset, Offset.zero); |
| await tester.pumpWidget(widget()); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.moveSlots.length, 1); |
| expect(swapper.moveSlots, contains(const Pair<bool>(true, false))); |
| expect(swapper.removeSlots, isEmpty); |
| expect(parentDataFor(redBox).offset, Offset.zero); |
| expect(parentDataFor(blueBox).offset, const Offset(0, 300)); |
| await tester.pumpWidget(const SwapperWithProperOverrides()); |
| expect(redBox.attached, false); |
| expect(blueBox.attached, false); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.moveSlots.length, 1); |
| expect(swapper.removeSlots.length, 2); |
| expect(swapper.removeSlots, contains('stable')); |
| expect(swapper.removeSlots, contains(false)); |
| }); |
| |
| testWidgets('RenderObjectElement *RenderObjectChild methods delegate to deprecated methods', (WidgetTester tester) async { |
| const Key redKey = ValueKey<String>('red'); |
| const Key blueKey = ValueKey<String>('blue'); |
| Widget widget() { |
| return SwapperWithDeprecatedOverrides( |
| stable: ColoredBox( |
| key: redKey, |
| color: Color(nonconst(0xffff0000)), |
| ), |
| swapper: ColoredBox( |
| key: blueKey, |
| color: Color(nonconst(0xff0000ff)), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(widget()); |
| final SwapperElement swapper = tester.element<SwapperElement>(find.byType(SwapperWithDeprecatedOverrides)); |
| final RenderBox redBox = tester.renderObject<RenderBox>(find.byKey(redKey)); |
| final RenderBox blueBox = tester.renderObject<RenderBox>(find.byKey(blueKey)); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.insertSlots, contains('stable')); |
| expect(swapper.insertSlots, contains(true)); |
| expect(swapper.moveSlots, isEmpty); |
| expect(swapper.removeSlots, isEmpty); |
| expect(parentDataFor(redBox).offset, const Offset(0, 300)); |
| expect(parentDataFor(blueBox).offset, Offset.zero); |
| await tester.pumpWidget(widget()); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.moveSlots.length, 1); |
| expect(swapper.moveSlots, contains(const Pair<bool>(null, false))); |
| expect(swapper.removeSlots, isEmpty); |
| expect(parentDataFor(redBox).offset, Offset.zero); |
| expect(parentDataFor(blueBox).offset, const Offset(0, 300)); |
| await tester.pumpWidget(const SwapperWithDeprecatedOverrides()); |
| expect(redBox.attached, false); |
| expect(blueBox.attached, false); |
| expect(swapper.insertSlots.length, 2); |
| expect(swapper.moveSlots.length, 1); |
| expect(swapper.removeSlots.length, 2); |
| expect(swapper.removeSlots, <bool?>[null,null]); |
| }); |
| |
| testWidgets('RenderObjectElement *ChildRenderObject methods fail with deprecation message', (WidgetTester tester) async { |
| const Key redKey = ValueKey<String>('red'); |
| const Key blueKey = ValueKey<String>('blue'); |
| Widget widget() { |
| return SwapperWithNoOverrides( |
| stable: ColoredBox( |
| key: redKey, |
| color: Color(nonconst(0xffff0000)), |
| ), |
| swapper: ColoredBox( |
| key: blueKey, |
| color: Color(nonconst(0xff0000ff)), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(widget()); |
| final FlutterError error = tester.takeException() as FlutterError; |
| final ErrorSummary summary = error.diagnostics.first as ErrorSummary; |
| expect(summary.toString(), contains('deprecated')); |
| }); |
| } |