Unverified Commit 0c40945a authored by Marcel Čampa's avatar Marcel Čampa Committed by GitHub

Implement `CupertinoListSection` and `CupertinoListTile` (#78732)

parent 9d2f5754
// 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),
),
);
}
}
// 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),
),
);
}
}
// 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));
});
}
// 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'; ...@@ -38,6 +38,8 @@ export 'src/cupertino/form_section.dart';
export 'src/cupertino/icon_theme_data.dart'; export 'src/cupertino/icon_theme_data.dart';
export 'src/cupertino/icons.dart'; export 'src/cupertino/icons.dart';
export 'src/cupertino/interface_level.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/localizations.dart';
export 'src/cupertino/nav_bar.dart'; export 'src/cupertino/nav_bar.dart';
export 'src/cupertino/page_scaffold.dart'; export 'src/cupertino/page_scaffold.dart';
......
...@@ -5,28 +5,17 @@ ...@@ -5,28 +5,17 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'list_section.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);
// Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in // Used for iOS "Inset Grouped" margin, determined from SwiftUI's Forms in
// iOS 14.2 SDK. // iOS 14.2 SDK.
const EdgeInsetsDirectional _kDefaultInsetGroupedRowsMargin = const EdgeInsetsDirectional _kFormDefaultInsetGroupedRowsMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
// Used for iOS "Inset Grouped" border radius, estimated from SwiftUI's Forms in // Standard header margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
// iOS 14.2 SDK. const EdgeInsetsDirectional _kFormDefaultHeaderMargin = EdgeInsetsDirectional.fromSTEB(20.0, 16.0, 20.0, 10.0);
// TODO(edrisian): This should be a rounded rectangle once that shape is added.
const BorderRadius _kDefaultInsetGroupedBorderRadius =
BorderRadius.all(Radius.circular(10.0));
// Used to differentiate the edge-to-edge section with the centered section. // Standard footer margin, determined from SwiftUI's Forms in iOS 14.2 SDK.
enum _CupertinoFormSectionType { base, insetGrouped } const EdgeInsetsDirectional _kFormDefaultFooterMargin = EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 10.0);
/// An iOS-style form section. /// An iOS-style form section.
/// ///
...@@ -65,6 +54,12 @@ enum _CupertinoFormSectionType { base, insetGrouped } ...@@ -65,6 +54,12 @@ enum _CupertinoFormSectionType { base, insetGrouped }
/// If null, defaults to [CupertinoColors.systemGroupedBackground]. /// If null, defaults to [CupertinoColors.systemGroupedBackground].
/// ///
/// {@macro flutter.material.Material.clipBehavior} /// {@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 { class CupertinoFormSection extends StatelessWidget {
/// Creates a section that mimics standard iOS forms. /// Creates a section that mimics standard iOS forms.
/// ///
...@@ -107,7 +102,7 @@ class CupertinoFormSection extends StatelessWidget { ...@@ -107,7 +102,7 @@ class CupertinoFormSection extends StatelessWidget {
this.backgroundColor = CupertinoColors.systemGroupedBackground, this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration, this.decoration,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
}) : _type = _CupertinoFormSectionType.base, }) : _type = CupertinoListSectionType.base,
assert(children.length > 0); assert(children.length > 0);
/// Creates a section that mimics standard "Inset Grouped" iOS forms. /// Creates a section that mimics standard "Inset Grouped" iOS forms.
...@@ -149,14 +144,14 @@ class CupertinoFormSection extends StatelessWidget { ...@@ -149,14 +144,14 @@ class CupertinoFormSection extends StatelessWidget {
required this.children, required this.children,
this.header, this.header,
this.footer, this.footer,
this.margin = _kDefaultInsetGroupedRowsMargin, this.margin = _kFormDefaultInsetGroupedRowsMargin,
this.backgroundColor = CupertinoColors.systemGroupedBackground, this.backgroundColor = CupertinoColors.systemGroupedBackground,
this.decoration, this.decoration,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
}) : _type = _CupertinoFormSectionType.insetGrouped, }) : _type = CupertinoListSectionType.insetGrouped,
assert(children.length > 0); assert(children.length > 0);
final _CupertinoFormSectionType _type; final CupertinoListSectionType _type;
/// Sets the form section header. The section header lies above the /// Sets the form section header. The section header lies above the
/// [children] rows. /// [children] rows.
...@@ -203,116 +198,48 @@ class CupertinoFormSection extends StatelessWidget { ...@@ -203,116 +198,48 @@ class CupertinoFormSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Color dividerColor = CupertinoColors.separator.resolveFrom(context); final Widget? headerWidget = header == null
final double dividerHeight = 1.0 / MediaQuery.of(context).devicePixelRatio; ? null
: DefaultTextStyle(
// Long divider is used for wrapping the top and bottom of rows. style: TextStyle(
// Only used in _CupertinoFormSectionType.base mode fontSize: 13.0,
final Widget longDivider = Container( color: CupertinoColors.secondaryLabel.resolveFrom(context),
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,
),
),
),
Padding(
padding: margin,
child: ClipRRect(
borderRadius: childrenGroupBorderRadius,
clipBehavior: clipBehavior,
child: decoratedChildrenGroup,
), ),
), child: Padding(
if (footer != null) padding: _kFormDefaultHeaderMargin,
Align( child: header,
alignment: AlignmentDirectional.centerStart, ));
child: DefaultTextStyle(
style: TextStyle( final Widget? footerWidget = footer == null
fontSize: 13.0, ? null
color: CupertinoColors.secondaryLabel.resolveFrom(context), : DefaultTextStyle(
), style: TextStyle(
child: Padding( fontSize: 13.0,
padding: _kDefaultFooterMargin, color: CupertinoColors.secondaryLabel.resolveFrom(context),
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);
} }
} }
// 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,
),
),
],
),
);
}
}
// 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() { ...@@ -153,7 +153,7 @@ void main() {
expect(find.byType(ClipRRect), findsOneWidget); 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( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
home: Center( home: Center(
...@@ -164,7 +164,7 @@ void main() { ...@@ -164,7 +164,7 @@ void main() {
), ),
); );
final RenderClipRRect renderClip = tester.allRenderObjects.whereType<RenderClipRRect>().first; final Iterable<RenderClipRRect> renderClips = tester.allRenderObjects.whereType<RenderClipRRect>();
expect(renderClip.clipBehavior, equals(Clip.none)); expect(renderClips, isEmpty);
}); });
} }
// 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);
});
}
// 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);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment