Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
* Migrated from `package:flutter/material.dart` and `package:flutter/cupertino.dart` to the decoupled [`material_ui`](https://pub.dev/packages/material_ui) and [`cupertino_ui`](https://pub.dev/packages/cupertino_ui) packages (#207)
* Requires Flutter >=3.44.0 and Dart SDK >=3.12.0
* Your app must use `MaterialApp`/`CupertinoApp` from `material_ui`/`cupertino_ui`. In an app still on `package:flutter/material.dart`, `SettingsList` can't read your theme and falls back to default colors and light mode. Stay on `^3.0.1` until your app has migrated.
* New default look that matches iOS 26/27, Android 16/17 and current Chrome settings (see Design refresh). Sizes, paddings and default colors changed. Colors you set in `SettingsThemeData` still win.
* iOS-style switch tiles (iOS, macOS, Windows) now show the new `CupertinoSettingsSwitch` instead of `CupertinoSwitch`. Tests that find `CupertinoSwitch` need to look for `CupertinoSettingsSwitch`.

### Design refresh
* iOS, macOS, Windows: 26pt cards with continuous corners and 20pt side margins, 52pt rows with 17pt text, 17pt semibold section headers, and the iOS secondary grey for headers and footers. iOS no longer uses all-caps headers, so pass headers in sentence case.
* iOS, macOS, Windows: a new switch that matches the iOS 26/27 one, drawn entirely in Flutter. It has a 63x28 track and a pill-shaped thumb, and while it is pressed or dragged the thumb becomes a Liquid Glass-style lens. It is public as `CupertinoSettingsSwitch`, so you can use it outside settings lists too.
* Android, Linux, Fuchsia: every tile sits on its own card, 2dp apart, with 20dp corners at the ends of a group and 4dp in between, on a `surfaceContainer` page. 16sp titles, tighter rows, and switches that show a check or a cross.
* Web: Chrome-style white cards with 8px corners and a light shadow, 14px titles and 13px descriptions, 20px leading icons, a chevron on navigation tiles, near-black section titles, and a 680px column on wide screens (16px side margins in narrow windows).

### Bug fixes
* Colors, dark mode and platform now follow the app theme in apps built on `material_ui` (#206)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ SettingsTile.navigation(

### `SettingsTile.switchTile` — switch tile

Renders a `Switch` on Material platforms and a `CupertinoSwitch` on iOS/macOS.
Renders a `Switch` on Material platforms and a `CupertinoSettingsSwitch` (the iOS 26+ switch) on iOS/macOS.

```dart
SettingsTile.switchTile(
Expand Down
Binary file modified assets/v3/android_material3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/v3/android_settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/v3/ios_cupertino.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/v3/web_chrome.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 22 additions & 14 deletions example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ void main() {
);
final cupertinoSwitchFinder = find.descendant(
of: find.widgetWithText(SettingsTile, 'Enable custom theme'),
matching: find.byType(CupertinoSwitch),
matching: find.byType(CupertinoSettingsSwitch),
);

final bool hasMaterialSwitch = switchFinder.evaluate().isNotEmpty;
Expand All @@ -124,15 +124,18 @@ void main() {
expect(tester.widget<Switch>(switchFinder).value, false);
} else {
expect(
tester.widget<CupertinoSwitch>(cupertinoSwitchFinder).value, false);
tester.widget<CupertinoSettingsSwitch>(cupertinoSwitchFinder).value,
false);
await tester.tap(cupertinoSwitchFinder);
await pumpSettled(tester);
expect(
tester.widget<CupertinoSwitch>(cupertinoSwitchFinder).value, true);
tester.widget<CupertinoSettingsSwitch>(cupertinoSwitchFinder).value,
true);
await tester.tap(cupertinoSwitchFinder);
await pumpSettled(tester);
expect(
tester.widget<CupertinoSwitch>(cupertinoSwitchFinder).value, false);
tester.widget<CupertinoSettingsSwitch>(cupertinoSwitchFinder).value,
false);
}

await goBack(tester);
Expand Down Expand Up @@ -207,13 +210,14 @@ void main() {
.widget<SettingsTile>(find.widgetWithText(SettingsTile, 'Platform'));
expect((iosTile.value as Text).data, 'iOS');

// iOS uses CupertinoSwitch
// iOS uses CupertinoSettingsSwitch
final iosSwitchFinder = find.descendant(
of: find.widgetWithText(SettingsTile, 'Enable custom theme'),
matching: find.byType(CupertinoSwitch),
matching: find.byType(CupertinoSettingsSwitch),
);
expect(iosSwitchFinder, findsOneWidget);
expect(tester.widget<CupertinoSwitch>(iosSwitchFinder).value, false);
expect(
tester.widget<CupertinoSettingsSwitch>(iosSwitchFinder).value, false);

await goBack(tester);
});
Expand Down Expand Up @@ -256,18 +260,18 @@ void main() {
await tester.tap(find.text('Material 3 Theme Demo'));
await pumpSettled(tester);

// The tile shows a Switch on Android and a CupertinoSwitch on iOS.
// The tile shows a Switch on Android and a CupertinoSettingsSwitch on iOS.
final darkModeSwitchFinder = find.descendant(
of: find.widgetWithText(SettingsTile, 'Dark mode'),
matching: find.byWidgetPredicate(
(widget) => widget is Switch || widget is CupertinoSwitch,
(widget) => widget is Switch || widget is CupertinoSettingsSwitch,
),
);
bool darkModeValue() {
final widget = tester.widget(darkModeSwitchFinder);
return widget is Switch
? widget.value
: (widget as CupertinoSwitch).value;
: (widget as CupertinoSettingsSwitch).value;
}

expect(darkModeSwitchFinder, findsOneWidget);
Expand Down Expand Up @@ -317,15 +321,19 @@ void main() {
// Toggle Dark Appearance
final darkAppearanceSwitchFinder = find.descendant(
of: find.widgetWithText(SettingsTile, 'Dark Appearance'),
matching: find.byType(CupertinoSwitch),
matching: find.byType(CupertinoSettingsSwitch),
);
expect(darkAppearanceSwitchFinder, findsOneWidget);
final initial =
tester.widget<CupertinoSwitch>(darkAppearanceSwitchFinder).value;
final initial = tester
.widget<CupertinoSettingsSwitch>(darkAppearanceSwitchFinder)
.value;

await tester.tap(darkAppearanceSwitchFinder);
await pumpSettled(tester);
expect(tester.widget<CupertinoSwitch>(darkAppearanceSwitchFinder).value,
expect(
tester
.widget<CupertinoSettingsSwitch>(darkAppearanceSwitchFinder)
.value,
!initial);

await goBack(tester);
Expand Down
3 changes: 0 additions & 3 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 98676QY3V2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -559,7 +558,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 98676QY3V2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -582,7 +580,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 98676QY3V2;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
10 changes: 5 additions & 5 deletions example/lib/screens/gallery/ios_developer_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class _IosDeveloperScreenState extends State<IosDeveloperScreen> {
platform: DevicePlatform.iOS,
sections: [
SettingsSection(
title: Text('APPEARANCE'),
title: Text('Appearance'),
tiles: [
SettingsTile.switchTile(
onToggle: (value) {
Expand All @@ -39,7 +39,7 @@ class _IosDeveloperScreenState extends State<IosDeveloperScreen> {
],
),
SettingsSection(
title: Text('DISPLAY ZOOM'),
title: Text('Display zoom'),
tiles: [
SettingsTile.navigation(
onPressed: (_) {
Expand All @@ -60,7 +60,7 @@ class _IosDeveloperScreenState extends State<IosDeveloperScreen> {
],
),
SettingsSection(
title: Text('UI AUTOMATION'),
title: Text('UI automation'),
tiles: [
SettingsTile.switchTile(
onToggle: (_) {},
Expand All @@ -78,7 +78,7 @@ class _IosDeveloperScreenState extends State<IosDeveloperScreen> {
],
),
SettingsSection(
title: Text('STATE RESTORATION TESTING'),
title: Text('State restoration testing'),
tiles: [
SettingsTile.switchTile(
onToggle: (_) {},
Expand All @@ -95,7 +95,7 @@ class _IosDeveloperScreenState extends State<IosDeveloperScreen> {
],
),
SettingsSection(
title: Text('IAD DEVELOPER APP TESTING'),
title: Text('iAd developer app testing'),
tiles: [
SettingsTile.navigation(
title: Text('Downtime'),
Expand Down
1 change: 1 addition & 0 deletions lib/settings_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export 'package:settings_ui/src/tiles/abstract_settings_tile.dart';
export 'package:settings_ui/src/tiles/settings_tile.dart';
export 'package:settings_ui/src/utils/platform_utils.dart';
export 'package:settings_ui/src/utils/settings_theme.dart';
export 'package:settings_ui/src/tiles/platforms/cupertino_settings_switch.dart';
16 changes: 12 additions & 4 deletions lib/src/list/settings_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,12 @@ class SettingsList extends StatelessWidget {
DevicePlatform platform,
BuildContext context,
) {
if (MediaQuery.of(context).size.width > 810) {
double padding = (MediaQuery.of(context).size.width - 810) / 2;
// Chrome's settings page uses a narrower 680px column than the other
// platforms' 810px.
final maxContentWidth = platform == DevicePlatform.web ? 680.0 : 810.0;
final screenWidth = MediaQuery.of(context).size.width;
if (screenWidth > maxContentWidth) {
double padding = (screenWidth - maxContentWidth) / 2;
switch (platform) {
case DevicePlatform.android:
case DevicePlatform.fuchsia:
Expand All @@ -137,7 +141,10 @@ class SettingsList extends StatelessWidget {
case DevicePlatform.windows:
return EdgeInsets.symmetric(horizontal: padding);
case DevicePlatform.web:
return EdgeInsets.symmetric(vertical: 20, horizontal: padding);
return EdgeInsets.symmetric(
vertical: 20,
horizontal: padding < 16 ? 16 : padding,
);
case DevicePlatform.device:
throw Exception(
'You can\'t use the DevicePlatform.device in this context. '
Expand All @@ -154,7 +161,8 @@ class SettingsList extends StatelessWidget {
case DevicePlatform.windows:
return const EdgeInsets.symmetric(vertical: 0);
case DevicePlatform.web:
return const EdgeInsets.symmetric(vertical: 20);
// Keep the cards off the screen edges in narrow browser windows.
return const EdgeInsets.symmetric(vertical: 20, horizontal: 16);
case DevicePlatform.device:
throw Exception(
'You can\'t use the DevicePlatform.device in this context. '
Expand Down
85 changes: 52 additions & 33 deletions lib/src/sections/platforms/android_settings_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,50 +24,69 @@ class AndroidSettingsSection extends StatelessWidget {
Widget buildSectionBody(BuildContext context) {
final theme = SettingsTheme.of(context);
final textScaler = MediaQuery.textScalerOf(context);
final tileList = buildTileList();

if (title == null) {
return tileList;
}

return Padding(
padding: margin ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
titlePadding ??
EdgeInsetsDirectional.only(
top: textScaler.scale(24),
bottom: textScaler.scale(10),
start: 24,
end: 24,
),
child: DefaultTextStyle(
style: (theme.themeData.titleTextStyle ?? const TextStyle())
.copyWith(color: theme.themeData.titleTextColor),
child: title!,
),
),
Container(
color: theme.themeData.settingsSectionBackground,
child: tileList,
),
if (title != null)
Padding(
padding:
titlePadding ??
EdgeInsetsDirectional.only(
top: textScaler.scale(24),
bottom: textScaler.scale(8),
start: 24,
end: 24,
),
child: DefaultTextStyle(
style:
(theme.themeData.titleTextStyle ??
const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
))
.copyWith(color: theme.themeData.titleTextColor),
child: title!,
),
)
else
SizedBox(height: textScaler.scale(16)),
buildTileList(theme),
],
),
);
}

Widget buildTileList() {
return ListView.builder(
shrinkWrap: true,
itemCount: tiles.length,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return tiles[index];
},
/// Android 16+ settings put each tile on its own card: 2dp apart, with
/// large corners at the ends of the group and small ones in between.
Widget buildTileList(SettingsTheme theme) {
const outer = Radius.circular(20);
const inner = Radius.circular(4);

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
for (var i = 0; i < tiles.length; i++)
Padding(
padding: EdgeInsets.only(top: i == 0 ? 0 : 2),
child: ClipRRect(
borderRadius: BorderRadius.vertical(
top: i == 0 ? outer : inner,
bottom: i == tiles.length - 1 ? outer : inner,
),
child: ColoredBox(
color:
theme.themeData.settingsSectionBackground ??
const Color(0x00000000),
child: tiles[i],
),
),
),
],
),
);
}
}
13 changes: 8 additions & 5 deletions lib/src/sections/platforms/ios_settings_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class IOSSettingsSection extends StatelessWidget {
bottom: isLastNonDescriptive
? textScaler.scale(27)
: textScaler.scale(10),
left: 16,
right: 16,
left: 20,
right: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand All @@ -43,13 +43,16 @@ class IOSSettingsSection extends StatelessWidget {
padding:
titlePadding ??
EdgeInsetsDirectional.only(
start: 18,
bottom: textScaler.scale(5),
start: 16,
bottom: textScaler.scale(8),
),
child: DefaultTextStyle(
style:
(theme.themeData.titleTextStyle ??
const TextStyle(fontSize: 13))
const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
))
.copyWith(color: theme.themeData.titleTextColor),
child: title!,
),
Expand Down
Loading
Loading