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