blob: 5890e689355ffa1c07d0984cfe02dbb8ea0e55dd [file] [log] [blame] [edit]
// 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 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
// This returns render paragraph of the Tab label text.
RenderParagraph getTabText(WidgetTester tester, String text) {
return tester.renderObject<RenderParagraph>(
find.descendant(
of: find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_TabStyle'),
matching: find.text(text),
),
);
}
// This creates and returns a TabController.
TabController createTabController({
required int length,
required TickerProvider vsync,
int initialIndex = 0,
Duration? animationDuration,
}) {
final TabController result = TabController(
length: length,
vsync: vsync,
initialIndex: initialIndex,
animationDuration: animationDuration,
);
addTearDown(result.dispose);
return result;
}
// This widget is used to test widget state in the tabs_test.dart file.
class TabStateMarker extends StatefulWidget {
const TabStateMarker({super.key, this.child});
final Widget? child;
@override
TabStateMarkerState createState() => TabStateMarkerState();
}
class TabStateMarkerState extends State<TabStateMarker> {
String? marker;
@override
Widget build(BuildContext context) {
return widget.child ?? Container();
}
}
// Tab controller builder for TabControllerFrame widget.
typedef TabControllerFrameBuilder = Widget Function(BuildContext context, TabController controller);
// This widget creates a TabController and passes it to the builder.
class TabControllerFrame extends StatefulWidget {
const TabControllerFrame({
super.key,
required this.length,
this.initialIndex = 0,
required this.builder,
});
final int length;
final int initialIndex;
final TabControllerFrameBuilder builder;
@override
TabControllerFrameState createState() => TabControllerFrameState();
}
class TabControllerFrameState extends State<TabControllerFrame>
with SingleTickerProviderStateMixin {
late TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(
vsync: this,
length: widget.length,
initialIndex: widget.initialIndex,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(context, _controller);
}
}
// Test utility class to test tab indicator drawing.
class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
TabIndicatorRecordingCanvas(this.indicatorColor);
final Color indicatorColor;
late Rect indicatorRect;
@override
void drawLine(Offset p1, Offset p2, Paint paint) {
// Assuming that the indicatorWeight is 2.0, the default.
const double indicatorWeight = 2.0;
if (paint.color == indicatorColor) {
indicatorRect = Rect.fromPoints(p1, p2).inflate(indicatorWeight / 2.0);
}
}
}
// This creates a Fake implementation of ScrollMetrics.
class TabMockScrollMetrics extends Fake implements ScrollMetrics {}
class TabBarTestScrollPhysics extends ScrollPhysics {
const TabBarTestScrollPhysics({super.parent});
@override
TabBarTestScrollPhysics applyTo(ScrollPhysics? ancestor) {
return TabBarTestScrollPhysics(parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
return offset == 10 ? 20 : offset;
}
static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio(
mass: 0.5,
stiffness: 500.0,
ratio: 1.1,
);
@override
SpringDescription get spring => _kDefaultSpring;
}
// This widget is used to log the lifecycle of the TabBarView children.
class TabBody extends StatefulWidget {
const TabBody({super.key, required this.index, required this.log, this.marker = ''});
final int index;
final List<String> log;
final String marker;
@override
State<TabBody> createState() => TabBodyState();
}
class TabBodyState extends State<TabBody> {
@override
void initState() {
widget.log.add('init: ${widget.index}');
super.initState();
}
@override
void didUpdateWidget(TabBody oldWidget) {
super.didUpdateWidget(oldWidget);
// To keep the logging straight, widgets must not change their index.
assert(oldWidget.index == widget.index);
}
@override
void dispose() {
widget.log.add('dispose: ${widget.index}');
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: widget.marker.isEmpty
? Text('${widget.index}')
: Text('${widget.index}-${widget.marker}'),
);
}
}
// This widget is used to test the lifecycle of the TabBarView children with Ink widget.
class TabKeepAliveInk extends StatefulWidget {
const TabKeepAliveInk({super.key, required this.title});
final String title;
@override
State<StatefulWidget> createState() => _TabKeepAliveInkState();
}
class _TabKeepAliveInkState extends State<TabKeepAliveInk> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Ink(child: Text(widget.title));
}
}
// This widget is used to test the lifecycle of the TabBarView children.
class TabAlwaysKeepAliveWidget extends StatefulWidget {
const TabAlwaysKeepAliveWidget({super.key});
static String text = 'AlwaysKeepAlive';
@override
State<TabAlwaysKeepAliveWidget> createState() => _TabAlwaysKeepAliveWidgetState();
}
class _TabAlwaysKeepAliveWidgetState extends State<TabAlwaysKeepAliveWidget>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Text(TabAlwaysKeepAliveWidget.text);
}
}
// This decoration is used to test the indicator decoration image configuration.
class TestIndicatorDecoration extends Decoration {
final List<TestIndicatorBoxPainter> painters = <TestIndicatorBoxPainter>[];
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
final TestIndicatorBoxPainter painter = TestIndicatorBoxPainter();
painters.add(painter);
return painter;
}
}
class TestIndicatorBoxPainter extends BoxPainter {
ImageConfiguration? lastConfiguration;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
lastConfiguration = configuration;
}
}
// Ease out sine (decelerating).
double _decelerateInterpolation(double fraction) {
return math.sin((fraction * math.pi) / 2.0);
}
// Ease in sine (accelerating).
double _accelerateInterpolation(double fraction) {
return 1.0 - math.cos((fraction * math.pi) / 2.0);
}
// Returns Tab indicator RRect with elastic animation.
RRect tabIndicatorRRectElasticAnimation(
RenderBox tabBarBox,
Rect currentRect,
Rect fromRect,
Rect toRect,
double progress,
) {
const double indicatorWeight = 3.0;
final double leftFraction = _accelerateInterpolation(progress);
final double rightFraction = _decelerateInterpolation(progress);
return RRect.fromLTRBAndCorners(
lerpDouble(fromRect.left, toRect.left, leftFraction)!,
tabBarBox.size.height - indicatorWeight,
lerpDouble(fromRect.right, toRect.right, rightFraction)!,
tabBarBox.size.height,
topLeft: const Radius.circular(indicatorWeight),
topRight: const Radius.circular(indicatorWeight),
);
}