382 lines
12 KiB
Dart
382 lines
12 KiB
Dart
// 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/foundation.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
|
|
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'));
|
|
});
|
|
}
|