mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Implement CupertinoListSection and CupertinoListTile (#78732)
This commit is contained in:
parent
9d2f575403
commit
0c40945a67
@ -0,0 +1,95 @@
|
||||
// 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.
|
||||
|
||||
// Flutter code sample for base CupertinoListSection and CupertinoListTile.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
void main() => runApp(const CupertinoListSectionBaseApp());
|
||||
|
||||
class CupertinoListSectionBaseApp extends StatelessWidget {
|
||||
const CupertinoListSectionBaseApp({super.key});
|
||||
|
||||
static const String _title = 'Flutter Code Sample';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CupertinoApp(
|
||||
title: _title,
|
||||
home: MyStatelessWidget(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyStatelessWidget extends StatelessWidget {
|
||||
const MyStatelessWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CupertinoListSection(
|
||||
header: const Text('My Reminders'),
|
||||
children: <CupertinoListTile>[
|
||||
CupertinoListTile(
|
||||
title: const Text('Open pull request'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
trailing: const CupertinoListTileChevron(),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _SecondPage(text: 'Open pull request');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('Push to master'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.systemRed,
|
||||
),
|
||||
additionalInfo: const Text('Not available'),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('View last commit'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.activeOrange,
|
||||
),
|
||||
additionalInfo: const Text('12 days ago'),
|
||||
trailing: const CupertinoListTileChevron(),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _SecondPage(text: 'Last commit');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SecondPage extends StatelessWidget {
|
||||
const _SecondPage({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: Center(
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
// 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.
|
||||
|
||||
// Flutter code sample for inset CupertinoListSection and CupertinoListTile.
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
void main() => runApp(const CupertinoListSectionInsetApp());
|
||||
|
||||
class CupertinoListSectionInsetApp extends StatelessWidget {
|
||||
const CupertinoListSectionInsetApp({super.key});
|
||||
|
||||
static const String _title = 'Flutter Code Sample';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CupertinoApp(
|
||||
title: _title,
|
||||
home: MyStatelessWidget(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyStatelessWidget extends StatelessWidget {
|
||||
const MyStatelessWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CupertinoListSection.insetGrouped(
|
||||
header: const Text('My Reminders'),
|
||||
children: <CupertinoListTile>[
|
||||
CupertinoListTile.notched(
|
||||
title: const Text('Open pull request'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
trailing: const CupertinoListTileChevron(),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _SecondPage(text: 'Open pull request');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoListTile.notched(
|
||||
title: const Text('Push to master'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.systemRed,
|
||||
),
|
||||
additionalInfo: const Text('Not available'),
|
||||
),
|
||||
CupertinoListTile.notched(
|
||||
title: const Text('View last commit'),
|
||||
leading: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: CupertinoColors.activeOrange,
|
||||
),
|
||||
additionalInfo: const Text('12 days ago'),
|
||||
trailing: const CupertinoListTileChevron(),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const _SecondPage(text: 'Last commit');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SecondPage extends StatelessWidget {
|
||||
const _SecondPage({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: Center(
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// 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/cupertino.dart';
|
||||
import 'package:flutter_api_samples/cupertino/list_section/list_section_base.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Has exactly 1 CupertinoListSection base widget', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoListSectionBaseApp(),
|
||||
);
|
||||
|
||||
final Finder listSectionFinder = find.byType(CupertinoListSection);
|
||||
expect(listSectionFinder, findsOneWidget);
|
||||
|
||||
final CupertinoListSection listSectionWidget = tester.widget<CupertinoListSection>(listSectionFinder);
|
||||
expect(listSectionWidget.type, equals(CupertinoListSectionType.base));
|
||||
});
|
||||
|
||||
testWidgets('CupertinoListSection has 3 CupertinoListTile children', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoListSectionBaseApp(),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoListTile), findsNWidgets(3));
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// 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/cupertino.dart';
|
||||
import 'package:flutter_api_samples/cupertino/list_section/list_section_inset.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Has exactly 1 CupertinoListSection inset grouped widget', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoListSectionInsetApp(),
|
||||
);
|
||||
|
||||
final Finder listSectionFinder = find.byType(CupertinoListSection);
|
||||
expect(listSectionFinder, findsOneWidget);
|
||||
|
||||
final CupertinoListSection listSectionWidget = tester.widget<CupertinoListSection>(listSectionFinder);
|
||||
expect(listSectionWidget.type, equals(CupertinoListSectionType.insetGrouped));
|
||||
});
|
||||
|
||||
testWidgets('CupertinoListSection has 3 CupertinoListTile children', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoListSectionInsetApp(),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoListTile), findsNWidgets(3));
|
||||
});
|
||||
}
|
||||
@ -38,6 +38,8 @@ export 'src/cupertino/form_section.dart';
|
||||
export 'src/cupertino/icon_theme_data.dart';
|
||||
export 'src/cupertino/icons.dart';
|
||||
export 'src/cupertino/interface_level.dart';
|
||||
export 'src/cupertino/list_section.dart';
|
||||
export 'src/cupertino/list_tile.dart';
|
||||
export 'src/cupertino/localizations.dart';
|
||||
export 'src/cupertino/nav_bar.dart';
|
||||
export 'src/cupertino/page_scaffold.dart';
|
||||
|
||||
@ -5,28 +5,17 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
|
||||
// Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultHeaderMargin =
|
||||
EdgeInsetsDirectional.fromSTEB(20.0, 16.0, 20.0, 10.0);
|
||||
|
||||
// Standard footer margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultFooterMargin =
|
||||
EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
import 'list_section.dart';
|
||||
|
||||
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
|
||||
// iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMargin =
|
||||
EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
const EdgeInsetsDirectional _kFormDefaultInsetGroupedRowsMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
|
||||
// Used for iOS "Inset Grouped" border radius, estimated from SwiftUI's Forms in
|
||||
// iOS 14.2 SDK.
|
||||
// TODO(edrisian): This should be a rounded rectangle once that shape is added.
|
||||
const BorderRadius _kDefaultInsetGroupedBorderRadius =
|
||||
BorderRadius.all(Radius.circular(10.0));
|
||||
// Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kFormDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 16.0, 20.0, 10.0);
|
||||
|
||||
// Used to differentiate the edge-to-edge section with the centered section.
|
||||
enum _CupertinoFormSectionType { base, insetGrouped }
|
||||
// Standard footer margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kFormDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
|
||||
/// An iOS-style form section.
|
||||
///
|
||||
@ -65,6 +54,12 @@ enum _CupertinoFormSectionType { base, insetGrouped }
|
||||
/// If null, defaults to [CupertinoColors.systemGroupedBackground].
|
||||
///
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoFormRow], an iOS-style list tile, a typical child of
|
||||
/// [CupertinoFormSection].
|
||||
/// * [CupertinoListSection], an iOS-style list section.
|
||||
class CupertinoFormSection extends StatelessWidget {
|
||||
/// Creates a section that mimics standard iOS forms.
|
||||
///
|
||||
@ -107,7 +102,7 @@ class CupertinoFormSection extends StatelessWidget {
|
||||
this.backgroundColor = CupertinoColors.systemGroupedBackground,
|
||||
this.decoration,
|
||||
this.clipBehavior = Clip.none,
|
||||
}) : _type = _CupertinoFormSectionType.base,
|
||||
}) : _type = CupertinoListSectionType.base,
|
||||
assert(children.length > 0);
|
||||
|
||||
/// Creates a section that mimics standard "Inset Grouped" iOS forms.
|
||||
@ -149,14 +144,14 @@ class CupertinoFormSection extends StatelessWidget {
|
||||
required this.children,
|
||||
this.header,
|
||||
this.footer,
|
||||
this.margin = _kDefaultInsetGroupedRowsMargin,
|
||||
this.margin = _kFormDefaultInsetGroupedRowsMargin,
|
||||
this.backgroundColor = CupertinoColors.systemGroupedBackground,
|
||||
this.decoration,
|
||||
this.clipBehavior = Clip.none,
|
||||
}) : _type = _CupertinoFormSectionType.insetGrouped,
|
||||
}) : _type = CupertinoListSectionType.insetGrouped,
|
||||
assert(children.length > 0);
|
||||
|
||||
final _CupertinoFormSectionType _type;
|
||||
final CupertinoListSectionType _type;
|
||||
|
||||
/// Sets the form section header. The section header lies above the
|
||||
/// [children] rows.
|
||||
@ -203,116 +198,48 @@ class CupertinoFormSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color dividerColor = CupertinoColors.separator.resolveFrom(context);
|
||||
final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// Long divider is used for wrapping the top and bottom of rows.
|
||||
// Only used in _CupertinoFormSectionType.base mode
|
||||
final Widget longDivider = Container(
|
||||
color: dividerColor,
|
||||
height: dividerHeight,
|
||||
);
|
||||
|
||||
// Short divider is used between rows.
|
||||
// The value of the starting inset (15.0) is determined using SwiftUI's Form
|
||||
// separators in the iOS 14.2 SDK.
|
||||
final Widget shortDivider = Container(
|
||||
margin: const EdgeInsetsDirectional.only(start: 15.0),
|
||||
color: dividerColor,
|
||||
height: dividerHeight,
|
||||
);
|
||||
|
||||
// We construct childrenWithDividers as follows:
|
||||
// Insert a short divider between all rows.
|
||||
// If it is a `_CupertinoFormSectionType.base` type, add a long divider
|
||||
// to the top and bottom of the rows.
|
||||
assert(children.isNotEmpty);
|
||||
|
||||
final List<Widget> childrenWithDividers = <Widget>[];
|
||||
|
||||
if (_type == _CupertinoFormSectionType.base) {
|
||||
childrenWithDividers.add(longDivider);
|
||||
}
|
||||
|
||||
children.sublist(0, children.length - 1).forEach((Widget widget) {
|
||||
childrenWithDividers.add(widget);
|
||||
childrenWithDividers.add(shortDivider);
|
||||
});
|
||||
|
||||
childrenWithDividers.add(children.last);
|
||||
if (_type == _CupertinoFormSectionType.base) {
|
||||
childrenWithDividers.add(longDivider);
|
||||
}
|
||||
|
||||
final BorderRadius childrenGroupBorderRadius;
|
||||
switch (_type) {
|
||||
case _CupertinoFormSectionType.insetGrouped:
|
||||
childrenGroupBorderRadius = _kDefaultInsetGroupedBorderRadius;
|
||||
break;
|
||||
case _CupertinoFormSectionType.base:
|
||||
childrenGroupBorderRadius = BorderRadius.zero;
|
||||
break;
|
||||
}
|
||||
|
||||
// Refactored the decorate children group in one place to avoid repeating it
|
||||
// twice down bellow in the returned widget.
|
||||
final DecoratedBox decoratedChildrenGroup = DecoratedBox(
|
||||
decoration: decoration ?? BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
decoration?.color ?? CupertinoColors.secondarySystemGroupedBackground,
|
||||
context,
|
||||
),
|
||||
borderRadius: childrenGroupBorderRadius,
|
||||
),
|
||||
child: Column(
|
||||
children: childrenWithDividers,
|
||||
),
|
||||
);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(backgroundColor, context),
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (header != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
child: Padding(
|
||||
padding: _kDefaultHeaderMargin,
|
||||
child: header,
|
||||
),
|
||||
),
|
||||
final Widget? headerWidget = header == null
|
||||
? null
|
||||
: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
Padding(
|
||||
padding: margin,
|
||||
child: ClipRRect(
|
||||
borderRadius: childrenGroupBorderRadius,
|
||||
clipBehavior: clipBehavior,
|
||||
child: decoratedChildrenGroup,
|
||||
child: Padding(
|
||||
padding: _kFormDefaultHeaderMargin,
|
||||
child: header,
|
||||
));
|
||||
|
||||
final Widget? footerWidget = footer == null
|
||||
? null
|
||||
: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
),
|
||||
if (footer != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
child: Padding(
|
||||
padding: _kDefaultFooterMargin,
|
||||
child: footer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
child: Padding(
|
||||
padding: _kFormDefaultFooterMargin,
|
||||
child: footer,
|
||||
));
|
||||
|
||||
return _type == CupertinoListSectionType.base
|
||||
? CupertinoListSection(
|
||||
header: headerWidget,
|
||||
footer: footerWidget,
|
||||
margin: margin,
|
||||
backgroundColor: backgroundColor,
|
||||
decoration: decoration,
|
||||
clipBehavior: clipBehavior,
|
||||
hasLeading: false,
|
||||
children: children)
|
||||
: CupertinoListSection.insetGrouped(
|
||||
header: headerWidget,
|
||||
footer: footerWidget,
|
||||
margin: margin,
|
||||
backgroundColor: backgroundColor,
|
||||
decoration: decoration,
|
||||
clipBehavior: clipBehavior,
|
||||
hasLeading: false,
|
||||
children: children);
|
||||
}
|
||||
}
|
||||
|
||||
486
packages/flutter/lib/src/cupertino/list_section.dart
Normal file
486
packages/flutter/lib/src/cupertino/list_section.dart
Normal file
@ -0,0 +1,486 @@
|
||||
// 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/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
// Margin on top of the list section. This was eyeballed from iOS 14.4 Simulator
|
||||
// and should be always present on top of the edge-to-edge variant.
|
||||
const double _kMarginTop = 22.0;
|
||||
|
||||
// Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 6.0);
|
||||
|
||||
// Header margin for inset grouped variant, determined from iOS 14.4 Simulator.
|
||||
const EdgeInsetsDirectional _kInsetGroupedDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 16.0, 20.0, 6.0);
|
||||
|
||||
// Standard footer margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 0.0);
|
||||
|
||||
// Footer margin for inset grouped variant, determined from iOS 14.4 Simulator.
|
||||
const EdgeInsetsDirectional _kInsetGroupedDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
|
||||
// Margin around children in edge-to-edge variant, determined from iOS 14.4
|
||||
// Simulator.
|
||||
const EdgeInsets _kDefaultRowsMargin = EdgeInsets.only(bottom: 8.0);
|
||||
|
||||
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
|
||||
// iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMargin = EdgeInsetsDirectional.fromSTEB(20.0, 20.0, 20.0, 10.0);
|
||||
|
||||
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
|
||||
// iOS 14.2 SDK.
|
||||
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMarginWithHeader = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
|
||||
|
||||
// Used for iOS "Inset Grouped" border radius, estimated from SwiftUI's Forms in
|
||||
// iOS 14.2 SDK.
|
||||
// TODO(edrisian): This should be a rounded rectangle once that shape is added.
|
||||
const BorderRadius _kDefaultInsetGroupedBorderRadius = BorderRadius.all(Radius.circular(10.0));
|
||||
|
||||
// The margin of divider used in base list section. Estimated from iOS 14.4 SDK
|
||||
// Settings app.
|
||||
const double _kBaseDividerMargin = 20.0;
|
||||
|
||||
// Additional margin of divider used in base list section with list tiles with
|
||||
// leading widgets. Estimated from iOS 14.4 SDK Settings app.
|
||||
const double _kBaseAdditionalDividerMargin = 44.0;
|
||||
|
||||
// The margin of divider used in inset grouped version of list section.
|
||||
// Estimated from iOS 14.4 SDK Reminders app.
|
||||
const double _kInsetDividerMargin = 14.0;
|
||||
|
||||
// Additional margin of divider used in inset grouped version of list section.
|
||||
// Estimated from iOS 14.4 SDK Reminders app.
|
||||
const double _kInsetAdditionalDividerMargin = 42.0;
|
||||
|
||||
// Additional margin of divider used in inset grouped version of list section
|
||||
// when there is no leading widgets. Estimated from iOS 14.4 SDK Notes app.
|
||||
const double _kInsetAdditionalDividerMarginWithoutLeading = 14.0;
|
||||
|
||||
// Color of header and footer text in edge-to-edge variant.
|
||||
const Color _kHeaderFooterColor = CupertinoDynamicColor(
|
||||
color: Color.fromRGBO(108, 108, 108, 1.0),
|
||||
darkColor: Color.fromRGBO(142, 142, 146, 1.0),
|
||||
highContrastColor: Color.fromRGBO(74, 74, 77, 1.0),
|
||||
darkHighContrastColor: Color.fromRGBO(176, 176, 183, 1.0),
|
||||
elevatedColor: Color.fromRGBO(108, 108, 108, 1.0),
|
||||
darkElevatedColor: Color.fromRGBO(142, 142, 146, 1.0),
|
||||
highContrastElevatedColor: Color.fromRGBO(108, 108, 108, 1.0),
|
||||
darkHighContrastElevatedColor: Color.fromRGBO(142, 142, 146, 1.0),
|
||||
);
|
||||
|
||||
/// Denotes what type of the list section a [CupertinoListSection] is.
|
||||
///
|
||||
/// This is for internal use only.
|
||||
enum CupertinoListSectionType {
|
||||
/// A basic form of [CupertinoListSection].
|
||||
base,
|
||||
|
||||
/// An inset-grouped style of [CupertinoListSection].
|
||||
insetGrouped,
|
||||
}
|
||||
|
||||
/// An iOS-style list section.
|
||||
///
|
||||
/// The [CupertinoListSection] is a container for children widgets. These are
|
||||
/// most often [CupertinoListTile]s.
|
||||
///
|
||||
/// The base constructor for [CupertinoListSection] constructs an
|
||||
/// edge-to-edge style section which includes an iOS-style header, the dividers
|
||||
/// between rows, and borders on top and bottom of the rows. An example of such
|
||||
/// list section are sections in iOS Settings app.
|
||||
///
|
||||
/// The [CupertinoListSection.insetGrouped] constructor creates a round-edged
|
||||
/// and padded section that is seen in iOS Notes and Reminders apps. It creates
|
||||
/// an iOS-style header, and the dividers between rows. Does not create borders
|
||||
/// on top and bottom of the rows.
|
||||
///
|
||||
/// The section [header] lies above the [children] rows, with margins and style
|
||||
/// that match the iOS style.
|
||||
///
|
||||
/// The section [footer] lies below the [children] rows and is used to provide
|
||||
/// additional information for current list section.
|
||||
///
|
||||
/// The [children] is the list of widgets to be displayed in this list section.
|
||||
/// Typically, the children are of type [CupertinoListTile], however these is
|
||||
/// not enforced.
|
||||
///
|
||||
/// The [margin] is used to provide spacing around the content area of the
|
||||
/// section encapsulating [children].
|
||||
///
|
||||
/// The [decoration] of [children] specifies how they should be decorated. If it
|
||||
/// is not provided in constructor, the background color of [children] defaults
|
||||
/// to [CupertinoColors.secondarySystemGroupedBackground] and border radius of
|
||||
/// children group defaults to 10.0 circular radius when constructing with
|
||||
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
|
||||
/// standard [CupertinoListSection] constructor.
|
||||
///
|
||||
/// The [dividerMargin] and [additionalDividerMargin] specify the starting
|
||||
/// margin of the divider between list tiles. The [dividerMargin] is always
|
||||
/// present, but [additionalDividerMargin] is only added to the [dividerMargin]
|
||||
/// if `hasLeading` is set to true in the constructor, which is the default
|
||||
/// value.
|
||||
///
|
||||
/// The [backgroundColor] of the section defaults to
|
||||
/// [CupertinoColors.systemGroupedBackground].
|
||||
///
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Creates a base [CupertinoListSection] containing [CupertinoListTile]s with
|
||||
/// `leading`, `title`, `additionalInfo` and `trailing` widgets.
|
||||
///
|
||||
/// ** See code in examples/api/lib/cupertino/list_section/list_section_base.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Creates an "Inset Grouped" [CupertinoListSection] containing
|
||||
/// notched [CupertinoListTile]s with `leading`, `title`, `additionalInfo` and
|
||||
/// `trailing` widgets.
|
||||
///
|
||||
/// ** See code in examples/api/lib/cupertino/list_section/list_section_inset.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoListTile], an iOS-style list tile, a typical child of
|
||||
/// [CupertinoListSection].
|
||||
/// * [CupertinoFormSection], an iOS-style form section.
|
||||
class CupertinoListSection extends StatelessWidget {
|
||||
/// Creates a section that mimics standard iOS forms.
|
||||
///
|
||||
/// The base constructor for [CupertinoListSection] constructs an
|
||||
/// edge-to-edge style section which includes an iOS-style header, the dividers
|
||||
/// between rows, and borders on top and bottom of the rows. An example of such
|
||||
/// list section are sections in iOS Settings app.
|
||||
///
|
||||
/// The [header] parameter sets the form section header. The section header
|
||||
/// lies above the [children] rows, with margins that match the iOS style.
|
||||
///
|
||||
/// The [footer] parameter sets the form section footer. The section footer
|
||||
/// lies below the [children] rows.
|
||||
///
|
||||
/// The [children] parameter is required and sets the list of rows shown in
|
||||
/// the section. The [children] parameter takes a list, as opposed to a more
|
||||
/// efficient builder function that lazy builds, because forms are intended to
|
||||
/// be short in row count. It is recommended that only [CupertinoFormRow] and
|
||||
/// [CupertinoTextFormFieldRow] widgets be included in the [children] list in
|
||||
/// order to retain the iOS look.
|
||||
///
|
||||
/// The [margin] parameter sets the spacing around the content area of the
|
||||
/// section encapsulating [children], and defaults to zero padding.
|
||||
///
|
||||
/// The [decoration] parameter sets the decoration around [children].
|
||||
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
|
||||
/// If null, defaults to 10.0 circular radius when constructing with
|
||||
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
|
||||
/// standard [CupertinoListSection] constructor.
|
||||
///
|
||||
/// The [backgroundColor] parameter sets the background color behind the
|
||||
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
|
||||
///
|
||||
/// The [dividerMargin] parameter sets the starting offset of the divider
|
||||
/// between rows.
|
||||
///
|
||||
/// The [additionalDividerMargin] parameter adds additional margin to existing
|
||||
/// [dividerMargin] when [hasLeading] is set to true. By default, it offsets
|
||||
/// for the width of leading and space between leading and title of
|
||||
/// [CupertinoListTile], but it can be overwritten for custom look.
|
||||
///
|
||||
/// The [hasLeading] parameter specifies whether children [CupertinoListTile]
|
||||
/// widgets contain leading or not. Used for calculating correct starting
|
||||
/// margin for the divider between rows.
|
||||
///
|
||||
/// The [topMargin] is used to specify the margin above the list section. It
|
||||
/// matches the iOS look by default.
|
||||
///
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
const CupertinoListSection({
|
||||
super.key,
|
||||
this.children,
|
||||
this.header,
|
||||
this.footer,
|
||||
this.margin = _kDefaultRowsMargin,
|
||||
this.backgroundColor = CupertinoColors.systemGroupedBackground,
|
||||
this.decoration,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.dividerMargin = _kBaseDividerMargin,
|
||||
double? additionalDividerMargin,
|
||||
this.topMargin = _kMarginTop,
|
||||
bool hasLeading = true,
|
||||
}) : assert((children != null && children.length > 0) || header != null),
|
||||
type = CupertinoListSectionType.base,
|
||||
additionalDividerMargin = additionalDividerMargin ??
|
||||
(hasLeading ? _kBaseAdditionalDividerMargin : 0.0);
|
||||
|
||||
/// Creates a section that mimicks standard "Inset Grouped" iOS list section.
|
||||
///
|
||||
/// The [CupertinoListSection.insetGrouped] constructor creates a round-edged
|
||||
/// and padded section that is seen in iOS Notes and Reminders apps. It creates
|
||||
/// an iOS-style header, and the dividers between rows. Does not create borders
|
||||
/// on top and bottom of the rows.
|
||||
///
|
||||
/// The [header] parameter sets the form section header. The section header
|
||||
/// lies above the [children] rows, with margins that match the iOS style.
|
||||
///
|
||||
/// The [footer] parameter sets the form section footer. The section footer
|
||||
/// lies below the [children] rows.
|
||||
///
|
||||
/// The [children] parameter is required and sets the list of rows shown in
|
||||
/// the section. The [children] parameter takes a list, as opposed to a more
|
||||
/// efficient builder function that lazy builds, because forms are intended to
|
||||
/// be short in row count. It is recommended that only [CupertinoListTile]
|
||||
/// widget be included in the [children] list in order to retain the iOS look.
|
||||
///
|
||||
/// The [margin] parameter sets the spacing around the content area of the
|
||||
/// section encapsulating [children], and defaults to the standard
|
||||
/// notched-style iOS form padding.
|
||||
///
|
||||
/// The [decoration] parameter sets the decoration around [children].
|
||||
/// If null, defaults to [CupertinoColors.secondarySystemGroupedBackground].
|
||||
/// If null, defaults to 10.0 circular radius when constructing with
|
||||
/// [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
|
||||
/// standard [CupertinoListSection] constructor.
|
||||
///
|
||||
/// The [backgroundColor] parameter sets the background color behind the
|
||||
/// section. If null, defaults to [CupertinoColors.systemGroupedBackground].
|
||||
///
|
||||
/// The [dividerMargin] parameter sets the starting offset of the divider
|
||||
/// between rows.
|
||||
///
|
||||
/// The [additionalDividerMargin] parameter adds additional margin to existing
|
||||
/// [dividerMargin] when [hasLeading] is set to true. By default, it offsets
|
||||
/// for the width of leading and space between leading and title of
|
||||
/// [CupertinoListTile], but it can be overwritten for custom look.
|
||||
///
|
||||
/// The [hasLeading] parameter specifies whether children [CupertinoListTile]
|
||||
/// widgets contain leading or not. Used for calculating correct starting
|
||||
/// margin for the divider between rows.
|
||||
///
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
const CupertinoListSection.insetGrouped({
|
||||
super.key,
|
||||
this.children,
|
||||
this.header,
|
||||
this.footer,
|
||||
EdgeInsetsGeometry? margin,
|
||||
this.backgroundColor = CupertinoColors.systemGroupedBackground,
|
||||
this.decoration,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.dividerMargin = _kInsetDividerMargin,
|
||||
double? additionalDividerMargin,
|
||||
this.topMargin,
|
||||
bool hasLeading = true,
|
||||
}) : assert((children != null && children.length > 0) || header != null),
|
||||
type = CupertinoListSectionType.insetGrouped,
|
||||
additionalDividerMargin = additionalDividerMargin ??
|
||||
(hasLeading
|
||||
? _kInsetAdditionalDividerMargin
|
||||
: _kInsetAdditionalDividerMarginWithoutLeading),
|
||||
margin = margin ?? (header == null ? _kDefaultInsetGroupedRowsMargin : _kDefaultInsetGroupedRowsMarginWithHeader);
|
||||
|
||||
/// The type of list section, either base or inset grouped.
|
||||
///
|
||||
/// This member is public for testing purposes only and cannot be set
|
||||
/// manually. Instead, use a corresponding constructors.
|
||||
@visibleForTesting
|
||||
final CupertinoListSectionType type;
|
||||
|
||||
/// Sets the form section header. The section header lies above the [children]
|
||||
/// rows. Usually a [Text] widget.
|
||||
final Widget? header;
|
||||
|
||||
/// Sets the form section footer. The section footer lies below the [children]
|
||||
/// rows. Usually a [Text] widget.
|
||||
final Widget? footer;
|
||||
|
||||
/// Margin around the content area of the section encapsulating [children].
|
||||
///
|
||||
/// Defaults to zero padding if constructed with standard
|
||||
/// [CupertinoListSection] constructor. Defaults to the standard notched-style
|
||||
/// iOS margin when constructing with [CupertinoListSection.insetGrouped].
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
/// The list of rows in the section. Usually a list of [CupertinoListTile]s.
|
||||
///
|
||||
/// This takes a list, as opposed to a more efficient builder function that
|
||||
/// lazy builds, because such lists are intended to be short in row count.
|
||||
/// It is recommended that only [CupertinoListTile] widget be included in the
|
||||
/// [children] list in order to retain the iOS look.
|
||||
final List<Widget>? children;
|
||||
|
||||
/// Sets the decoration around [children].
|
||||
///
|
||||
/// If null, background color defaults to
|
||||
/// [CupertinoColors.secondarySystemGroupedBackground].
|
||||
///
|
||||
/// If null, border radius defaults to 10.0 circular radius when constructing
|
||||
/// with [CupertinoListSection.insetGrouped]. Defaults to zero radius for the
|
||||
/// standard [CupertinoListSection] constructor.
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
/// Sets the background color behind the section.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.systemGroupedBackground].
|
||||
final Color backgroundColor;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The starting offset of a margin between two list tiles.
|
||||
final double dividerMargin;
|
||||
|
||||
/// Additional starting inset of the divider used between rows. This is used
|
||||
/// when adding a leading icon to children and a divider should start at the
|
||||
/// text inset instead of the icon.
|
||||
final double additionalDividerMargin;
|
||||
|
||||
/// Margin above the list section. Only used in edge-to-edge variant and it
|
||||
/// matches iOS style by default.
|
||||
final double? topMargin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color dividerColor = CupertinoColors.separator.resolveFrom(context);
|
||||
final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// Long divider is used for wrapping the top and bottom of rows.
|
||||
// Only used in CupertinoListSectionType.base mode.
|
||||
final Widget longDivider = Container(
|
||||
color: dividerColor,
|
||||
height: dividerHeight,
|
||||
);
|
||||
|
||||
// Short divider is used between rows.
|
||||
final Widget shortDivider = Container(
|
||||
margin: EdgeInsetsDirectional.only(
|
||||
start: dividerMargin + additionalDividerMargin),
|
||||
color: dividerColor,
|
||||
height: dividerHeight,
|
||||
);
|
||||
|
||||
Widget? headerWidget;
|
||||
if (header != null) {
|
||||
headerWidget = DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle.merge(
|
||||
type == CupertinoListSectionType.base
|
||||
? TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
_kHeaderFooterColor, context))
|
||||
: const TextStyle(
|
||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: header!,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? footerWidget;
|
||||
if (footer != null) {
|
||||
footerWidget = DefaultTextStyle(
|
||||
style: type == CupertinoListSectionType.base
|
||||
? CupertinoTheme.of(context).textTheme.textStyle.merge(TextStyle(
|
||||
fontSize: 13.0,
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
_kHeaderFooterColor, context),
|
||||
))
|
||||
: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: footer!,
|
||||
);
|
||||
}
|
||||
|
||||
BorderRadius? childrenGroupBorderRadius;
|
||||
DecoratedBox? decoratedChildrenGroup;
|
||||
if (children != null && children!.isNotEmpty) {
|
||||
// We construct childrenWithDividers as follows:
|
||||
// Insert a short divider between all rows.
|
||||
// If it is a `CupertinoListSectionType.base` type, add a long divider
|
||||
// to the top and bottom of the rows.
|
||||
final List<Widget> childrenWithDividers = <Widget>[];
|
||||
|
||||
if (type == CupertinoListSectionType.base) {
|
||||
childrenWithDividers.add(longDivider);
|
||||
}
|
||||
|
||||
children!.sublist(0, children!.length - 1).forEach((Widget widget) {
|
||||
childrenWithDividers.add(widget);
|
||||
childrenWithDividers.add(shortDivider);
|
||||
});
|
||||
|
||||
childrenWithDividers.add(children!.last);
|
||||
if (type == CupertinoListSectionType.base) {
|
||||
childrenWithDividers.add(longDivider);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CupertinoListSectionType.insetGrouped:
|
||||
childrenGroupBorderRadius = _kDefaultInsetGroupedBorderRadius;
|
||||
break;
|
||||
case CupertinoListSectionType.base:
|
||||
childrenGroupBorderRadius = BorderRadius.zero;
|
||||
break;
|
||||
}
|
||||
|
||||
// Refactored the decorate children group in one place to avoid repeating it
|
||||
// twice down bellow in the returned widget.
|
||||
decoratedChildrenGroup = DecoratedBox(
|
||||
decoration: decoration ??
|
||||
BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
decoration?.color ??
|
||||
CupertinoColors.secondarySystemGroupedBackground,
|
||||
context),
|
||||
borderRadius: childrenGroupBorderRadius,
|
||||
),
|
||||
child: Column(children: childrenWithDividers),
|
||||
);
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(backgroundColor, context)),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (type == CupertinoListSectionType.base)
|
||||
SizedBox(height: topMargin),
|
||||
if (headerWidget != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: type == CupertinoListSectionType.base
|
||||
? _kDefaultHeaderMargin
|
||||
: _kInsetGroupedDefaultHeaderMargin,
|
||||
child: headerWidget,
|
||||
),
|
||||
),
|
||||
if (children != null && children!.isNotEmpty)
|
||||
Padding(
|
||||
padding: margin,
|
||||
child: clipBehavior == Clip.none
|
||||
? decoratedChildrenGroup
|
||||
: ClipRRect(
|
||||
borderRadius: childrenGroupBorderRadius,
|
||||
clipBehavior: clipBehavior,
|
||||
child: decoratedChildrenGroup,
|
||||
),
|
||||
),
|
||||
if (footerWidget != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: type == CupertinoListSectionType.base
|
||||
? _kDefaultFooterMargin
|
||||
: _kInsetGroupedDefaultFooterMargin,
|
||||
child: footerWidget,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
414
packages/flutter/lib/src/cupertino/list_tile.dart
Normal file
414
packages/flutter/lib/src/cupertino/list_tile.dart
Normal file
@ -0,0 +1,414 @@
|
||||
// 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:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'icons.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
// These constants were eyeballed from iOS 14.4 Settings app for base, Notes for
|
||||
// notched without leading, and Reminders app for notched with leading.
|
||||
const double _kLeadingSize = 28.0;
|
||||
const double _kNotchedLeadingSize = 30.0;
|
||||
const double _kMinHeight = _kLeadingSize + 2 * 8.0;
|
||||
const double _kMinHeightWithSubtitle = _kLeadingSize + 2 * 10.0;
|
||||
const double _kNotchedMinHeight = _kNotchedLeadingSize + 2 * 12.0;
|
||||
const double _kNotchedMinHeightWithoutLeading = _kNotchedLeadingSize + 2 * 10.0;
|
||||
const EdgeInsetsDirectional _kPadding = EdgeInsetsDirectional.only(start: 20.0, end: 14.0);
|
||||
const EdgeInsetsDirectional _kPaddingWithSubtitle = EdgeInsetsDirectional.only(start: 20.0, end: 14.0);
|
||||
const EdgeInsets _kNotchedPadding = EdgeInsets.symmetric(horizontal: 14.0);
|
||||
const EdgeInsetsDirectional _kNotchedPaddingWithoutLeading = EdgeInsetsDirectional.fromSTEB(28.0, 10.0, 14.0, 10.0);
|
||||
const double _kLeadingToTitle = 16.0;
|
||||
const double _kNotchedLeadingToTitle = 12.0;
|
||||
const double _kNotchedTitleToSubtitle = 3.0;
|
||||
const double _kAdditionalInfoToTrailing = 6.0;
|
||||
const double _kNotchedTitleWithSubtitleFontSize = 16.0;
|
||||
const double _kSubtitleFontSize = 12.0;
|
||||
const double _kNotchedSubtitleFontSize = 14.0;
|
||||
|
||||
enum _CupertinoListTileType { base, notched }
|
||||
|
||||
/// An iOS-style list tile.
|
||||
///
|
||||
/// The [CupertinoListTile] is a Cupertino equivalent of Material [ListTile].
|
||||
/// It comes in two forms, an old-fashioned edge-to-edge variant known from iOS
|
||||
/// Settings app and in a new, "Inset Grouped" form, known from either iOS Notes
|
||||
/// or Reminders app. The first is constructed using default constructor, and
|
||||
/// the latter using named constructor [CupertinoListTile.notched].
|
||||
///
|
||||
/// The [title], [subtitle], and [additionalInfo] are usually [Text] widgets.
|
||||
/// They are all limited to one line so it is a responsibility of the caller to
|
||||
/// take care of text wrapping.
|
||||
///
|
||||
/// The size of [leading] is by default constrained to match the iOS size,
|
||||
/// depending of the type of list tile. This can however be overriden by
|
||||
/// providing [leadingSize]. The [trailing] widget is not constrained and is
|
||||
/// therefore a responsibility of the caller to ensure reasonable size of the
|
||||
/// [trailing] widget.
|
||||
///
|
||||
/// The background color of the tile can be set with [backgroundColor] for the
|
||||
/// state before tile was tapped and with [backgroundColorActivated] for the
|
||||
/// state after the tile was tapped. By default, both values are set to match
|
||||
/// the default iOS appearance.
|
||||
///
|
||||
/// The [padding] and [leadingToTitle] are by default set to match iOS but can
|
||||
/// be overwritten if necessary.
|
||||
///
|
||||
/// The [onTap] callback provides an option to react to taps anywhere inside the
|
||||
/// list tile. This can be used to navigate routes and according to iOS
|
||||
/// behaviour it should not be used for example to toggle the [CupertinoSwitch]
|
||||
/// in the trailing widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoListSection], an iOS-style list that is a typical container for
|
||||
/// [CupertinoListTile].
|
||||
/// * [ListTile], a Material Design list tile.
|
||||
class CupertinoListTile extends StatefulWidget {
|
||||
/// Creates an edge-to-edge iOS-style list tile like the tiles in iOS Settings
|
||||
/// app.
|
||||
///
|
||||
/// The [title] parameter is required. It is used to convey the most important
|
||||
/// information of list tile. It is typically a [Text].
|
||||
///
|
||||
/// The [subtitle] parameter is used to display additional information. It is
|
||||
/// placed below the [title].
|
||||
///
|
||||
/// The [additionalInfo] parameter is used to display additional information.
|
||||
/// It is placed at the end of the tile, before the [trailing] if supplied.
|
||||
///
|
||||
/// The [leading] parameter is typically an [Icon] or an [Image] and it comes
|
||||
/// at the start of the tile. If omitted in all list tiles, a `hasLeading` of
|
||||
/// enclosing [CupertinoListSection] should be set to `false` to ensure
|
||||
/// correct margin of divider between tiles.
|
||||
///
|
||||
/// The [trailing] parameter is typically a [CupertinoListTileChevron], an
|
||||
/// [Icon], or a [CupertinoButton]. It is placed at the very end of the tile.
|
||||
///
|
||||
/// The [onTap] parameter is used to provide an action that is called when the
|
||||
/// tile is tapped. It is mainly used for navigating to a new route. It should
|
||||
/// not be used to toggle a trailing [CupertinoSwitch] and similar usecases
|
||||
/// because when tile is tapped, it switches the background color and remains
|
||||
/// changed. This is according to iOS behaviour.
|
||||
///
|
||||
/// The [backgroundColor] provides a custom background color for the tile in
|
||||
/// a state before tapped. By default, it matches the theme's background color
|
||||
/// which is by default a [CupertinoColors.systemBackground].
|
||||
///
|
||||
/// The [backgroundColorActivated] provides a custom background color for the
|
||||
/// tile after it was tapped. By default, it matches the theme's background
|
||||
/// color which is by default a [CupertinoColors.systemGrey4].
|
||||
///
|
||||
/// The [padding] parameter sets the padding of the content inside the tile.
|
||||
/// It defaults to a value that matches the iOS look, depending on a type of
|
||||
/// [CupertinoListTile]. For native look, it should not be provided.
|
||||
///
|
||||
/// The [leadingSize] constrains the width and height of the leading widget.
|
||||
/// By default, it is set to a value that matches the iOS look, depending on a
|
||||
/// type of [CupertinoListTile]. For native look, it should not be provided.
|
||||
///
|
||||
/// The [leadingToTitle] specifies the horizontal space between [leading] and
|
||||
/// [title] widgets. By default, it is set to a value that matched the iOS
|
||||
/// look, depending on a type of [CupertinoListTile]. For native look, it
|
||||
/// should not be provided.
|
||||
const CupertinoListTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.additionalInfo,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.backgroundColor,
|
||||
this.backgroundColorActivated,
|
||||
this.padding,
|
||||
this.leadingSize = _kLeadingSize,
|
||||
this.leadingToTitle = _kLeadingToTitle,
|
||||
}) : _type = _CupertinoListTileType.base;
|
||||
|
||||
/// Creates a notched iOS-style list tile like the tiles in iOS Notes app or
|
||||
/// Reminders app.
|
||||
///
|
||||
/// The [title] parameter is required. It is used to convey the most important
|
||||
/// information of list tile. It is typically a [Text].
|
||||
///
|
||||
/// The [subtitle] parameter is used to display additional information. It is
|
||||
/// placed below the [title].
|
||||
///
|
||||
/// The [additionalInfo] parameter is used to display additional information.
|
||||
/// It is placed at the end of the tile, before the [trailing] if supplied.
|
||||
///
|
||||
/// The [leading] parameter is typically an [Icon] or an [Image] and it comes
|
||||
/// at the start of the tile. If omitted in all list tiles, a `hasLeading` of
|
||||
/// enclosing [CupertinoListSection] should be set to `false` to ensure
|
||||
/// correct margin of divider between tiles. For Notes-like tile appearance,
|
||||
/// the [leading] can be left `null`.
|
||||
///
|
||||
/// The [trailing] parameter is typically a [CupertinoListTileChevron], an
|
||||
/// [Icon], or a [CupertinoButton]. It is placed at the very end of the tile.
|
||||
/// For Notes-like tile appearance, the [trailing] can be left `null`.
|
||||
///
|
||||
/// The [onTap] parameter is used to provide an action that is called when the
|
||||
/// tile is tapped. It is mainly used for navigating to a new route. It should
|
||||
/// not be used to toggle a trailing [CupertinoSwitch] and similar usecases
|
||||
/// because when tile is tapped, it switches the background color and remains
|
||||
/// changed. This is according to iOS behaviour.
|
||||
///
|
||||
/// The [backgroundColor] provides a custom background color for the tile in
|
||||
/// a state before tapped. By default, it matches the theme's background color
|
||||
/// which is by default a [CupertinoColors.systemBackground].
|
||||
///
|
||||
/// The [backgroundColorActivated] provides a custom background color for the
|
||||
/// tile after it was tapped. By default, it matches the theme's background
|
||||
/// color which is by default a [CupertinoColors.systemGrey4].
|
||||
///
|
||||
/// The [padding] parameter sets the padding of the content inside the tile.
|
||||
/// It defaults to a value that matches the iOS look, depending on a type of
|
||||
/// [CupertinoListTile]. For native look, it should not be provided.
|
||||
///
|
||||
/// The [leadingSize] constrains the width and height of the leading widget.
|
||||
/// By default, it is set to a value that matches the iOS look, depending on a
|
||||
/// type of [CupertinoListTile]. For native look, it should not be provided.
|
||||
///
|
||||
/// The [leadingToTitle] specifies the horizontal space between [leading] and
|
||||
/// [title] widgets. By default, it is set to a value that matched the iOS
|
||||
/// look, depending on a type of [CupertinoListTile]. For native look, it
|
||||
/// should not be provided.
|
||||
const CupertinoListTile.notched({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.additionalInfo,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.backgroundColor,
|
||||
this.backgroundColorActivated,
|
||||
this.padding,
|
||||
this.leadingSize = _kNotchedLeadingSize,
|
||||
this.leadingToTitle = _kNotchedLeadingToTitle,
|
||||
}) : _type = _CupertinoListTileType.notched;
|
||||
|
||||
final _CupertinoListTileType _type;
|
||||
|
||||
/// A [title] is used to convey the central information. Usually a [Text].
|
||||
final Widget title;
|
||||
|
||||
/// A [subtitle] is used to display additional information. It is located
|
||||
/// below [title]. Usually a [Text] widget.
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Similar to [subtitle], an [additionalInfo] is used to display additional
|
||||
/// information. However, instead of being displayed below [title], it is
|
||||
/// displayed on the right, before [trailing]. Usually a [Text] widget.
|
||||
final Widget? additionalInfo;
|
||||
|
||||
/// A widget displayed at the start of the [CupertinoListTile]. This is
|
||||
/// typically an `Icon` or an `Image`.
|
||||
final Widget? leading;
|
||||
|
||||
/// A widget displayed at the end of the [CupertinoListTile]. This is usually
|
||||
/// a right chevron icon (e.g. `CupertinoListTileChevron`), or an `Icon`.
|
||||
final Widget? trailing;
|
||||
|
||||
/// The [onTap] function is called when a user taps on [CupertinoListTile]. If
|
||||
/// left `null`, the [CupertinoListTile] will not react on taps. If this is a
|
||||
/// `Future<void> Function()`, then the [CupertinoListTile] remains activated
|
||||
/// until the returned future is awaited. This is according to iOS behaviour.
|
||||
/// However, if this function is a `void Function()`, then the tile is active
|
||||
/// only for the duration of invocation.
|
||||
final FutureOr<void> Function()? onTap;
|
||||
|
||||
/// The [backgroundColor] of the tile in normal state. Once the tile is
|
||||
/// tapped, the background color switches to [backgroundColorActivated]. It is
|
||||
/// set to match the iOS look by default.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The [backgroundColorActivated] is the background color of the tile after
|
||||
/// the tile was tapped. It is set to match the iOS look by default.
|
||||
final Color? backgroundColorActivated;
|
||||
|
||||
/// Padding of the content inside [CupertinoListTile].
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// The [leadingSize] is used to constrain the width and height of [leading]
|
||||
/// widget.
|
||||
final double leadingSize;
|
||||
|
||||
/// The horizontal space between [leading] widget and [title].
|
||||
final double leadingToTitle;
|
||||
|
||||
@override
|
||||
State<CupertinoListTile> createState() => _CupertinoListTileState();
|
||||
}
|
||||
|
||||
class _CupertinoListTileState extends State<CupertinoListTile> {
|
||||
bool _tapped = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TextStyle titleTextStyle =
|
||||
widget._type == _CupertinoListTileType.base || widget.subtitle == null
|
||||
? CupertinoTheme.of(context).textTheme.textStyle
|
||||
: CupertinoTheme.of(context).textTheme.textStyle.merge(
|
||||
TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: widget.leading == null ? _kNotchedTitleWithSubtitleFontSize : null,
|
||||
),
|
||||
);
|
||||
|
||||
final TextStyle subtitleTextStyle = widget._type == _CupertinoListTileType.base
|
||||
? CupertinoTheme.of(context).textTheme.textStyle.merge(
|
||||
TextStyle(
|
||||
fontSize: _kSubtitleFontSize,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
)
|
||||
: CupertinoTheme.of(context).textTheme.textStyle.merge(
|
||||
TextStyle(
|
||||
fontSize: _kNotchedSubtitleFontSize,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
),
|
||||
);
|
||||
|
||||
final TextStyle? additionalInfoTextStyle = widget.additionalInfo != null
|
||||
? CupertinoTheme.of(context).textTheme.textStyle.merge(
|
||||
TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)))
|
||||
: null;
|
||||
|
||||
final Widget title = DefaultTextStyle(
|
||||
style: titleTextStyle,
|
||||
maxLines: 1,
|
||||
child: widget.title,
|
||||
);
|
||||
|
||||
EdgeInsetsGeometry? padding = widget.padding;
|
||||
if (padding == null) {
|
||||
switch (widget._type) {
|
||||
case _CupertinoListTileType.base:
|
||||
padding = widget.subtitle == null ? _kPadding : _kPaddingWithSubtitle;
|
||||
break;
|
||||
case _CupertinoListTileType.notched:
|
||||
padding = widget.leading == null ? _kNotchedPaddingWithoutLeading : _kNotchedPadding;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget? subtitle;
|
||||
if (widget.subtitle != null) {
|
||||
subtitle = DefaultTextStyle(
|
||||
style: subtitleTextStyle,
|
||||
maxLines: 1,
|
||||
child: widget.subtitle!,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? additionalInfo;
|
||||
if (widget.additionalInfo != null) {
|
||||
additionalInfo = DefaultTextStyle(
|
||||
style: additionalInfoTextStyle!,
|
||||
maxLines: 1,
|
||||
child: widget.additionalInfo!,
|
||||
);
|
||||
}
|
||||
|
||||
// The color for default state tile is set to either what user provided or
|
||||
// null and it will resolve to the correct color provided by context. But if
|
||||
// the tile was tapped, it is set to what user provided or if null to the
|
||||
// default color that matched the iOS-style.
|
||||
Color? backgroundColor = widget.backgroundColor;
|
||||
if (_tapped) {
|
||||
backgroundColor = widget.backgroundColorActivated ?? CupertinoColors.systemGrey4.resolveFrom(context);
|
||||
}
|
||||
|
||||
double minHeight;
|
||||
switch (widget._type) {
|
||||
case _CupertinoListTileType.base:
|
||||
minHeight = subtitle == null ? _kMinHeight : _kMinHeightWithSubtitle;
|
||||
break;
|
||||
case _CupertinoListTileType.notched:
|
||||
minHeight = widget.leading == null ? _kNotchedMinHeightWithoutLeading : _kNotchedMinHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
final Widget child = Container(
|
||||
constraints: BoxConstraints(minWidth: double.infinity, minHeight: minHeight),
|
||||
color: backgroundColor,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (widget.leading != null) ...<Widget>[
|
||||
SizedBox(
|
||||
width: widget.leadingSize,
|
||||
height: widget.leadingSize,
|
||||
child: Center(
|
||||
child: widget.leading,
|
||||
),
|
||||
),
|
||||
SizedBox(width: widget.leadingToTitle),
|
||||
] else
|
||||
SizedBox(height: widget.leadingSize),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
title,
|
||||
if (subtitle != null) ...<Widget>[
|
||||
const SizedBox(height: _kNotchedTitleToSubtitle),
|
||||
subtitle,
|
||||
],
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
if (additionalInfo != null) ...<Widget>[
|
||||
additionalInfo,
|
||||
if (widget.trailing != null)
|
||||
const SizedBox(width: _kAdditionalInfoToTrailing),
|
||||
],
|
||||
if (widget.trailing != null) widget.trailing!
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.onTap == null) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => setState(() { _tapped = true; }),
|
||||
onTapCancel: () => setState(() { _tapped = false; }),
|
||||
onTap: () async {
|
||||
await widget.onTap!();
|
||||
setState(() { _tapped = false; });
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A typical iOS trailing widget used to denote that a `CupertinoListTile` is a
|
||||
/// button with an action.
|
||||
///
|
||||
/// The [CupertinoListTileChevron] is meant as a convenience implementation of
|
||||
/// trailing right chevron.
|
||||
class CupertinoListTileChevron extends StatelessWidget {
|
||||
/// Creates a typical widget used to denote that a `CupertinoListTile` is a
|
||||
/// button with action.
|
||||
const CupertinoListTileChevron({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Icon(
|
||||
CupertinoIcons.right_chevron,
|
||||
size: CupertinoTheme.of(context).textTheme.textStyle.fontSize,
|
||||
color: CupertinoColors.systemGrey2.resolveFrom(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -153,7 +153,7 @@ void main() {
|
||||
expect(find.byType(ClipRRect), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Not setting clipBehavior does not clip children section', (WidgetTester tester) async {
|
||||
testWidgets('Not setting clipBehavior does not produce a RenderClipRRect object', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
@ -164,7 +164,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final RenderClipRRect renderClip = tester.allRenderObjects.whereType<RenderClipRRect>().first;
|
||||
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||
final Iterable<RenderClipRRect> renderClips = tester.allRenderObjects.whereType<RenderClipRRect>();
|
||||
expect(renderClips, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
179
packages/flutter/test/cupertino/list_section_test.dart
Normal file
179
packages/flutter/test/cupertino/list_section_test.dart
Normal file
@ -0,0 +1,179 @@
|
||||
// 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/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('shows header', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
header: const Text('Header'),
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Header'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows footer', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
footer: const Text('Footer'),
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Footer'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows long dividers in edge-to-edge section part 1', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Since the children list is reconstructed with dividers in it, the column
|
||||
// retrieved should have 3 items for an input [children] param with 1 child.
|
||||
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
|
||||
expect(childrenColumn.children.length, 3);
|
||||
});
|
||||
|
||||
testWidgets('shows long dividers in edge-to-edge section part 2', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Since the children list is reconstructed with dividers in it, the column
|
||||
// retrieved should have 5 items for an input [children] param with 2
|
||||
// children. Two long dividers, two rows, and one short divider.
|
||||
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
|
||||
expect(childrenColumn.children.length, 5);
|
||||
});
|
||||
|
||||
testWidgets('does not show long dividers in insetGrouped section part 1', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection.insetGrouped(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Since the children list is reconstructed without long dividers in it, the
|
||||
// column retrieved should have 1 item for an input [children] param with 1
|
||||
// child.
|
||||
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
|
||||
expect(childrenColumn.children.length, 1);
|
||||
});
|
||||
|
||||
testWidgets('does not show long dividers in insetGrouped section part 2', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection.insetGrouped(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Since the children list is reconstructed with short dividers in it, the
|
||||
// column retrieved should have 3 items for an input [children] param with 2
|
||||
// children. Two long dividers, two rows, and one short divider.
|
||||
final Column childrenColumn = tester.widget(find.byType(Column).at(1));
|
||||
expect(childrenColumn.children.length, 3);
|
||||
});
|
||||
|
||||
testWidgets('sets background color for section', (WidgetTester tester) async {
|
||||
const Color backgroundColor = CupertinoColors.systemBlue;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
backgroundColor: backgroundColor,
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox).first);
|
||||
final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
|
||||
expect(boxDecoration.color, backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets('setting clipBehavior clips children section', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ClipRRect), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('not setting clipBehavior does not clip children section', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(title: Text('CupertinoListTile')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(ClipRRect), findsNothing);
|
||||
});
|
||||
}
|
||||
482
packages/flutter/test/cupertino/list_tile_test.dart
Normal file
482
packages/flutter/test/cupertino/list_tile_test.dart
Normal file
@ -0,0 +1,482 @@
|
||||
// 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/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('shows title', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<Text>(find.byType(Text)), title);
|
||||
expect(find.text('CupertinoListTile'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows subtitle', (WidgetTester tester) async {
|
||||
const Widget subtitle = Text('CupertinoListTile subtitle');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListTile(
|
||||
title: Icon(CupertinoIcons.add),
|
||||
subtitle: subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<Text>(find.byType(Text)), subtitle);
|
||||
expect(find.text('CupertinoListTile subtitle'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows additionalInfo', (WidgetTester tester) async {
|
||||
const Widget additionalInfo = Text('Not Connected');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListTile(
|
||||
title: Icon(CupertinoIcons.add),
|
||||
additionalInfo: additionalInfo,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<Text>(find.byType(Text)), additionalInfo);
|
||||
expect(find.text('Not Connected'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows trailing', (WidgetTester tester) async {
|
||||
const Widget trailing = CupertinoListTileChevron();
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListTile(
|
||||
title: Icon(CupertinoIcons.add),
|
||||
trailing: trailing,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<CupertinoListTileChevron>(find.byType(CupertinoListTileChevron)), trailing);
|
||||
});
|
||||
|
||||
testWidgets('shows leading', (WidgetTester tester) async {
|
||||
const Widget leading = Icon(CupertinoIcons.add);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoListTile(
|
||||
leading: leading,
|
||||
title: Text('CupertinoListTile'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<Icon>(find.byType(Icon)), leading);
|
||||
});
|
||||
|
||||
testWidgets('sets backgroundColor', (WidgetTester tester) async {
|
||||
const Color backgroundColor = CupertinoColors.systemRed;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(
|
||||
title: Text('CupertinoListTile'),
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets('does not change backgroundColor when tapped if onTap is not provided', (WidgetTester tester) async {
|
||||
const Color backgroundColor = CupertinoColors.systemBlue;
|
||||
const Color backgroundColorActivated = CupertinoColors.systemRed;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(
|
||||
title: Text('CupertinoListTile'),
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundColorActivated: backgroundColorActivated,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(CupertinoListTile));
|
||||
await tester.pump();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
});
|
||||
|
||||
testWidgets('changes backgroundColor when tapped if onTap is provided', (WidgetTester tester) async {
|
||||
const Color backgroundColor = CupertinoColors.systemBlue;
|
||||
const Color backgroundColorActivated = CupertinoColors.systemRed;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('CupertinoListTile'),
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundColorActivated: backgroundColorActivated,
|
||||
onTap: () async { await Future<void>.delayed(const Duration(milliseconds: 1), () {}); },
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
Container container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColor);
|
||||
|
||||
// Pump only one frame so the color change persists.
|
||||
await tester.tap(find.byType(CupertinoListTile));
|
||||
await tester.pump();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
container = tester.widgetList<Container>(find.byType(Container)).elementAt(1);
|
||||
expect(container.color, backgroundColorActivated);
|
||||
|
||||
// Pump the rest of the frames to complete the test.
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('does not contain GestureDetector if onTap is not provided', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
children: const <Widget>[
|
||||
CupertinoListTile(
|
||||
title: Text('CupertinoListTile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
expect(find.byType(GestureDetector), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('contains GestureDetector if onTap is provided', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('CupertinoListTile'),
|
||||
onTap: () async {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
expect(find.byType(GestureDetector), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('resets the background color when navigated back', (WidgetTester tester) async {
|
||||
const Color backgroundColor = CupertinoColors.systemBlue;
|
||||
const Color backgroundColorActivated = CupertinoColors.systemRed;
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final Widget secondPage = Center(
|
||||
child: CupertinoButton(
|
||||
child: const Text('Go back'),
|
||||
onPressed: () => Navigator.of(context).pop<void>(),
|
||||
),
|
||||
);
|
||||
return Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child:CupertinoListTile(
|
||||
title: const Text('CupertinoListTile'),
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundColorActivated: backgroundColorActivated,
|
||||
onTap: () => Navigator.of(context).push(CupertinoPageRoute<Widget>(
|
||||
builder: (BuildContext context) => secondPage,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate to second page.
|
||||
await tester.tap(find.byType(CupertinoListTile));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Go back to first page.
|
||||
await tester.tap(find.byType(CupertinoButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Container inside CupertinoListTile is the second one in row.
|
||||
final Container container = tester.widget<Container>(find.byType(Container));
|
||||
expect(container.color, backgroundColor);
|
||||
});
|
||||
|
||||
group('alignment of widgets for left-to-right', () {
|
||||
testWidgets('leading is on the left of title', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget leading = Icon(CupertinoIcons.add);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
leading: leading,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getTopLeft(find.byType(Text));
|
||||
final Offset foundLeading = tester.getTopRight(find.byType(Icon));
|
||||
|
||||
expect(foundTitle.dx > foundLeading.dx, true);
|
||||
});
|
||||
|
||||
testWidgets('subtitle is placed below title and aligned on left', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile title');
|
||||
const Widget subtitle = Text('CupertinoListTile subtitle');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getBottomLeft(find.text('CupertinoListTile title'));
|
||||
final Offset foundSubtitle = tester.getTopLeft(find.text('CupertinoListTile subtitle'));
|
||||
|
||||
expect(foundTitle.dx, equals(foundSubtitle.dx));
|
||||
expect(foundTitle.dy < foundSubtitle.dy, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('additionalInfo is on the right of title', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget additionalInfo = Text('Not Connected');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
additionalInfo: additionalInfo,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getTopRight(find.text('CupertinoListTile'));
|
||||
final Offset foundInfo = tester.getTopLeft(find.text('Not Connected'));
|
||||
|
||||
expect(foundTitle.dx < foundInfo.dx, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('trailing is on the right of additionalInfo', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget additionalInfo = Text('Not Connected');
|
||||
const Widget trailing = CupertinoListTileChevron();
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
additionalInfo: additionalInfo,
|
||||
trailing: trailing,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundInfo = tester.getTopRight(find.text('Not Connected'));
|
||||
final Offset foundTrailing = tester.getTopLeft(find.byType(CupertinoListTileChevron));
|
||||
|
||||
expect(foundInfo.dx < foundTrailing.dx, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('alignment of widgets for right-to-left', () {
|
||||
testWidgets('leading is on the right of title', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget leading = Icon(CupertinoIcons.add);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
leading: leading,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getTopRight(find.byType(Text));
|
||||
final Offset foundLeading = tester.getTopLeft(find.byType(Icon));
|
||||
|
||||
expect(foundTitle.dx < foundLeading.dx, true);
|
||||
});
|
||||
|
||||
testWidgets('subtitle is placed below title and aligned on right', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile title');
|
||||
const Widget subtitle = Text('CupertinoListTile subtitle');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getBottomRight(find.text('CupertinoListTile title'));
|
||||
final Offset foundSubtitle = tester.getTopRight(find.text('CupertinoListTile subtitle'));
|
||||
|
||||
expect(foundTitle.dx, equals(foundSubtitle.dx));
|
||||
expect(foundTitle.dy < foundSubtitle.dy, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('additionalInfo is on the left of title', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget additionalInfo = Text('Not Connected');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
additionalInfo: additionalInfo,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundTitle = tester.getTopLeft(find.text('CupertinoListTile'));
|
||||
final Offset foundInfo = tester.getTopRight(find.text('Not Connected'));
|
||||
|
||||
expect(foundTitle.dx > foundInfo.dx, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('trailing is on the left of additionalInfo', (WidgetTester tester) async {
|
||||
const Widget title = Text('CupertinoListTile');
|
||||
const Widget additionalInfo = Text('Not Connected');
|
||||
const Widget trailing = CupertinoListTileChevron();
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp(
|
||||
home: Center(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: CupertinoListTile(
|
||||
title: title,
|
||||
additionalInfo: additionalInfo,
|
||||
trailing: trailing,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset foundInfo = tester.getTopLeft(find.text('Not Connected'));
|
||||
final Offset foundTrailing = tester.getTopRight(find.byType(CupertinoListTileChevron));
|
||||
|
||||
expect(foundInfo.dx > foundTrailing.dx, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user