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.
// 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( style: TextStyle(
fontSize: 13.0, fontSize: 13.0,
color: CupertinoColors.secondaryLabel.resolveFrom(context), color: CupertinoColors.secondaryLabel.resolveFrom(context),
), ),
child: Padding( child: Padding(
padding: _kDefaultHeaderMargin, padding: _kFormDefaultHeaderMargin,
child: header, child: header,
), ));
),
), final Widget? footerWidget = footer == null
Padding( ? null
padding: margin, : DefaultTextStyle(
child: ClipRRect(
borderRadius: childrenGroupBorderRadius,
clipBehavior: clipBehavior,
child: decoratedChildrenGroup,
),
),
if (footer != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: TextStyle( style: TextStyle(
fontSize: 13.0, fontSize: 13.0,
color: CupertinoColors.secondaryLabel.resolveFrom(context), color: CupertinoColors.secondaryLabel.resolveFrom(context),
), ),
child: Padding( child: Padding(
padding: _kDefaultFooterMargin, padding: _kFormDefaultFooterMargin,
child: footer, 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);
} }
} }
This diff is collapsed.
This diff is collapsed.
...@@ -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);
});
}
This diff is collapsed.
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