Adds CupertinoTheme (#23759)

This commit is contained in:
xster 2018-12-18 20:36:35 -08:00 committed by GitHub
parent 8b34a12d45
commit b8a035a3d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2443 additions and 918 deletions

View file

@ -1 +1 @@
e07cc0cb4fdf912062e71a6fd97cc91478d6e3b9
c47f1308188dca65b3899228cac37f252ea8b411

View file

@ -13,8 +13,11 @@ class CupertinoProgressIndicatorDemo extends StatelessWidget {
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Back',
middle: const Text('Cupertino Activity Indicator'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
middle: const Text('Activity Indicator'),
trailing: CupertinoDemoDocumentationButton(routeName),
),
child: const Center(

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@ -15,181 +14,193 @@ class CupertinoAlertDemo extends StatefulWidget {
}
class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String lastSelectedValue;
void showDemoDialog<T>({BuildContext context, Widget child}) {
showDialog<T>(
void showDemoDialog({BuildContext context, Widget child}) {
showCupertinoDialog<String>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => child,
).then<void>((T value) {
// The value passed to Navigator.pop() or null.
).then((String value) {
if (value != null) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text('You selected: $value'),
),
);
setState(() { lastSelectedValue = value; });
}
});
}
void showDemoActionSheet<T>({BuildContext context, Widget child}) {
showCupertinoModalPopup<T>(
void showDemoActionSheet({BuildContext context, Widget child}) {
showCupertinoModalPopup<String>(
context: context,
builder: (BuildContext context) => child,
).then<void>((T value) {
).then((String value) {
if (value != null) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text('You selected: $value'),
),
);
setState(() { lastSelectedValue = value; });
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Cupertino Alerts'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoAlertDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Alerts'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoAlertDemo.routeName),
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0),
children: <Widget>[
CupertinoButton(
child: const Text('Alert'),
color: CupertinoColors.activeBlue,
onPressed: () {
showDemoDialog<String>(
context: context,
child: CupertinoAlertDialog(
title: const Text('Discard draft?'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Discard'),
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context, 'Discard');
},
),
CupertinoDialogAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, 'Cancel');
},
),
],
),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton(
child: const Text('Alert with Title'),
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog<String>(
context: context,
child: CupertinoAlertDialog(
title: const Text('Allow "Maps" to access your location while you are using the app?'),
content: const Text('Your current location will be displayed on the map and used '
'for directions, nearby search results, and estimated travel times.'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Don\'t Allow'),
onPressed: () {
Navigator.pop(context, 'Disallow');
},
),
CupertinoDialogAction(
child: const Text('Allow'),
onPressed: () {
Navigator.pop(context, 'Allow');
},
),
],
),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton(
child: const Text('Alert with Buttons'),
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog<String>(
context: context,
child: const CupertinoDessertDialog(
title: Text('Select Favorite Dessert'),
content: Text('Please select your favorite type of dessert from the '
'list below. Your selection will be used to customize the suggested '
'list of eateries in your area.'),
),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton(
child: const Text('Alert Buttons Only'),
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog<String>(
context: context,
child: const CupertinoDessertDialog(),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton(
child: const Text('Action Sheet'),
color: CupertinoColors.activeBlue,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoActionSheet<String>(
context: context,
child: CupertinoActionSheet(
title: const Text('Favorite Dessert'),
message: const Text('Please select the best dessert from the options below.'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('Profiteroles'),
onPressed: () {
Navigator.pop(context, 'Profiteroles');
},
),
CupertinoActionSheetAction(
child: const Text('Cannolis'),
onPressed: () {
Navigator.pop(context, 'Cannolis');
},
),
CupertinoActionSheetAction(
child: const Text('Trifle'),
onPressed: () {
Navigator.pop(context, 'Trifle');
},
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: Builder(
builder: (BuildContext context) {
final List<Widget> stackChildren = <Widget>[
ListView(
// Add more padding to the normal safe area.
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
+ MediaQuery.of(context).padding,
children: <Widget>[
CupertinoButton.filled(
child: const Text('Alert'),
onPressed: () {
Navigator.pop(context, 'Cancel');
showDemoDialog(
context: context,
child: CupertinoAlertDialog(
title: const Text('Discard draft?'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Discard'),
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context, 'Discard');
},
),
CupertinoDialogAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, 'Cancel');
},
),
],
),
);
},
)
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert with Title'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog(
context: context,
child: CupertinoAlertDialog(
title: const Text('Allow "Maps" to access your location while you are using the app?'),
content: const Text('Your current location will be displayed on the map and used '
'for directions, nearby search results, and estimated travel times.'),
actions: <Widget>[
CupertinoDialogAction(
child: const Text('Don\'t Allow'),
onPressed: () {
Navigator.pop(context, 'Disallow');
},
),
CupertinoDialogAction(
child: const Text('Allow'),
onPressed: () {
Navigator.pop(context, 'Allow');
},
),
],
),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert with Buttons'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog(
context: context,
child: const CupertinoDessertDialog(
title: Text('Select Favorite Dessert'),
content: Text('Please select your favorite type of dessert from the '
'list below. Your selection will be used to customize the suggested '
'list of eateries in your area.'),
),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Alert Buttons Only'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoDialog(
context: context,
child: const CupertinoDessertDialog(),
);
},
),
const Padding(padding: EdgeInsets.all(8.0)),
CupertinoButton.filled(
child: const Text('Action Sheet'),
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 36.0),
onPressed: () {
showDemoActionSheet(
context: context,
child: CupertinoActionSheet(
title: const Text('Favorite Dessert'),
message: const Text('Please select the best dessert from the options below.'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('Profiteroles'),
onPressed: () {
Navigator.pop(context, 'Profiteroles');
},
),
CupertinoActionSheetAction(
child: const Text('Cannolis'),
onPressed: () {
Navigator.pop(context, 'Cannolis');
},
),
CupertinoActionSheetAction(
child: const Text('Trifle'),
onPressed: () {
Navigator.pop(context, 'Trifle');
},
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, 'Cancel');
},
)
),
);
},
),
],
),
];
if (lastSelectedValue != null) {
stackChildren.add(
Positioned(
bottom: 32.0,
child: Text('You selected: $lastSelectedValue'),
),
);
},
),
],
}
return Stack(
alignment: Alignment.center,
children: stackChildren,
);
},
),
),
);
}

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@ -19,64 +18,71 @@ class _CupertinoButtonDemoState extends State<CupertinoButtonsDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cupertino Buttons'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoButtonsDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Buttons'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoButtonsDemo.routeName),
),
body: Column(
children: <Widget> [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'iOS themed buttons are flat. They can have borders or backgrounds but '
'only when necessary.'
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget> [
Text(_pressedCount > 0
? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}'
: ' '),
const Padding(padding: EdgeInsets.all(12.0)),
Align(
alignment: const Alignment(0.0, -0.2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CupertinoButton(
child: const Text('Cupertino Button'),
onPressed: () {
setState(() { _pressedCount += 1; });
}
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'iOS themed buttons are flat. They can have borders or backgrounds but '
'only when necessary.'
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget> [
Text(_pressedCount > 0
? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}'
: ' '),
const Padding(padding: EdgeInsets.all(12.0)),
Align(
alignment: const Alignment(0.0, -0.2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CupertinoButton(
child: const Text('Cupertino Button'),
onPressed: () {
setState(() { _pressedCount += 1; });
}
),
const CupertinoButton(
child: Text('Disabled'),
onPressed: null,
),
],
),
const CupertinoButton(
child: Text('Disabled'),
onPressed: null,
),
],
),
),
const Padding(padding: EdgeInsets.all(12.0)),
CupertinoButton.filled(
child: const Text('With Background'),
onPressed: () {
setState(() { _pressedCount += 1; });
}
),
const Padding(padding: EdgeInsets.all(12.0)),
const CupertinoButton.filled(
child: Text('Disabled'),
onPressed: null,
),
],
),
const Padding(padding: EdgeInsets.all(12.0)),
CupertinoButton(
child: const Text('With Background'),
color: CupertinoColors.activeBlue,
onPressed: () {
setState(() { _pressedCount += 1; });
}
),
const Padding(padding: EdgeInsets.all(12.0)),
const CupertinoButton(
child: Text('Disabled'),
color: CupertinoColors.activeBlue,
onPressed: null,
),
],
)
),
],
),
],
),
)
);
}

View file

@ -52,11 +52,7 @@ class CupertinoNavigationDemo extends StatelessWidget {
// Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => Future<bool>.value(true),
child: DefaultTextStyle(
style: const TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
color: CupertinoColors.black,
),
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
@ -241,7 +237,6 @@ class Tab1RowItem extends StatelessWidget {
CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.plus_circled,
color: CupertinoColors.activeBlue,
semanticLabel: 'Add',
),
onPressed: () { },
@ -249,7 +244,6 @@ class Tab1RowItem extends StatelessWidget {
CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.share,
color: CupertinoColors.activeBlue,
semanticLabel: 'Share',
),
onPressed: () { },
@ -352,8 +346,7 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton(
color: CupertinoColors.activeBlue,
CupertinoButton.filled(
minSize: 30.0,
padding: const EdgeInsets.symmetric(horizontal: 24.0),
borderRadius: BorderRadius.circular(32.0),
@ -367,12 +360,11 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
),
onPressed: () { },
),
CupertinoButton(
color: CupertinoColors.activeBlue,
CupertinoButton.filled(
minSize: 30.0,
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(32.0),
child: const Icon(CupertinoIcons.ellipsis, color: CupertinoColors.white),
child: const Icon(CupertinoIcons.ellipsis),
onPressed: () { },
),
],
@ -722,7 +714,11 @@ class CupertinoDemoTab3 extends StatelessWidget {
trailing: trailingButtons,
),
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)),
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)),
@ -736,9 +732,9 @@ class CupertinoDemoTab3 extends StatelessWidget {
);
},
child: Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
border: Border(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
),
@ -750,10 +746,10 @@ class CupertinoDemoTab3 extends StatelessWidget {
top: false,
bottom: false,
child: Row(
children: const <Widget>[
children: <Widget>[
Text(
'Sign in',
style: TextStyle(color: CupertinoColors.activeBlue),
style: TextStyle(color: CupertinoTheme.of(context).primaryColor),
)
],
),
@ -791,8 +787,7 @@ class Tab3Dialog extends StatelessWidget {
color: Color(0xFF646464),
),
const Padding(padding: EdgeInsets.only(top: 18.0)),
CupertinoButton(
color: CupertinoColors.activeBlue,
CupertinoButton.filled(
child: const Text('Sign in'),
onPressed: () {
Navigator.pop(context);

View file

@ -2,7 +2,6 @@
// 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/material.dart';
import 'package:intl/intl.dart';
import '../../gallery/demo.dart';
@ -34,9 +33,9 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
Widget _buildMenu(List<Widget> children) {
return Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
border: Border(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
),
@ -47,16 +46,9 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
child: SafeArea(
top: false,
bottom: false,
child: DefaultTextStyle(
style: const TextStyle(
letterSpacing: -0.24,
fontSize: 17.0,
color: CupertinoColors.black,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
),
),
@ -249,19 +241,23 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cupertino Picker'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoPickerDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Picker'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoPickerDemo.routeName),
),
body: DefaultTextStyle(
style: const TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
color: CupertinoColors.black,
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)),
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: ListView(
children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)),

View file

@ -39,19 +39,21 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: const TextStyle(
fontFamily: '.SF UI Text',
inherit: false,
fontSize: 17.0,
color: CupertinoColors.black,
),
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoPageScaffold(
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)),
decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: const Text('Cupertino Refresh'),
largeTitle: const Text('Refresh'),
// We're specifying a back label here because the previous page
// is a Material page. CupertinoPageRoutes could auto-populate
// these back labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoRefreshControlDemo.routeName),
),
@ -152,7 +154,7 @@ class _ListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: CupertinoColors.white,
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
height: 60.0,
padding: const EdgeInsets.only(top: 9.0),
child: Row(
@ -215,11 +217,11 @@ class _ListItem extends StatelessWidget {
letterSpacing: -0.41,
),
),
const Padding(
padding: EdgeInsets.only(left: 9.0),
Padding(
padding: const EdgeInsets.only(left: 9.0),
child: Icon(
CupertinoIcons.info,
color: CupertinoColors.activeBlue
color: CupertinoTheme.of(context).primaryColor,
),
),
],

View file

@ -50,68 +50,77 @@ class _CupertinoSegmentedControlDemoState extends State<CupertinoSegmentedContro
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Segmented Control'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSegmentedControlDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Segmented Control'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSegmentedControlDemo.routeName),
),
body: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(16.0),
),
SizedBox(
width: 500.0,
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 32.0,
horizontal: 16.0,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.all(16.0),
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 64.0,
horizontal: 16.0,
SizedBox(
width: 500.0,
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
),
decoration: BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.circular(3.0),
boxShadow: const <BoxShadow>[
BoxShadow(
offset: Offset(0.0, 3.0),
blurRadius: 5.0,
spreadRadius: -1.0,
color: _kKeyUmbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 6.0),
blurRadius: 10.0,
spreadRadius: 0.0,
color: _kKeyPenumbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 1.0),
blurRadius: 18.0,
spreadRadius: 0.0,
color: _kAmbientShadowOpacity,
),
],
),
child: icons[sharedValue],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 32.0,
horizontal: 16.0,
),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 64.0,
horizontal: 16.0,
),
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(3.0),
boxShadow: const <BoxShadow>[
BoxShadow(
offset: Offset(0.0, 3.0),
blurRadius: 5.0,
spreadRadius: -1.0,
color: _kKeyUmbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 6.0),
blurRadius: 10.0,
spreadRadius: 0.0,
color: _kKeyPenumbraOpacity,
),
BoxShadow(
offset: Offset(0.0, 1.0),
blurRadius: 18.0,
spreadRadius: 0.0,
color: _kAmbientShadowOpacity,
),
],
),
child: icons[sharedValue],
),
),
),
],
),
],
),
),
);
}

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@ -20,49 +19,58 @@ class _CupertinoSliderDemoState extends State<CupertinoSliderDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cupertino Sliders'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSliderDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Sliders'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSliderDemo.routeName),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _value,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_value = value;
});
}
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _value,
min: 0.0,
max: 100.0,
onChanged: (double value) {
setState(() {
_value = value;
});
}
),
Text('Cupertino Continuous: ${_value.toStringAsFixed(1)}'),
]
),
Text('Cupertino Continuous: ${_value.toStringAsFixed(1)}'),
]
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _discreteValue,
min: 0.0,
max: 100.0,
divisions: 5,
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
}
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
CupertinoSlider(
value: _discreteValue,
min: 0.0,
max: 100.0,
divisions: 5,
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
}
),
Text('Cupertino Discrete: $_discreteValue'),
]
),
Text('Cupertino Discrete: $_discreteValue'),
]
],
),
],
),
),
),
);

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart';
@ -20,62 +19,71 @@ class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cupertino Switch'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSwitchDemo.routeName)],
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Switch'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoSwitchDemo.routeName),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Semantics(
container: true,
child: Column(
children: <Widget>[
CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Semantics(
container: true,
child: Column(
children: <Widget>[
CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
const Text(
'Active'
),
],
),
const Text(
'Active'
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: true,
onChanged: null,
),
Text(
'Disabled'
),
],
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: false,
onChanged: null,
),
Text(
'Disabled'
),
],
),
),
],
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: true,
onChanged: null,
),
Text(
'Disabled'
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: false,
onChanged: null,
),
Text(
'Disabled'
),
],
),
),
],
),
),
),
);

View file

@ -160,6 +160,9 @@ class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> {
),
child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
middle: Text('Text Fields'),
),

View file

@ -4,6 +4,7 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
@ -125,7 +126,6 @@ class _GalleryAppState extends State<GalleryApp> {
child: home,
);
}
return MaterialApp(
theme: _options.theme.data.copyWith(platform: _options.platform),
title: 'Flutter Gallery',
@ -137,7 +137,17 @@ class _GalleryAppState extends State<GalleryApp> {
builder: (BuildContext context, Widget child) {
return Directionality(
textDirection: _options.textDirection,
child: _applyTextScaleFactor(child),
child: _applyTextScaleFactor(
// Specifically use a blank Cupertino theme here and do not transfer
// over the Material primary color etc except the brightness to
// showcase standard iOS looks.
CupertinoTheme(
data: CupertinoThemeData(
brightness: _options.theme.data.brightness,
),
child: child,
),
),
);
},
home: home,

View file

@ -30,5 +30,7 @@ export 'src/cupertino/tab_scaffold.dart';
export 'src/cupertino/tab_view.dart';
export 'src/cupertino/text_field.dart';
export 'src/cupertino/text_selection.dart';
export 'src/cupertino/text_theme.dart';
export 'src/cupertino/theme.dart';
export 'src/cupertino/thumb_painter.dart';
export 'widgets.dart';

View file

@ -10,16 +10,7 @@ import 'colors.dart';
import 'icons.dart';
import 'localizations.dart';
import 'route.dart';
// Based on specs from https://developer.apple.com/design/resources/ for
// iOS 12.
const TextStyle _kDefaultTextStyle = TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.38,
color: CupertinoColors.black,
decoration: TextDecoration.none,
);
import 'theme.dart';
/// An application that uses Cupertino design.
///
@ -79,6 +70,7 @@ class CupertinoApp extends StatefulWidget {
Key key,
this.navigatorKey,
this.home,
this.theme,
this.routes = const <String, WidgetBuilder>{},
this.initialRoute,
this.onGenerateRoute,
@ -114,6 +106,12 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.home}
final Widget home;
/// The top-level [CupertinoTheme] styling.
///
/// A null [theme] or unspecified [theme] attributes will default to iOS
/// system values.
final CupertinoThemeData theme;
/// The application's top-level routing table.
///
/// When a named route is pushed with [Navigator.pushNamed], the route name is
@ -266,48 +264,52 @@ class _CupertinoAppState extends State<CupertinoApp> {
@override
Widget build(BuildContext context) {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
return ScrollConfiguration(
behavior: _AlwaysCupertinoScrollBehavior(),
child: WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
// TODO(dnfield): when https://github.com/dart-lang/sdk/issues/34572 is resolved
// this can use type arguments again
pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<dynamic>(settings: settings, builder: builder),
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _kDefaultTextStyle,
color: widget.color ?? CupertinoColors.activeBlue,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return CupertinoButton(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
color: CupertinoColors.activeBlue,
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
child: CupertinoTheme(
data: effectiveThemeData,
child: WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
// TODO(dnfield): when https://github.com/dart-lang/sdk/issues/34572 is resolved
// this can use type arguments again
pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) =>
CupertinoPageRoute<dynamic>(settings: settings, builder: builder),
home: widget.home,
routes: widget.routes,
initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute,
onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: effectiveThemeData.textTheme.textStyle,
color: widget.color ?? CupertinoColors.activeBlue,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return CupertinoButton.filled(
child: const Icon(
CupertinoIcons.search,
size: 28.0,
color: CupertinoColors.white,
),
padding: EdgeInsets.zero,
onPressed: onPressed,
);
},
),
),
);
}

View file

@ -7,11 +7,11 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
// Standard iOS 10 tab bar height.
const double _kTabBarHeight = 50.0;
const Color _kDefaultTabBarBackgroundColor = Color(0xCCF8F8F8);
const Color _kDefaultTabBarBorderColor = Color(0x4C000000);
/// An iOS-styled bottom navigation tab bar.
@ -21,10 +21,12 @@ const Color _kDefaultTabBarBorderColor = Color(0x4C000000);
///
/// This [StatelessWidget] doesn't store the active tab itself. You must
/// listen to the [onTap] callbacks and call `setState` with a new [currentIndex]
/// for the new selection to reflect.
/// for the new selection to reflect. This can also be done automatically
/// by wrapping this with a [CupertinoTabScaffold].
///
/// Tab changes typically trigger a switch between [Navigator]s, each with its
/// own navigation stack, per standard iOS design.
/// own navigation stack, per standard iOS design. This can be done by using
/// [CupertinoTabView]s inside each tab builder in [CupertinoTabScaffold].
///
/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
/// default), it will produce a blurring effect to the content behind it.
@ -40,8 +42,8 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
@required this.items,
this.onTap,
this.currentIndex = 0,
this.backgroundColor = _kDefaultTabBarBackgroundColor,
this.activeColor = CupertinoColors.activeBlue,
this.backgroundColor,
this.activeColor,
this.inactiveColor = CupertinoColors.inactiveGray,
this.iconSize = 30.0,
this.border = const Border(
@ -56,6 +58,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
assert(currentIndex != null),
assert(0 <= currentIndex && currentIndex < items.length),
assert(iconSize != null),
assert(inactiveColor != null),
super(key: key);
/// The interactive items laid out within the bottom navigation bar.
@ -78,14 +81,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
/// The background color of the tab bar. If it contains transparency, the
/// tab bar will automatically produce a blurring effect to the content
/// behind it.
///
/// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null.
final Color backgroundColor;
/// The foreground color of the icon and title for the [BottomNavigationBarItem]
/// of the selected tab.
///
/// Defaults to [CupertinoTheme]'s `primaryColor` if null.
final Color activeColor;
/// The foreground color of the icon and title for the [BottomNavigationBarItem]s
/// in the unselected state.
///
/// Defaults to [CupertinoColors.inactiveGray] and cannot be null.
final Color inactiveColor;
/// The size of all of the [BottomNavigationBarItem] icons.
@ -102,19 +111,25 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
/// The default value is a one physical pixel top border with grey color.
final Border border;
/// True if the tab bar's background color has no transparency.
bool get opaque => backgroundColor.alpha == 0xFF;
@override
Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
/// Indicates whether the tab bar is fully opaque or can have contents behind
/// it show through it.
bool opaque(BuildContext context) {
final Color backgroundColor =
this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
return backgroundColor.alpha == 0xFF;
}
@override
Widget build(BuildContext context) {
final double bottomPadding = MediaQuery.of(context).padding.bottom;
Widget result = DecoratedBox(
decoration: BoxDecoration(
border: border,
color: backgroundColor,
color: backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
),
child: SizedBox(
height: _kTabBarHeight + bottomPadding,
@ -124,19 +139,13 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
size: iconSize,
),
child: DefaultTextStyle( // Default with the inactive state.
style: TextStyle(
fontFamily: '.SF UI Text',
fontSize: 10.0,
letterSpacing: 0.1,
fontWeight: FontWeight.w400,
color: inactiveColor,
),
style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactiveColor),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: Row(
// Align bottom since we want the labels to be aligned.
crossAxisAlignment: CrossAxisAlignment.end,
children: _buildTabItems(),
children: _buildTabItems(context),
),
),
),
@ -144,7 +153,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
),
);
if (!opaque) {
if (!opaque(context)) {
// For non-opaque backgrounds, apply a blur effect.
result = ClipRect(
child: BackdropFilter(
@ -157,13 +166,14 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
return result;
}
List<Widget> _buildTabItems() {
List<Widget> _buildTabItems(BuildContext context) {
final List<Widget> result = <Widget>[];
for (int index = 0; index < items.length; index += 1) {
final bool active = index == currentIndex;
result.add(
_wrapActiveItem(
context,
Expanded(
child: Semantics(
selected: active,
@ -205,10 +215,11 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
}
/// Change the active tab item's icon and title colors to active.
Widget _wrapActiveItem(Widget item, { @required bool active }) {
Widget _wrapActiveItem(BuildContext context, Widget item, { @required bool active }) {
if (!active)
return item;
final Color activeColor = this.activeColor ?? CupertinoTheme.of(context).primaryColor;
return IconTheme.merge(
data: IconThemeData(color: activeColor),
child: DefaultTextStyle.merge(

View file

@ -5,28 +5,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
const Color _kDisabledBackground = Color(0xFFA9A9A9);
const Color _kDisabledForeground = Color(0xFFC4C4C4);
const TextStyle _kButtonTextStyle = TextStyle(
fontFamily: '.SF UI Text',
inherit: false,
fontSize: 17.5,
letterSpacing: -0.24,
fontWeight: FontWeight.w400,
color: CupertinoColors.activeBlue,
textBaseline: TextBaseline.alphabetic,
);
final TextStyle _kDisabledButtonTextStyle = _kButtonTextStyle.copyWith(
color: _kDisabledForeground,
);
final TextStyle _kBackgroundButtonTextStyle = _kButtonTextStyle.copyWith(
color: CupertinoColors.white,
);
// Measured against iOS 12 in Xcode.
const Color _kDisabledForeground = Color(0xFFD1D1D1);
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
@ -53,7 +36,26 @@ class CupertinoButton extends StatefulWidget {
this.pressedOpacity = 0.1,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
@required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0));
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
_filled = false;
/// Creates an iOS-style button with a filled background.
///
/// The background color is derived from the [CupertinoTheme]'s `primaryColor`.
///
/// To specify a custom background color, use the [color] argument of the
/// default constructor.
const CupertinoButton.filled({
@required this.child,
this.padding,
this.disabledColor,
this.minSize = 44.0,
this.pressedOpacity = 0.1,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
@required this.onPressed,
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
color = null,
_filled = true;
/// The widget below this widget in the tree.
///
@ -68,6 +70,9 @@ class CupertinoButton extends StatefulWidget {
/// The color of the button's background.
///
/// Defaults to null which produces a button with no background or border.
///
/// Defaults to the [CupertinoTheme]'s `primaryColor` when the
/// [CupertinoButton.filled] constructor is used.
final Color color;
/// The color of the button's background when the button is disabled.
@ -105,6 +110,8 @@ class CupertinoButton extends StatefulWidget {
/// Defaults to round corners of 8 logical pixels.
final BorderRadius borderRadius;
final bool _filled;
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
/// enable a button, set its [onPressed] property to a non-null value.
bool get enabled => onPressed != null;
@ -198,7 +205,15 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
@override
Widget build(BuildContext context) {
final bool enabled = widget.enabled;
final Color backgroundColor = widget.color;
final Color primaryColor = CupertinoTheme.of(context).primaryColor;
final Color backgroundColor = widget.color ?? (widget._filled ? primaryColor : null);
final Color foregroundColor = backgroundColor != null
? CupertinoTheme.of(context).primaryContrastingColor
: enabled
? primaryColor
: _kDisabledForeground;
final TextStyle textStyle =
CupertinoTheme.of(context).textTheme.textStyle.copyWith(color: foregroundColor);
return GestureDetector(
behavior: HitTestBehavior.opaque,
@ -232,12 +247,11 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
widthFactor: 1.0,
heightFactor: 1.0,
child: DefaultTextStyle(
style: backgroundColor != null
? _kBackgroundButtonTextStyle
: enabled
? _kButtonTextStyle
: _kDisabledButtonTextStyle,
child: widget.child,
style: textStyle,
child: IconTheme(
data: IconThemeData(color: foregroundColor),
child: widget.child,
),
),
),
),

View file

@ -11,15 +11,27 @@ class CupertinoColors {
/// iOS 10's default blue color. Used to indicate active elements such as
/// buttons, selected tabs and your own chat bubbles.
///
/// This is SystemBlue in the iOS palette.
static const Color activeBlue = Color(0xFF007AFF);
/// iOS 10's default green color. Used to indicate active accents such as
/// the switch in its on state and some accent buttons such as the call button
/// and Apple Map's 'Go' button.
///
/// This is SystemGreen in the iOS palette.
static const Color activeGreen = Color(0xFF4CD964);
/// iOS 12's default dark mode color. Used in place of the [activeBlue] color
/// as the default active elements' color when the theme's brightness is dark.
///
/// This is SystemOrange in the iOS palette.
static const Color activeOrange = Color(0xFFFF9500);
/// Opaque white color. Used for backgrounds and fonts against dark backgrounds.
///
/// This is SystemWhiteColor in the iOS palette.
///
/// See also:
///
/// * [material.Colors.white], the same color, in the material design palette.
@ -28,6 +40,8 @@ class CupertinoColors {
/// Opaque black color. Used for texts against light backgrounds.
///
/// This is SystemBlackColor in the iOS palette.
///
/// See also:
///
/// * [material.Colors.black], the same color, in the material design palette.
@ -35,12 +49,26 @@ class CupertinoColors {
static const Color black = Color(0xFF000000);
/// Used in iOS 10 for light background fills such as the chat bubble background.
///
/// This is SystemLightGrayColor in the iOS palette.
static const Color lightBackgroundGray = Color(0xFFE5E5EA);
/// Used in iOS 12 for very light background fills in tables between cell groups.
///
/// This is SystemExtraLightGrayColor in the iOS palette.
static const Color extraLightBackgroundGray = Color(0xFFEFEFF4);
/// Used in iOS 12 for very dark background fills in tables between cell groups
/// in dark mode.
// Value derived from screenshot from the dark themed Apple Watch app.
static const Color darkBackgroundGray = Color(0xFF171717);
/// Used in iOS 11 for unselected selectables such as tab bar items in their
/// inactive state or de-emphasized subtitles and details text.
///
/// Not the same gray as disabled buttons etc.
///
/// This is SystemGrayColor in the iOS palette.
static const Color inactiveGray = Color(0xFF8E8E93);
/// Used for iOS 10 for destructive actions such as the delete actions in
@ -48,5 +76,7 @@ class CupertinoColors {
///
/// Not the same red as the camera shutter or springboard icon notifications
/// or the foreground red theme in various native apps such as HealthKit.
///
/// This is SystemRed in the iOS palette.
static const Color destructiveRed = Color(0xFFFF3B30);
}

View file

@ -11,10 +11,10 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
import 'colors.dart';
import 'icons.dart';
import 'page_scaffold.dart';
import 'route.dart';
import 'theme.dart';
/// Standard iOS navigation bar height without the status bar.
///
@ -36,7 +36,6 @@ const double _kNavBarBackButtonTapWidth = 50.0;
/// Title text transfer fade.
const Duration _kNavBarTitleFadeDuration = Duration(milliseconds: 150);
const Color _kDefaultNavBarBackgroundColor = Color(0xCCF8F8F8);
const Color _kDefaultNavBarBorderColor = Color(0x4C000000);
const Border _kDefaultNavBarBorder = Border(
@ -47,22 +46,6 @@ const Border _kDefaultNavBarBorder = Border(
),
);
const TextStyle _kMiddleTitleTextStyle = TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
fontWeight: FontWeight.w600,
letterSpacing: -0.08,
color: CupertinoColors.black,
);
const TextStyle _kLargeTitleTextStyle = TextStyle(
fontFamily: '.SF Pro Display',
fontSize: 34.0,
fontWeight: FontWeight.w700,
letterSpacing: 0.24,
color: CupertinoColors.black,
);
// There's a single tag for all instances of navigation bars because they can
// all transition between each other (per Navigator) via Hero transitions.
const _HeroTag _defaultHeroTag = _HeroTag(null);
@ -94,15 +77,6 @@ class _HeroTag {
}
}
TextStyle _navBarItemStyle(Color color) {
return TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
letterSpacing: -0.24,
color: color,
);
}
/// Returns `child` wrapped with background and a bottom border if background color
/// is opaque. Otherwise, also blur with [BackdropFilter].
///
@ -145,6 +119,21 @@ Widget _wrapWithBackground({
);
}
// This exists to support backward compatibility with arguments like
// `actionsForegroundColor`. CupertinoThemes can be used to support these
// scenarios now. To support `actionsForegroundColor`, the nav bar rewraps
// its children with a CupertinoTheme.
Widget _wrapActiveColor(Color color, BuildContext context, Widget child) {
if (color == null) {
return child;
}
return CupertinoTheme(
data: CupertinoTheme.of(context).copyWith(primaryColor: color),
child: child,
);
}
// Whether the current route supports nav bar hero transitions from or to.
bool _isTransitionable(BuildContext context) {
final ModalRoute<dynamic> route = ModalRoute.of(context);
@ -206,9 +195,9 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
this.backgroundColor = _kDefaultNavBarBackgroundColor,
this.backgroundColor,
this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue,
this.actionsForegroundColor,
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
}) : assert(automaticallyImplyLeading != null),
@ -296,6 +285,8 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
/// The background color of the navigation bar. If it contains transparency, the
/// tab bar will automatically produce a blurring effect to the content
/// behind it.
///
/// Defaults to [CupertinoTheme]'s `barBackgroundColor` if null.
/// {@endtemplate}
final Color backgroundColor;
@ -321,11 +312,16 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
/// {@endtemplate}
final Border border;
/// {@template flutter.cupertino.navBar.actionsForegroundColor}
/// Default color used for text and icons of the [leading] and [trailing]
/// widgets in the navigation bar.
///
/// Defaults to the `primaryColor` of the [CupertinoTheme] when null.
/// {@endtemplate}
///
/// The default color for text in the [middle] slot is always black, as per
/// iOS standard design.
@Deprecated('Use CupertinoTheme and primaryColor to propagate color')
final Color actionsForegroundColor;
/// {@template flutter.cupertino.navBar.transitionBetweenRoutes}
@ -366,7 +362,7 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer
/// True if the navigation bar's background color has no transparency.
@override
bool get fullObstruction => backgroundColor.alpha == 0xFF;
bool get fullObstruction => backgroundColor == null ? null : backgroundColor.alpha == 0xFF;
@override
Size get preferredSize {
@ -393,6 +389,9 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
@override
Widget build(BuildContext context) {
final Color backgroundColor =
widget.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
keys: keys,
route: ModalRoute.of(context),
@ -403,40 +402,55 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
userMiddle: widget.middle,
userTrailing: widget.trailing,
padding: widget.padding,
actionsForegroundColor: widget.actionsForegroundColor,
userLargeTitle: null,
large: false,
);
final Widget navBar = _wrapWithBackground(
border: widget.border,
backgroundColor: widget.backgroundColor,
child: _PersistentNavigationBar(
components: components,
padding: widget.padding,
backgroundColor: backgroundColor,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: _PersistentNavigationBar(
components: components,
padding: widget.padding,
),
),
);
if (!widget.transitionBetweenRoutes || !_isTransitionable(context)) {
return navBar;
// Lint ignore to maintain backward compatibility.
return _wrapActiveColor(widget.actionsForegroundColor, context, navBar); // ignore: deprecated_member_use
}
return Hero(
tag: widget.heroTag == _defaultHeroTag
? _HeroTag(Navigator.of(context))
: widget.heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
transitionOnUserGestures: true,
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: widget.backgroundColor,
actionsForegroundColor: widget.actionsForegroundColor,
border: widget.border,
hasUserMiddle: widget.middle != null,
largeExpanded: false,
child: navBar,
return _wrapActiveColor(
// Lint ignore to maintain backward compatibility.
widget.actionsForegroundColor, // ignore: deprecated_member_use
context,
Builder(
// Get the context that might have a possibly changed CupertinoTheme.
builder: (BuildContext context) {
return Hero(
tag: widget.heroTag == _defaultHeroTag
? _HeroTag(Navigator.of(context))
: widget.heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
transitionOnUserGestures: true,
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: backgroundColor,
backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: null,
border: widget.border,
hasUserMiddle: widget.middle != null,
largeExpanded: false,
child: navBar,
),
);
},
),
);
}
@ -502,9 +516,9 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
this.middle,
this.trailing,
this.border = _kDefaultNavBarBorder,
this.backgroundColor = _kDefaultNavBarBackgroundColor,
this.backgroundColor,
this.padding,
this.actionsForegroundColor = CupertinoColors.activeBlue,
this.actionsForegroundColor,
this.transitionBetweenRoutes = true,
this.heroTag = _defaultHeroTag,
}) : assert(automaticallyImplyLeading != null),
@ -582,11 +596,11 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
/// {@macro flutter.cupertino.navBar.border}
final Border border;
/// Default color used for text and icons of the [leading] and [trailing]
/// widgets in the navigation bar.
/// {@macro flutter.cupertino.navBar.actionsForegroundColor}
///
/// The default color for text in the [largeTitle] slot is always black, as per
/// iOS standard design.
@Deprecated('Use CupertinoTheme and primaryColor to propagate color')
final Color actionsForegroundColor;
/// {@macro flutter.cupertino.navBar.transitionBetweenRoutes}
@ -616,6 +630,9 @@ class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigation
@override
Widget build(BuildContext context) {
// Lint ignore to maintain backward compatibility.
final Color actionsForegroundColor = widget.actionsForegroundColor ?? CupertinoTheme.of(context).primaryColor; // ignore: deprecated_member_use
final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
keys: keys,
route: ModalRoute.of(context),
@ -627,24 +644,28 @@ class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigation
userTrailing: widget.trailing,
userLargeTitle: widget.largeTitle,
padding: widget.padding,
actionsForegroundColor: widget.actionsForegroundColor,
large: true,
);
return SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: _LargeTitleNavigationBarSliverDelegate(
keys: keys,
components: components,
userMiddle: widget.middle,
backgroundColor: widget.backgroundColor,
border: widget.border,
padding: widget.padding,
actionsForegroundColor: widget.actionsForegroundColor,
transitionBetweenRoutes: widget.transitionBetweenRoutes,
heroTag: widget.heroTag,
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
alwaysShowMiddle: widget.middle != null,
return _wrapActiveColor(
// Lint ignore to maintain backward compatibility.
widget.actionsForegroundColor, // ignore: deprecated_member_use
context,
SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: _LargeTitleNavigationBarSliverDelegate(
keys: keys,
components: components,
userMiddle: widget.middle,
backgroundColor: widget.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
border: widget.border,
padding: widget.padding,
actionsForegroundColor: actionsForegroundColor,
transitionBetweenRoutes: widget.transitionBetweenRoutes,
heroTag: widget.heroTag,
persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
alwaysShowMiddle: widget.middle != null,
),
),
);
}
@ -702,40 +723,43 @@ class _LargeTitleNavigationBarSliverDelegate
final Widget navBar = _wrapWithBackground(
border: border,
backgroundColor: backgroundColor,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned(
top: persistentHeight,
left: 0.0,
right: 0.0,
bottom: 0.0,
child: ClipRect(
// The large title starts at the persistent bar.
// It's aligned with the bottom of the sliver and expands clipped
// and behind the persistent bar.
child: OverflowBox(
minHeight: 0.0,
maxHeight: double.infinity,
alignment: AlignmentDirectional.bottomStart,
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: _kNavBarEdgePadding,
bottom: 8.0, // Bottom has a different padding.
),
child: SafeArea(
top: false,
bottom: false,
child: AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration,
child: Semantics(
header: true,
child: DefaultTextStyle(
style: _kLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: components.largeTitle,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned(
top: persistentHeight,
left: 0.0,
right: 0.0,
bottom: 0.0,
child: ClipRect(
// The large title starts at the persistent bar.
// It's aligned with the bottom of the sliver and expands clipped
// and behind the persistent bar.
child: OverflowBox(
minHeight: 0.0,
maxHeight: double.infinity,
alignment: AlignmentDirectional.bottomStart,
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: _kNavBarEdgePadding,
bottom: 8.0, // Bottom has a different padding.
),
child: SafeArea(
top: false,
bottom: false,
child: AnimatedOpacity(
opacity: showLargeTitle ? 1.0 : 0.0,
duration: _kNavBarTitleFadeDuration,
child: Semantics(
header: true,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: components.largeTitle,
),
),
),
),
@ -743,14 +767,14 @@ class _LargeTitleNavigationBarSliverDelegate
),
),
),
),
Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: persistentNavigationBar,
),
],
Positioned(
left: 0.0,
right: 0.0,
top: 0.0,
child: persistentNavigationBar,
),
],
),
),
);
@ -772,7 +796,9 @@ class _LargeTitleNavigationBarSliverDelegate
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: backgroundColor,
actionsForegroundColor: actionsForegroundColor,
backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
border: border,
hasUserMiddle: userMiddle != null,
largeExpanded: showLargeTitle,
@ -822,7 +848,7 @@ class _PersistentNavigationBar extends StatelessWidget {
if (middle != null) {
middle = DefaultTextStyle(
style: _kMiddleTitleTextStyle,
style: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
child: Semantics(header: true, child: middle),
);
// When the middle's visibility can change on the fly like with large title
@ -844,7 +870,6 @@ class _PersistentNavigationBar extends StatelessWidget {
leading = CupertinoNavigationBarBackButton._assemble(
backChevron,
backLabel,
components.actionsForegroundColor,
);
}
@ -919,7 +944,6 @@ class _NavigationBarStaticComponents {
@required Widget userTrailing,
@required Widget userLargeTitle,
@required EdgeInsetsDirectional padding,
@required this.actionsForegroundColor,
@required bool large,
}) : leading = createLeading(
leadingKey: keys.leadingKey,
@ -927,7 +951,6 @@ class _NavigationBarStaticComponents {
route: route,
automaticallyImplyLeading: automaticallyImplyLeading,
padding: padding,
actionsForegroundColor: actionsForegroundColor,
),
backChevron = createBackChevron(
backChevronKey: keys.backChevronKey,
@ -954,7 +977,6 @@ class _NavigationBarStaticComponents {
trailingKey: keys.trailingKey,
userTrailing: userTrailing,
padding: padding,
actionsForegroundColor: actionsForegroundColor,
),
largeTitle = createLargeTitle(
largeTitleKey: keys.largeTitleKey,
@ -978,8 +1000,6 @@ class _NavigationBarStaticComponents {
return null;
}
final Color actionsForegroundColor;
final KeyedSubtree leading;
static KeyedSubtree createLeading({
@required GlobalKey leadingKey,
@ -987,7 +1007,6 @@ class _NavigationBarStaticComponents {
@required ModalRoute<dynamic> route,
@required bool automaticallyImplyLeading,
@required EdgeInsetsDirectional padding,
@required Color actionsForegroundColor
}) {
Widget leadingContent;
@ -1016,15 +1035,11 @@ class _NavigationBarStaticComponents {
padding: EdgeInsetsDirectional.only(
start: padding?.start ?? _kNavBarEdgePadding,
),
child: DefaultTextStyle(
style: _navBarItemStyle(actionsForegroundColor),
child: IconTheme.merge(
data: IconThemeData(
color: actionsForegroundColor,
size: 32.0,
),
child: leadingContent,
child: IconTheme.merge(
data: const IconThemeData(
size: 32.0,
),
child: leadingContent,
),
),
);
@ -1116,7 +1131,6 @@ class _NavigationBarStaticComponents {
@required GlobalKey trailingKey,
@required Widget userTrailing,
@required EdgeInsetsDirectional padding,
@required Color actionsForegroundColor,
}) {
if (userTrailing == null) {
return null;
@ -1128,15 +1142,11 @@ class _NavigationBarStaticComponents {
padding: EdgeInsetsDirectional.only(
end: padding?.end ?? _kNavBarEdgePadding,
),
child: DefaultTextStyle(
style: _navBarItemStyle(actionsForegroundColor),
child: IconTheme.merge(
data: IconThemeData(
color: actionsForegroundColor,
size: 32.0,
),
child: userTrailing,
child: IconTheme.merge(
data: const IconThemeData(
size: 32.0,
),
child: userTrailing,
),
),
);
@ -1188,24 +1198,24 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
///
/// The [color] parameter must not be null.
const CupertinoNavigationBarBackButton({
@required this.color,
this.color,
this.previousPageTitle,
}) : _backChevron = null,
_backLabel = null,
assert(color != null);
_backLabel = null;
// Allow the back chevron and label to be separately created (and keyed)
// because they animate separately during page transitions.
const CupertinoNavigationBarBackButton._assemble(
this._backChevron,
this._backLabel,
this.color,
) : previousPageTitle = null,
assert(color != null);
color = null;
/// The [Color] of the back button.
///
/// Must not be null.
/// Can be used to override the color of the back button chevron and label.
///
/// Defaults to [CupertinoTheme]'s `primaryColor` if null.
final Color color;
/// An override for showing the previous route's title. If null, it will be
@ -1225,16 +1235,21 @@ class CupertinoNavigationBarBackButton extends StatelessWidget {
'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
);
TextStyle actionTextStyle = CupertinoTheme.of(context).textTheme.navActionTextStyle;
if (color != null) {
actionTextStyle = actionTextStyle.copyWith(color: color);
}
return CupertinoButton(
child: Semantics(
container: true,
excludeSemantics: true,
label: 'Back',
button: true,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
child: DefaultTextStyle(
style: _navBarItemStyle(color),
child: DefaultTextStyle(
style: actionTextStyle,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
@ -1367,18 +1382,23 @@ class _TransitionableNavigationBar extends StatelessWidget {
_TransitionableNavigationBar({
@required this.componentsKeys,
@required this.backgroundColor,
@required this.actionsForegroundColor,
@required this.backButtonTextStyle,
@required this.titleTextStyle,
@required this.largeTitleTextStyle,
@required this.border,
@required this.hasUserMiddle,
@required this.largeExpanded,
@required this.child,
}) : assert(componentsKeys != null),
assert(largeExpanded != null),
assert(!largeExpanded || largeTitleTextStyle != null),
super(key: componentsKeys.navBarBoxKey);
final _NavigationBarStaticComponentsKeys componentsKeys;
final Color backgroundColor;
final Color actionsForegroundColor;
final TextStyle backButtonTextStyle;
final TextStyle titleTextStyle;
final TextStyle largeTitleTextStyle;
final Border border;
final bool hasUserMiddle;
final bool largeExpanded;
@ -1558,8 +1578,12 @@ class _NavigationBarComponentsTransition {
topComponents = topNavBar.componentsKeys,
bottomNavBarBox = bottomNavBar.renderBox,
topNavBarBox = topNavBar.renderBox,
bottomActionsStyle = _navBarItemStyle(bottomNavBar.actionsForegroundColor),
topActionsStyle = _navBarItemStyle(topNavBar.actionsForegroundColor),
bottomBackButtonTextStyle = bottomNavBar.backButtonTextStyle,
topBackButtonTextStyle = topNavBar.backButtonTextStyle,
bottomTitleTextStyle = bottomNavBar.titleTextStyle,
topTitleTextStyle = topNavBar.titleTextStyle,
bottomLargeTitleTextStyle = bottomNavBar.largeTitleTextStyle,
topLargeTitleTextStyle = topNavBar.largeTitleTextStyle,
bottomHasUserMiddle = bottomNavBar.hasUserMiddle,
topHasUserMiddle = topNavBar.hasUserMiddle,
bottomLargeExpanded = bottomNavBar.largeExpanded,
@ -1588,8 +1612,13 @@ class _NavigationBarComponentsTransition {
final RenderBox bottomNavBarBox;
final RenderBox topNavBarBox;
final TextStyle bottomActionsStyle;
final TextStyle topActionsStyle;
final TextStyle bottomBackButtonTextStyle;
final TextStyle topBackButtonTextStyle;
final TextStyle bottomTitleTextStyle;
final TextStyle topTitleTextStyle;
final TextStyle bottomLargeTitleTextStyle;
final TextStyle topLargeTitleTextStyle;
final bool bottomHasUserMiddle;
final bool topHasUserMiddle;
final bool bottomLargeExpanded;
@ -1702,7 +1731,7 @@ class _NavigationBarComponentsTransition {
child: FadeTransition(
opacity: fadeOutBy(0.6),
child: DefaultTextStyle(
style: bottomActionsStyle,
style: bottomBackButtonTextStyle,
child: bottomBackChevron.child,
),
),
@ -1734,7 +1763,7 @@ class _NavigationBarComponentsTransition {
child: FadeTransition(
opacity: fadeOutBy(0.2),
child: DefaultTextStyle(
style: bottomActionsStyle,
style: bottomBackButtonTextStyle,
child: bottomBackLabel.child,
),
),
@ -1770,8 +1799,8 @@ class _NavigationBarComponentsTransition {
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyleTransition(
style: animation.drive(TextStyleTween(
begin: _kMiddleTitleTextStyle,
end: topActionsStyle,
begin: bottomTitleTextStyle,
end: topBackButtonTextStyle,
)),
child: bottomMiddle.child,
),
@ -1790,7 +1819,7 @@ class _NavigationBarComponentsTransition {
opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7),
// Keep the font when transitioning into a non-back label leading.
child: DefaultTextStyle(
style: _kMiddleTitleTextStyle,
style: bottomTitleTextStyle,
child: bottomMiddle.child,
),
),
@ -1826,8 +1855,8 @@ class _NavigationBarComponentsTransition {
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyleTransition(
style: animation.drive(TextStyleTween(
begin: _kLargeTitleTextStyle,
end: topActionsStyle,
begin: bottomLargeTitleTextStyle,
end: topBackButtonTextStyle,
)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -1861,7 +1890,7 @@ class _NavigationBarComponentsTransition {
opacity: fadeOutBy(0.4),
// Keep the font when transitioning into a non-back-label leading.
child: DefaultTextStyle(
style: _kLargeTitleTextStyle,
style: bottomLargeTitleTextStyle,
child: bottomLargeTitle.child,
),
),
@ -1936,7 +1965,7 @@ class _NavigationBarComponentsTransition {
child: FadeTransition(
opacity: fadeInFrom(bottomBackChevron == null ? 0.7 : 0.4),
child: DefaultTextStyle(
style: topActionsStyle,
style: topBackButtonTextStyle,
child: topBackChevron.child,
),
),
@ -1985,8 +2014,8 @@ class _NavigationBarComponentsTransition {
opacity: midClickOpacity ?? fadeInFrom(0.4),
child: DefaultTextStyleTransition(
style: animation.drive(TextStyleTween(
begin: _kLargeTitleTextStyle,
end: topActionsStyle,
begin: bottomLargeTitleTextStyle,
end: topBackButtonTextStyle,
)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -2010,8 +2039,8 @@ class _NavigationBarComponentsTransition {
opacity: midClickOpacity ?? fadeInFrom(0.3),
child: DefaultTextStyleTransition(
style: animation.drive(TextStyleTween(
begin: _kMiddleTitleTextStyle,
end: topActionsStyle,
begin: bottomTitleTextStyle,
end: topBackButtonTextStyle,
)),
child: topBackLabel.child,
),
@ -2053,7 +2082,7 @@ class _NavigationBarComponentsTransition {
child: FadeTransition(
opacity: fadeInFrom(0.25),
child: DefaultTextStyle(
style: _kMiddleTitleTextStyle,
style: topTitleTextStyle,
child: topMiddle.child,
),
),
@ -2101,7 +2130,7 @@ class _NavigationBarComponentsTransition {
child: FadeTransition(
opacity: fadeInFrom(0.3),
child: DefaultTextStyle(
style: _kLargeTitleTextStyle,
style: topLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: topLargeTitle.child,

View file

@ -4,7 +4,7 @@
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
/// Implements a single iOS application page's layout.
///
@ -21,7 +21,7 @@ class CupertinoPageScaffold extends StatelessWidget {
const CupertinoPageScaffold({
Key key,
this.navigationBar,
this.backgroundColor = CupertinoColors.white,
this.backgroundColor,
this.resizeToAvoidBottomInset = true,
@required this.child,
}) : assert(child != null),
@ -48,7 +48,7 @@ class CupertinoPageScaffold extends StatelessWidget {
/// The color of the widget that underlies the entire scaffold.
///
/// By default uses [CupertinoColors.white] color.
/// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null.
final Color backgroundColor;
/// Whether the [child] should size itself to avoid the window's bottom inset.
@ -78,10 +78,13 @@ class CupertinoPageScaffold extends StatelessWidget {
? existingMediaQuery.viewInsets.bottom
: 0.0;
final bool fullObstruction =
navigationBar.fullObstruction ?? CupertinoTheme.of(context).barBackgroundColor.alpha == 0xFF;
// If navigation bar is opaquely obstructing, directly shift the main content
// down. If translucent, let main content draw behind navigation bar but hint the
// obstructed area.
if (navigationBar.fullObstruction) {
if (fullObstruction) {
paddedContent = Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: child,
@ -114,7 +117,9 @@ class CupertinoPageScaffold extends StatelessWidget {
}
return DecoratedBox(
decoration: BoxDecoration(color: backgroundColor),
decoration: BoxDecoration(
color: backgroundColor ?? CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: stacked,
),

View file

@ -9,7 +9,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
// Minimum padding from horizontal edges of segmented control to edges of
// encompassing widget.
@ -56,9 +56,8 @@ const Duration _kFadeDuration = Duration(milliseconds: 165);
///
/// A segmented control may optionally be created with custom colors. The
/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
/// arguments can be used to change the segmented control's colors from
/// [CupertinoColors.activeBlue] and [CupertinoColors.white] to a custom
/// configuration.
/// arguments can be used to override the segmented control's colors from
/// [CupertinoTheme] defaults.
///
/// See also:
///
@ -66,8 +65,7 @@ const Duration _kFadeDuration = Duration(milliseconds: 165);
class CupertinoSegmentedControl<T> extends StatefulWidget {
/// Creates an iOS-style segmented control bar.
///
/// The [children], [onValueChanged], [unselectedColor], [selectedColor],
/// [borderColor], and [pressedColor] arguments must not be null. The
/// The [children] and [onValueChanged] arguments must not be null. The
/// [children] argument must be an ordered [Map] such as a [LinkedHashMap].
/// Further, the length of the [children] list must be greater than one.
///
@ -85,19 +83,17 @@ class CupertinoSegmentedControl<T> extends StatefulWidget {
@required this.children,
@required this.onValueChanged,
this.groupValue,
this.unselectedColor = CupertinoColors.white,
this.selectedColor = CupertinoColors.activeBlue,
this.borderColor = CupertinoColors.activeBlue,
this.pressedColor = const Color(0x33007AFF),
this.unselectedColor,
this.selectedColor,
this.borderColor,
this.pressedColor,
}) : assert(children != null),
assert(children.length >= 2),
assert(onValueChanged != null),
assert(groupValue == null || children.keys.any((T child) => child == groupValue),
'The groupValue must be either null or one of the keys in the children map.'),
assert(unselectedColor != null),
assert(selectedColor != null),
assert(borderColor != null),
assert(pressedColor != null),
assert(
groupValue == null || children.keys.any((T child) => child == groupValue),
'The groupValue must be either null or one of the keys in the children map.',
),
super(key: key);
/// The identifying keys and corresponding widget values in the
@ -163,36 +159,24 @@ class CupertinoSegmentedControl<T> extends StatefulWidget {
/// The color used to fill the backgrounds of unselected widgets and as the
/// text color of the selected widget.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.white].
/// Defaults to [CupertinoTheme]'s `primaryContrastingColor` if null.
final Color unselectedColor;
/// The color used to fill the background of the selected widget and as the text
/// color of unselected widgets.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.activeBlue].
/// Defaults to [CupertinoTheme]'s `primaryColor` if null.
final Color selectedColor;
/// The color used as the border around each widget.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// [CupertinoColors.activeBlue].
/// Defaults to [CupertinoTheme]'s `primaryColor` if null.
final Color borderColor;
/// The color used to fill the background of the widget the user is
/// temporarily interacting with through a long press or drag.
///
/// This attribute must not be null.
///
/// If this attribute is unspecified, this color will default to
/// `Color(0x33007AFF)`, a light, partially-transparent blue color.
/// Defaults to the selectedColor at 20% opacity if null.
final Color pressedColor;
@override
@ -210,21 +194,68 @@ class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
ColorTween _reverseBackgroundColorTween;
ColorTween _textColorTween;
@override
void initState() {
super.initState();
Color _selectedColor;
Color _unselectedColor;
Color _borderColor;
Color _pressedColor;
AnimationController createAnimationController() {
return AnimationController(
duration: _kFadeDuration,
vsync: this,
)..addListener(() {
setState(() {
// State of background/text colors has changed
});
});
}
bool _updateColors() {
assert(mounted, 'This should only be called after didUpdateDependencies');
bool changed = false;
final Color selectedColor = widget.selectedColor ?? CupertinoTheme.of(context).primaryColor;
if (_selectedColor != selectedColor) {
changed = true;
_selectedColor = selectedColor;
}
final Color unselectedColor = widget.unselectedColor ?? CupertinoTheme.of(context).primaryContrastingColor;
if (_unselectedColor != unselectedColor) {
changed = true;
_unselectedColor = unselectedColor;
}
final Color borderColor = widget.borderColor ?? CupertinoTheme.of(context).primaryColor;
if (_borderColor != borderColor) {
changed = true;
_borderColor = borderColor;
}
final Color pressedColor = widget.pressedColor ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
if (_pressedColor != pressedColor) {
changed = true;
_pressedColor = pressedColor;
}
_forwardBackgroundColorTween = ColorTween(
begin: widget.pressedColor,
end: widget.selectedColor,
begin: _pressedColor,
end: _selectedColor,
);
_reverseBackgroundColorTween = ColorTween(
begin: widget.unselectedColor,
end: widget.selectedColor,
begin: _unselectedColor,
end: _selectedColor,
);
_textColorTween = ColorTween(
begin: widget.selectedColor,
end: widget.unselectedColor,
begin: _selectedColor,
end: _unselectedColor,
);
return changed;
}
void _updateAnimationControllers() {
assert(mounted, 'This should only be called after didUpdateDependencies');
for (AnimationController controller in _selectionControllers) {
controller.dispose();
}
_selectionControllers.clear();
_childTweens.clear();
for (T key in widget.children.keys) {
final AnimationController animationController = createAnimationController();
@ -238,15 +269,36 @@ class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
}
}
AnimationController createAnimationController() {
return AnimationController(
duration: _kFadeDuration,
vsync: this,
)..addListener(() {
setState(() {
// State of background/text colors has changed
});
});
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_updateColors()) {
_updateAnimationControllers();
}
}
@override
void didUpdateWidget(CupertinoSegmentedControl<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (_updateColors() || oldWidget.children.length != widget.children.length) {
_updateAnimationControllers();
}
if (oldWidget.groupValue != widget.groupValue) {
int index = 0;
for (T key in widget.children.keys) {
if (widget.groupValue == key) {
_childTweens[index] = _forwardBackgroundColorTween;
_selectionControllers[index].forward();
} else {
_childTweens[index] = _reverseBackgroundColorTween;
_selectionControllers[index].reverse();
}
index += 1;
}
}
}
@override
@ -257,6 +309,7 @@ class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
super.dispose();
}
void _onTapDown(T currentKey) {
if (_pressedKey == null && currentKey != widget.groupValue) {
setState(() {
@ -282,53 +335,18 @@ class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
if (_selectionControllers[index].isAnimating)
return _textColorTween.evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey)
return widget.unselectedColor;
return widget.selectedColor;
return _unselectedColor;
return _selectedColor;
}
Color getBackgroundColor(int index, T currentKey) {
if (_selectionControllers[index].isAnimating)
return _childTweens[index].evaluate(_selectionControllers[index]);
if (widget.groupValue == currentKey)
return widget.selectedColor;
return _selectedColor;
if (_pressedKey == currentKey)
return widget.pressedColor;
return widget.unselectedColor;
}
void updateAnimationControllers() {
if (_selectionControllers.length > widget.children.length) {
_selectionControllers.length = widget.children.length;
_childTweens.length = widget.children.length;
} else {
for (int index = _selectionControllers.length; index < widget.children.length; index += 1) {
_selectionControllers.add(createAnimationController());
_childTweens.add(_reverseBackgroundColorTween);
}
}
}
@override
void didUpdateWidget(CupertinoSegmentedControl<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.children.length != widget.children.length) {
updateAnimationControllers();
}
if (oldWidget.groupValue != widget.groupValue) {
int index = 0;
for (T key in widget.children.keys) {
if (widget.groupValue == key) {
_childTweens[index] = _forwardBackgroundColorTween;
_selectionControllers[index].forward();
} else {
_childTweens[index] = _reverseBackgroundColorTween;
_selectionControllers[index].reverse();
}
index += 1;
}
}
return _pressedColor;
return _unselectedColor;
}
@override
@ -385,7 +403,7 @@ class _SegmentedControlState<T> extends State<CupertinoSegmentedControl<T>>
selectedIndex: selectedIndex,
pressedIndex: pressedIndex,
backgroundColors: _backgroundColors,
borderColor: widget.borderColor,
borderColor: _borderColor,
);
return Padding(

View file

@ -9,7 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
import 'thumb_painter.dart';
// Examples can assume:
@ -189,7 +189,7 @@ class CupertinoSlider extends StatefulWidget {
/// The color to use for the portion of the slider that has been selected.
///
/// Defaults to [CupertinoColors.activeBlue].
/// Defaults to the [CupertinoTheme]'s primary color if null.
final Color activeColor;
@override
@ -228,7 +228,7 @@ class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderSt
return _CupertinoSliderRenderObjectWidget(
value: (widget.value - widget.min) / (widget.max - widget.min),
divisions: widget.divisions,
activeColor: widget.activeColor ?? CupertinoColors.activeBlue,
activeColor: widget.activeColor ?? CupertinoTheme.of(context).primaryColor,
onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
@ -485,15 +485,14 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
final double trackActive = offset.dx + _thumbCenter;
final Canvas canvas = context.canvas;
final Paint paint = Paint();
if (visualPosition > 0.0) {
paint.color = rightColor;
final Paint paint = Paint()..color = rightColor;
canvas.drawRRect(RRect.fromLTRBXY(trackLeft, trackTop, trackActive, trackBottom, 1.0, 1.0), paint);
}
if (visualPosition < 1.0) {
paint.color = leftColor;
final Paint paint = Paint()..color = leftColor;
canvas.drawRRect(RRect.fromLTRBXY(trackActive, trackTop, trackRight, trackBottom, 1.0, 1.0), paint);
}

View file

@ -88,7 +88,8 @@ class CupertinoSwitch extends StatefulWidget {
/// The color to use when this switch is on.
///
/// Defaults to [CupertinoColors.activeGreen].
/// Defaults to [CupertinoColors.activeGreen] when null and ignores the
/// [CupertinoTheme] in accordance to native iOS behavior.
final Color activeColor;
@override

View file

@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart';
import 'bottom_tab_bar.dart';
import 'theme.dart';
/// Implements a tabbed iOS application's root layout and behavior structure.
///
@ -173,13 +174,13 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
// TODO(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912
final double bottomPadding = widget.tabBar.preferredSize.height
+ existingMediaQuery.padding.bottom;
final double bottomPadding =
widget.tabBar.preferredSize.height + existingMediaQuery.padding.bottom;
// If tab bar opaque, directly stop the main content higher. If
// translucent, let main content draw behind the tab bar but hint the
// obstructed area.
if (widget.tabBar.opaque) {
if (widget.tabBar.opaque(context)) {
content = Padding(
padding: EdgeInsets.only(bottom: bottomPadding),
child: content,
@ -219,8 +220,13 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
));
}
return Stack(
children: stacked,
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoTheme.of(context).scaffoldBackgroundColor
),
child: Stack(
children: stacked,
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'icons.dart';
import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
@ -32,15 +33,6 @@ const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
);
// Default iOS style from HIG specs with larger font.
const TextStyle _kDefaultTextStyle = TextStyle(
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.38,
color: CupertinoColors.black,
decoration: TextDecoration.none,
);
// Value extracted via color reader from iOS simulator.
const Color _kSelectionHighlightColor = Color(0x667FAACF);
const Color _kInactiveTextColor = Color(0xFFC2C2C2);
@ -160,7 +152,7 @@ class CupertinoTextField extends StatefulWidget {
TextInputType keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.style = _kDefaultTextStyle,
this.style,
this.textAlign = TextAlign.start,
this.autofocus = false,
this.obscureText = false,
@ -271,7 +263,7 @@ class CupertinoTextField extends StatefulWidget {
///
/// Also serves as a base for the [placeholder] text's style.
///
/// Defaults to a standard iOS style and cannot be null.
/// Defaults to the standard iOS font style from [CupertinoTheme] if null.
final TextStyle style;
/// {@macro flutter.widgets.editableText.textAlign}
@ -558,7 +550,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
);
}
Widget _addTextDependentAttachments(Widget editableText) {
Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle) {
assert(editableText != null);
assert(textStyle != null);
// If there are no surrounding widgets, just return the core editable text
// part.
if (widget.placeholder == null &&
@ -593,7 +587,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
widget.placeholder,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: widget.style.merge(
style: textStyle.merge(
const TextStyle(
color: _kInactiveTextColor,
fontWeight: FontWeight.w300,
@ -637,14 +631,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
assert(debugCheckHasDirectionality(context));
final Brightness keyboardAppearance = widget.keyboardAppearance;
final TextEditingController controller = _effectiveController;
final FocusNode focusNode = _effectiveFocusNode;
final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
final bool enabled = widget.enabled ?? true;
if (widget.maxLength != null && widget.maxLengthEnforced) {
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
}
final TextStyle textStyle = widget.style ?? CupertinoTheme.of(context).textTheme.textStyle;
final Widget paddedEditable = Padding(
padding: widget.padding,
@ -652,11 +645,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
child: EditableText(
key: _editableTextKey,
controller: controller,
focusNode: focusNode,
focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: widget.style,
style: textStyle,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
@ -674,7 +667,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
cursorColor: widget.cursorColor,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
keyboardAppearance: widget.keyboardAppearance,
),
),
);
@ -692,14 +685,18 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
decoration: widget.decoration,
// The main decoration and the disabled scrim exists separately.
child: Container(
color: enabled ? null : _kDisabledBackground,
color: enabled
? null
: CupertinoTheme.of(context).brightness == Brightness.light
? _kDisabledBackground
: CupertinoColors.darkBackgroundGray,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onLongPress: _handleLongPress,
excludeFromSemantics: true,
child: _addTextDependentAttachments(paddedEditable),
child: _addTextDependentAttachments(paddedEditable, textStyle),
),
),
),

View file

@ -0,0 +1,181 @@
// Copyright 2018 The Chromium 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/foundation.dart';
import 'package:flutter/services.dart' show Brightness;
import 'package:flutter/widgets.dart';
import 'colors.dart';
// Values derived from https://developer.apple.com/design/resources/.
const TextStyle _kDefaultLightTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.41,
color: CupertinoColors.black,
decoration: TextDecoration.none,
);
// Values derived from https://developer.apple.com/design/resources/.
const TextStyle _kDefaultDarkTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.41,
color: CupertinoColors.white,
decoration: TextDecoration.none,
);
// Values derived from https://developer.apple.com/design/resources/.
const TextStyle _kDefaultActionTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 17.0,
letterSpacing: -0.41,
color: CupertinoColors.activeBlue,
decoration: TextDecoration.none,
);
// Values derived from https://developer.apple.com/design/resources/.
const TextStyle _kDefaultTabLabelTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 10.0,
letterSpacing: -0.24,
color: CupertinoColors.inactiveGray,
);
const TextStyle _kDefaultMiddleTitleLightTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 17.0,
fontWeight: FontWeight.w600,
letterSpacing: -0.41,
color: CupertinoColors.black,
);
const TextStyle _kDefaultMiddleTitleDarkTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Text',
fontSize: 17.0,
fontWeight: FontWeight.w600,
letterSpacing: -0.41,
color: CupertinoColors.white,
);
const TextStyle _kDefaultLargeTitleLightTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 34.0,
fontWeight: FontWeight.w700,
letterSpacing: 0.41,
color: CupertinoColors.black,
);
const TextStyle _kDefaultLargeTitleDarkTextStyle = TextStyle(
inherit: false,
fontFamily: '.SF Pro Display',
fontSize: 34.0,
fontWeight: FontWeight.w700,
letterSpacing: 0.41,
color: CupertinoColors.white,
);
/// Cupertino typography theme in a [CupertinoThemeData].
@immutable
class CupertinoTextThemeData extends Diagnosticable {
/// Create a [CupertinoTextThemeData].
///
/// The [primaryColor] and [isLight] parameters are used to derive TextStyle
/// defaults of other attributes such as [textStyle] and [actionTextStyle]
/// etc. The default value of [primaryColor] is [CupertinoColors.activeBlue]
/// and the default value of [isLight] is true.
///
/// Other [TextStyle] parameters default to default iOS text styles when
/// unspecified.
const CupertinoTextThemeData({
Color primaryColor,
Brightness brightness,
TextStyle textStyle,
TextStyle actionTextStyle,
TextStyle tabLabelTextStyle,
TextStyle navTitleTextStyle,
TextStyle navLargeTitleTextStyle,
TextStyle navActionTextStyle,
}) : _primaryColor = primaryColor ?? CupertinoColors.activeBlue,
_brightness = brightness,
_textStyle = textStyle,
_actionTextStyle = actionTextStyle,
_tabLabelTextStyle = tabLabelTextStyle,
_navTitleTextStyle = navTitleTextStyle,
_navLargeTitleTextStyle = navLargeTitleTextStyle,
_navActionTextStyle = navActionTextStyle;
final Color _primaryColor;
final Brightness _brightness;
bool get _isLight => _brightness != Brightness.dark;
final TextStyle _textStyle;
/// Typography of general text content for Cupertino widgets.
TextStyle get textStyle => _textStyle ?? (_isLight ? _kDefaultLightTextStyle : _kDefaultDarkTextStyle);
final TextStyle _actionTextStyle;
/// Typography of interactive text content such as text in a button without background.
TextStyle get actionTextStyle {
return _actionTextStyle ?? _kDefaultActionTextStyle.copyWith(
color: _primaryColor,
);
}
final TextStyle _tabLabelTextStyle;
/// Typography of unselected tabs.
TextStyle get tabLabelTextStyle => _tabLabelTextStyle ?? _kDefaultTabLabelTextStyle;
final TextStyle _navTitleTextStyle;
/// Typography of titles in standard navigation bars.
TextStyle get navTitleTextStyle {
return _navTitleTextStyle ??
(_isLight ? _kDefaultMiddleTitleLightTextStyle : _kDefaultMiddleTitleDarkTextStyle);
}
final TextStyle _navLargeTitleTextStyle;
/// Typography of large titles in sliver navigation bars.
TextStyle get navLargeTitleTextStyle {
return _navLargeTitleTextStyle ??
(_isLight ? _kDefaultLargeTitleLightTextStyle : _kDefaultLargeTitleDarkTextStyle);
}
final TextStyle _navActionTextStyle;
/// Typography of interative text content in navigation bars.
TextStyle get navActionTextStyle {
return _navActionTextStyle ?? _kDefaultActionTextStyle.copyWith(
color: _primaryColor,
);
}
/// Returns a copy of the current [CupertinoTextThemeData] instance with
/// specified overrides.
CupertinoTextThemeData copyWith({
Color primaryColor,
Brightness brightness,
TextStyle textStyle,
TextStyle actionTextStyle,
TextStyle tabLabelTextStyle,
TextStyle navTitleTextStyle,
TextStyle navLargeTitleTextStyle,
TextStyle navActionTextStyle,
}) {
return CupertinoTextThemeData(
primaryColor: primaryColor ?? _primaryColor,
brightness: brightness ?? _brightness,
textStyle: textStyle ?? _textStyle,
actionTextStyle: actionTextStyle ?? _actionTextStyle,
tabLabelTextStyle: tabLabelTextStyle ?? _tabLabelTextStyle,
navTitleTextStyle: navTitleTextStyle ?? _navTitleTextStyle,
navLargeTitleTextStyle: navLargeTitleTextStyle ?? _navLargeTitleTextStyle,
navActionTextStyle: navActionTextStyle ?? _navActionTextStyle,
);
}
}

View file

@ -0,0 +1,263 @@
// Copyright 2018 The Chromium 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/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'text_theme.dart';
export 'package:flutter/services.dart' show Brightness;
// Values derived from https://developer.apple.com/design/resources/.
const Color _kDefaultBarLightBackgroundColor = Color(0xCCF8F8F8);
const Color _kDefaultBarDarkBackgroundColor = Color(0xB7212121);
/// Applies a visual styling theme to descendant Cupertino widgets.
///
/// Affects the color and text styles of Cupertino widgets whose styling
/// are not overridden when constructing the respective widgets instances.
///
/// Descendant widgets can retrieve the current [CupertinoThemeData] by calling
/// [CupertinoTheme.of]. An [InheritedWidget] dependency is created when
/// an ancestor [CupertinoThemeData] is retrieved via [CupertinoTheme.of].
///
/// See also:
///
/// * [CupertinoThemeData], specifies the theme's visual styling.
/// * [CupertinoApp], which will automatically add a [CupertinoTheme].
/// * [Theme], a Material theme which will automatically add a [CupertinoTheme]
/// with a [CupertinoThemeData] derived from the Material [ThemeData].
class CupertinoTheme extends InheritedWidget {
/// Creates a [CupertinoTheme] to change descendant Cupertino widgets' styling.
///
/// The [data] and [child] parameters must not be null.
const CupertinoTheme({
Key key,
@required this.data,
@required Widget child,
}) : assert(child != null),
assert(data != null),
super(key: key, child: child);
/// The [CupertinoThemeData] styling for this theme.
final CupertinoThemeData data;
@override
bool updateShouldNotify(CupertinoTheme oldWidget) => data != oldWidget.data;
/// Retrieve the [CupertinoThemeData] from an ancestor [CupertinoTheme] widget.
///
/// Returns a default [CupertinoThemeData] if no [CupertinoTheme] widgets
/// exist in the ancestry tree.
static CupertinoThemeData of(BuildContext context) {
final CupertinoTheme theme = context.inheritFromWidgetOfExactType(CupertinoTheme);
return theme?.data ?? const CupertinoThemeData();
}
}
/// Styling specifications for a [CupertinoTheme].
///
/// All constructor parameters can be null, in which case a
/// [CupertinoColors.activeBlue] based default iOS theme styling is used.
///
/// Parameters can also be partially specified, in which case some parameters
/// will cascade down to other dependent parameters to create a cohesive
/// visual effect. For instance, if a [primaryColor] is specified, it would
/// cascade down to affect some fonts in [textTheme] if [textTheme] is not
/// specified.
///
/// See also:
///
/// * [CupertinoTheme], in which this [CupertinoThemeData] is inserted.
/// * [ThemeData], a Material equivalent that also configures Cupertino
/// styling via a [CupertinoThemeData] subclass [MaterialBasedCupertinoThemeData].
@immutable
class CupertinoThemeData extends Diagnosticable {
/// Create a [CupertinoTheme] styling specification.
///
/// Unspecified parameters default to a reasonable iOS default style.
const CupertinoThemeData({
Brightness brightness,
Color primaryColor,
Color primaryContrastingColor,
CupertinoTextThemeData textTheme,
Color barBackgroundColor,
Color scaffoldBackgroundColor,
}) : this.raw(
brightness,
primaryColor,
primaryContrastingColor,
textTheme,
barBackgroundColor,
scaffoldBackgroundColor,
);
/// Same as the default constructor but with positional arguments to avoid
/// forgetting any and to specify all arguments.
///
/// Used by subclasses to get the superclass's defaulting behaviors.
@protected
const CupertinoThemeData.raw(
this._brightness,
this._primaryColor,
this._primaryContrastingColor,
this._textTheme,
this._barBackgroundColor,
this._scaffoldBackgroundColor,
);
bool get _isLight => brightness == Brightness.light;
/// The general brightness theme of the [CupertinoThemeData].
///
/// Affects all other theme properties when unspecified. Defaults to
/// [Brightness.light].
///
/// If coming from a Material [Theme] and unspecified, [brightness] will be
/// derived from the Material [ThemeData]'s `brightness`.
Brightness get brightness => _brightness ?? Brightness.light;
final Brightness _brightness;
/// A color used on interactive elements of the theme.
///
/// This color is generally used on text and icons in buttons and tappable
/// elements. Defaults to [CupertinoColors.activeBlue] or
/// [CupertinoColors.activeOrange] when [brightness] is light or dark.
///
/// If coming from a Material [Theme] and unspecified, [primaryColor] will be
/// derived from the Material [ThemeData]'s `colorScheme.primary`. However, in
/// iOS styling, the [primaryColor] is more sparsely used than in Material
/// Design where the [primaryColor] can appear on non-interactive surfaces like
/// the [AppBar] background, [TextField] borders etc.
Color get primaryColor {
return _primaryColor ??
(_isLight ? CupertinoColors.activeBlue : CupertinoColors.activeOrange);
}
final Color _primaryColor;
/// A color used for content that must contrast against a [primaryColor] background.
///
/// For example, this color is used for a [CupertinoButton]'s text and icons
/// when the button's background is [primaryColor].
///
/// If coming from a Material [Theme] and unspecified, [primaryContrastingColor]
/// will be derived from the Material [ThemeData]'s `colorScheme.onPrimary`.
Color get primaryContrastingColor {
return _primaryContrastingColor ??
(_isLight ? CupertinoColors.white : CupertinoColors.black);
}
final Color _primaryContrastingColor;
/// Text styles used by Cupertino widgets.
///
/// Derived from [brightness] and [primaryColor] if unspecified, including
/// [brightness] and [primaryColor] of a Material [ThemeData] if coming
/// from a Material [Theme].
CupertinoTextThemeData get textTheme {
return _textTheme ?? CupertinoTextThemeData(
brightness: brightness,
primaryColor: primaryColor,
);
}
final CupertinoTextThemeData _textTheme;
/// Background color of the top nav bar and bottom tab bar.
///
/// Defaults to a light gray or a dark gray translucent color depending
/// on the [brightness].
Color get barBackgroundColor {
return _barBackgroundColor ??
(_isLight ? _kDefaultBarLightBackgroundColor : _kDefaultBarDarkBackgroundColor);
}
final Color _barBackgroundColor;
/// Background color of the scaffold.
///
/// Defaults to white or black depending on the [brightness].
Color get scaffoldBackgroundColor {
return _scaffoldBackgroundColor ??
(_isLight ? CupertinoColors.white : CupertinoColors.black);
}
final Color _scaffoldBackgroundColor;
/// Return an instance of the [CupertinoThemeData] whose property getters
/// only return the construction time specifications with no derived values.
///
/// Used in Material themes to let unspecified properties fallback to Material
/// theme properties instead of iOS defaults.
CupertinoThemeData noDefault() {
return _NoDefaultCupertinoThemeData(
_brightness,
_primaryColor,
_primaryContrastingColor,
_textTheme,
_barBackgroundColor,
_scaffoldBackgroundColor,
);
}
/// Create a copy of [CupertinoThemeData] with specified attributes overridden.
///
/// Only the current instance's specified attributes are copied instead of
/// derived values. For instance, if the current [primaryColor] is implied
/// to be [CupertinoColors.activeOrange] due to the current [brightness],
/// copying with a different [brightness] will also change the copy's
/// implied [primaryColor].
CupertinoThemeData copyWith({
Brightness brightness,
Color primaryColor,
Color primaryContrastingColor,
CupertinoTextThemeData textTheme,
Color barBackgroundColor,
Color scaffoldBackgroundColor,
}) {
return CupertinoThemeData(
brightness: brightness ?? _brightness,
primaryColor: primaryColor ?? _primaryColor,
primaryContrastingColor: primaryContrastingColor ?? _primaryContrastingColor,
textTheme: textTheme ?? _textTheme,
barBackgroundColor: barBackgroundColor ?? _barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor ?? _scaffoldBackgroundColor,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
const CupertinoThemeData defaultData = CupertinoThemeData();
properties.add(EnumProperty<Brightness>('brightness', brightness, defaultValue: defaultData.brightness));
properties.add(DiagnosticsProperty<Color>('primaryColor', primaryColor, defaultValue: defaultData.primaryColor));
properties.add(DiagnosticsProperty<Color>('primaryContrastingColor', primaryContrastingColor, defaultValue: defaultData.primaryContrastingColor));
properties.add(DiagnosticsProperty<CupertinoTextThemeData>('textTheme', textTheme, defaultValue: defaultData.textTheme));
properties.add(DiagnosticsProperty<Color>('barBackgroundColor', barBackgroundColor, defaultValue: defaultData.barBackgroundColor));
properties.add(DiagnosticsProperty<Color>('scaffoldBackgroundColor', scaffoldBackgroundColor, defaultValue: defaultData.scaffoldBackgroundColor));
}
}
class _NoDefaultCupertinoThemeData extends CupertinoThemeData {
const _NoDefaultCupertinoThemeData(
this.brightness,
this.primaryColor,
this.primaryContrastingColor,
this.textTheme,
this.barBackgroundColor,
this.scaffoldBackgroundColor,
) : super.raw(
brightness,
primaryColor,
primaryContrastingColor,
textTheme,
barBackgroundColor,
scaffoldBackgroundColor,
);
@override final Brightness brightness;
@override final Color primaryColor;
@override final Color primaryContrastingColor;
@override final CupertinoTextThemeData textTheme;
@override final Color barBackgroundColor;
@override final Color scaffoldBackgroundColor;
}

View file

@ -443,7 +443,7 @@ class _MaterialAppState extends State<MaterialApp> {
mini: true,
);
},
)
),
);
assert(() {

View file

@ -2,6 +2,7 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
@ -143,7 +144,15 @@ class Theme extends StatelessWidget {
theme: this,
child: IconTheme(
data: data.iconTheme,
child: child,
child: CupertinoTheme(
// We're using a MaterialBasedCupertinoThemeData here instead of a
// CupertinoThemeData because it defers some properties to the Material
// ThemeData.
data: MaterialBasedCupertinoThemeData(
materialTheme: data,
),
child: child,
),
),
);
}

View file

@ -4,6 +4,7 @@
import 'dart:ui' show Color, hashValues;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -153,6 +154,7 @@ class ThemeData extends Diagnosticable {
ColorScheme colorScheme,
DialogTheme dialogTheme,
Typography typography,
CupertinoThemeData cupertinoOverrideTheme
}) {
brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark;
@ -246,6 +248,7 @@ class ThemeData extends Diagnosticable {
labelStyle: textTheme.body2,
);
dialogTheme ??= const DialogTheme();
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
return ThemeData.raw(
brightness: brightness,
@ -294,11 +297,13 @@ class ThemeData extends Diagnosticable {
colorScheme: colorScheme,
dialogTheme: dialogTheme,
typography: typography,
cupertinoOverrideTheme: cupertinoOverrideTheme,
);
}
/// Create a [ThemeData] given a set of exact values. All the values
/// must be specified.
/// Create a [ThemeData] given a set of exact values. All the values must be
/// specified. They all must also be non-null except for
/// [cupertinoOverrideTheme].
///
/// This will rarely be used directly. It is used by [lerp] to
/// create intermediate themes based on two themes created with the
@ -353,6 +358,7 @@ class ThemeData extends Diagnosticable {
@required this.colorScheme,
@required this.dialogTheme,
@required this.typography,
@required this.cupertinoOverrideTheme,
}) : assert(brightness != null),
assert(primaryColor != null),
assert(primaryColorBrightness != null),
@ -630,6 +636,18 @@ class ThemeData extends Diagnosticable {
/// [primaryTextTheme], and [accentTextTheme].
final Typography typography;
/// Components of the [CupertinoThemeData] to override from the Material
/// [ThemeData] adaptation.
///
/// By default, [cupertinoOverrideTheme] is null and Cupertino widgets
/// descendant to the Material [Theme] will adhere to a [CupertinoTheme]
/// derived from the Material [ThemeData]. e.g. [ThemeData]'s [ColorTheme]
/// will also inform the [CupertinoThemeData]'s `primaryColor` etc.
///
/// This cascading effect for individual attributes of the [CupertinoThemeData]
/// can be overridden using attributes of this [cupertinoOverrideTheme].
final CupertinoThemeData cupertinoOverrideTheme;
/// Creates a copy of this theme but with the given fields replaced with the new values.
ThemeData copyWith({
Brightness brightness,
@ -678,7 +696,9 @@ class ThemeData extends Diagnosticable {
ColorScheme colorScheme,
DialogTheme dialogTheme,
Typography typography,
CupertinoThemeData cupertinoOverrideTheme,
}) {
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
return ThemeData.raw(
brightness: brightness ?? this.brightness,
primaryColor: primaryColor ?? this.primaryColor,
@ -726,6 +746,7 @@ class ThemeData extends Diagnosticable {
colorScheme: colorScheme ?? this.colorScheme,
dialogTheme: dialogTheme ?? this.dialogTheme,
typography: typography ?? this.typography,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
);
}
@ -853,6 +874,7 @@ class ThemeData extends Diagnosticable {
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
typography: Typography.lerp(a.typography, b.typography, t),
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
);
}
@ -909,7 +931,8 @@ class ThemeData extends Diagnosticable {
(otherData.pageTransitionsTheme == pageTransitionsTheme) &&
(otherData.colorScheme == colorScheme) &&
(otherData.dialogTheme == dialogTheme) &&
(otherData.typography == typography);
(otherData.typography == typography) &&
(otherData.cupertinoOverrideTheme == cupertinoOverrideTheme);
}
@override
@ -967,6 +990,7 @@ class ThemeData extends Diagnosticable {
colorScheme,
dialogTheme,
typography,
cupertinoOverrideTheme,
),
),
);
@ -1019,6 +1043,109 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography));
properties.add(DiagnosticsProperty<CupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme));
}
}
/// A [CupertinoThemeData] that defers unspecified theme attributes to an
/// upstream Material [ThemeData].
///
/// This type of [CupertinoThemeData] is used by the Material [Theme] to
/// harmonize the [CupertinoTheme] with the material theme's colors and text
/// styles.
///
/// In the most basic case, [ThemeData]'s `cupertinoOverrideTheme` is null and
/// and descendant Cupertino widgets' styling is derived from the Material theme.
///
/// To override individual parts of the Material-derived Cupertino styling,
/// `cupertinoOverrideTheme`'s construction parameters can be used.
///
/// To completely decouple the Cupertino styling from Material theme derivation,
/// another [CupertinoTheme] widget can be inserted as a descendant of the
/// Material [Theme]. On a [MaterialApp], this can be done using the `builder`
/// parameter on the constructor.
///
/// See also:
///
/// * [CupertinoThemeData], whose null constructor parameters default to
/// reasonable iOS styling defaults rather than harmonizing with a Material
/// theme.
/// * [Theme], widget which inserts a [CupertinoTheme] with this
/// [MaterialBasedCupertinoThemeData].
// This class subclasses CupertinoThemeData rather than composes one because it
// _is_ a CupertinoThemeData with partially altered behavior. e.g. its textTheme
// is from the superclass and based on the primaryColor but the primaryColor
// comes from the Material theme unless overridden.
class MaterialBasedCupertinoThemeData extends CupertinoThemeData {
/// Create a [MaterialBasedCupertinoThemeData] based on a Material [ThemeData]
/// and its `cupertinoOverrideTheme`.
///
/// The [materialTheme] parameter must not be null.
MaterialBasedCupertinoThemeData({
@required ThemeData materialTheme,
}) : assert(materialTheme != null),
_materialTheme = materialTheme,
// Pass all values to the superclass so Material-agnostic properties
// like barBackgroundColor can still behave like a normal
// CupertinoThemeData.
super.raw(
materialTheme.cupertinoOverrideTheme?.brightness,
materialTheme.cupertinoOverrideTheme?.primaryColor,
materialTheme.cupertinoOverrideTheme?.primaryContrastingColor,
materialTheme.cupertinoOverrideTheme?.textTheme,
materialTheme.cupertinoOverrideTheme?.barBackgroundColor,
materialTheme.cupertinoOverrideTheme?.scaffoldBackgroundColor,
);
final ThemeData _materialTheme;
@override
Brightness get brightness => _materialTheme.cupertinoOverrideTheme?.brightness ?? _materialTheme.brightness;
@override
Color get primaryColor => _materialTheme.cupertinoOverrideTheme?.primaryColor ?? _materialTheme.colorScheme.primary;
@override
Color get primaryContrastingColor => _materialTheme.cupertinoOverrideTheme?.primaryContrastingColor ?? _materialTheme.colorScheme.onPrimary;
@override
Color get scaffoldBackgroundColor => _materialTheme.cupertinoOverrideTheme?.scaffoldBackgroundColor ?? _materialTheme.scaffoldBackgroundColor;
/// Copies the [ThemeData]'s `cupertinoOverrideTheme`.
///
/// Only the specified override attributes of the [ThemeData]'s
/// `cupertinoOverrideTheme` and the newly specified parameters are in the
/// returned [CupertinoThemeData]. No derived attributes from iOS defaults or
/// from cascaded Material theme attributes are copied.
///
/// [MaterialBasedCupertinoThemeData.copyWith] cannot change the base
/// Material [ThemeData]. To change the base Material [ThemeData], create a
/// new Material [Theme] and use `copyWith` on the Material [ThemeData]
/// instead.
@override
CupertinoThemeData copyWith({
Brightness brightness,
Color primaryColor,
Color primaryContrastingColor,
CupertinoTextThemeData textTheme,
Color barBackgroundColor,
Color scaffoldBackgroundColor,
}) {
return _materialTheme.cupertinoOverrideTheme?.copyWith(
brightness: brightness,
primaryColor: primaryColor,
primaryContrastingColor: primaryContrastingColor,
textTheme: textTheme,
barBackgroundColor: barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor,
) ?? CupertinoThemeData(
brightness: brightness,
primaryColor: primaryColor,
primaryContrastingColor: primaryContrastingColor,
textTheme: textTheme,
barBackgroundColor: barBackgroundColor,
scaffoldBackgroundColor: scaffoldBackgroundColor,
);
}
}

View file

@ -182,7 +182,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
// Proof by contradiction:
//
// If we introduce a loop, this retained layer must be appended to one of
// its descendent layers, say A. That means the child structure of A has
// its descendant layers, say A. That means the child structure of A has
// changed so A's _needsAddToScene is true. This contradicts
// _subtreeNeedsAddToScene being false.
if (!_subtreeNeedsAddToScene && _engineLayer != null) {

View file

@ -46,7 +46,7 @@ import 'framework.dart';
///
/// When the inherited model is rebuilt the [updateShouldNotify] and
/// [updateShouldNotifyDependent] methods are used to decide what
/// should be rebuilt. If [updateShouldNotify] returns true, then the
/// should be rebuilt. If [updateShouldNotify] returns true, then the
/// inherited model's [updateShouldNotifyDependent] method is tested for
/// each dependent and the set of aspect objects it depends on.
/// The [updateShouldNotifyDependent] method must compare the set of aspect
@ -153,6 +153,8 @@ abstract class InheritedModel<T> extends InheritedWidget {
///
/// If [aspect] is null this method is the same as
/// `context.inheritFromWidgetOfExactType(T)`.
///
/// If no ancestor of type T exists, null is returned.
static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
if (aspect == null)
return context.inheritFromWidgetOfExactType(T);
@ -160,6 +162,10 @@ abstract class InheritedModel<T> extends InheritedWidget {
// Create a dependency on all of the type T ancestor models up until
// a model is found for which isSupportedAspect(aspect) is true.
final List<InheritedElement> models = _findModels<T>(context, aspect).toList();
if (models.isEmpty) {
return null;
}
final InheritedElement lastModel = models.last;
for (InheritedElement model in models) {
final T value = context.inheritFromElement(model, aspect: aspect);

View file

@ -43,7 +43,7 @@ typedef ValueWidgetBuilder<T> = Widget Function(BuildContext context, T value, W
/// * [AnimatedBuilder], which also triggers rebuilds from a [Listenable]
/// without passing back a specific value from a [ValueListenable].
/// * [NotificationListener], which lets you rebuild based on [Notification]
/// coming from its descendent widgets rather than a [ValueListenable] that
/// coming from its descendant widgets rather than a [ValueListenable] that
/// you have a direct reference to.
/// * [StreamBuilder], where a builder can depend on a [Stream] rather than
/// a [ValueListenable] for more advanced use cases.

View file

@ -68,6 +68,70 @@ void main() {
expect(actualActive.text.style.color, const Color(0xFF123456));
});
testWidgets('Tabs respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 1'),
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 2'),
),
],
currentIndex: 1,
),
),
);
RichText actualInactive = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color, CupertinoColors.inactiveGray);
RichText actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(actualActive.text.style.color, CupertinoColors.activeBlue);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 1'),
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
title: Text('Tab 2'),
),
],
currentIndex: 1,
),
),
);
actualInactive = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
expect(actualInactive.text.style.color, CupertinoColors.inactiveGray);
actualActive = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(actualActive.text.style.color, CupertinoColors.activeOrange);
});
testWidgets('Use active icon', (WidgetTester tester) async {
const TestImageProvider activeIcon = TestImageProvider(16, 16);
const TestImageProvider inactiveIcon = TestImageProvider(24, 24);

View file

@ -12,6 +12,7 @@ import '../widgets/semantics_tester.dart';
const TextStyle testStyle = TextStyle(
fontFamily: 'Ahem',
fontSize: 10.0,
letterSpacing: 0.0,
);
void main() {
@ -226,6 +227,80 @@ void main() {
expect(boxDecoration.color, const Color(0x00FF00));
});
testWidgets('Botton respects themes', (WidgetTester tester) async {
TextStyle textStyle;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoButton(
onPressed: () {},
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, CupertinoColors.activeBlue);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoButton.filled(
onPressed: () {},
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, CupertinoColors.white);
BoxDecoration decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox)
)
).decoration;
expect(decoration.color, CupertinoColors.activeBlue);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoButton(
onPressed: () {},
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, CupertinoColors.activeOrange);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoButton.filled(
onPressed: () {},
child: Builder(builder: (BuildContext context) {
textStyle = DefaultTextStyle.of(context).style;
return const Placeholder();
}),
),
),
);
expect(textStyle.color, CupertinoColors.black);
decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoButton),
matching: find.byType(DecoratedBox)
)
).decoration;
expect(decoration.color, CupertinoColors.activeOrange);
});
}
Widget boilerplate({ Widget child }) {

View file

@ -148,15 +148,62 @@ void main() {
expect(tester.getCenter(find.text('Title')).dx, 400.0);
});
testWidgets('Verify styles of each slot', (WidgetTester tester) async {
testWidgets('Nav bar uses theme defaults', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
const CupertinoApp(
CupertinoApp(
home: CupertinoNavigationBar(
leading: _ExpectStyles(color: Color(0xFF001122), index: 0x000001),
middle: _ExpectStyles(color: Color(0xFF000000), letterSpacing: -0.08, index: 0x000100),
trailing: _ExpectStyles(color: Color(0xFF001122), index: 0x010000),
actionsForegroundColor: Color(0xFF001122),
leading: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: CupertinoColors.activeBlue, index: 0x000001),
),
middle: const _ExpectStyles(color: CupertinoColors.black, index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: CupertinoColors.activeBlue, index: 0x010000),
),
),
),
);
expect(count, 0x010101);
});
testWidgets('Nav bar respects themes', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: CupertinoNavigationBar(
leading: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: CupertinoColors.activeOrange, index: 0x000001),
),
middle: const _ExpectStyles(color: CupertinoColors.white, index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: CupertinoColors.activeOrange, index: 0x010000),
),
),
),
);
expect(count, 0x010101);
});
testWidgets('Theme active color can be overriden', (WidgetTester tester) async {
count = 0x000000;
await tester.pumpWidget(
CupertinoApp(
home: CupertinoNavigationBar(
leading: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: Color(0xFF001122), index: 0x000001),
),
middle: const _ExpectStyles(color: Color(0xFF000000), index: 0x000100),
trailing: CupertinoButton(
onPressed: () {},
child: const _ExpectStyles(color: Color(0xFF001122), index: 0x010000),
),
actionsForegroundColor: const Color(0xFF001122),
),
),
);
@ -845,18 +892,18 @@ void main() {
}
class _ExpectStyles extends StatelessWidget {
const _ExpectStyles({ this.color, this.letterSpacing, this.index });
const _ExpectStyles({ this.color, this.index });
final Color color;
final double letterSpacing;
final int index;
@override
Widget build(BuildContext context) {
final TextStyle style = DefaultTextStyle.of(context).style;
expect(style.color, color);
expect(style.fontFamily, '.SF Pro Text');
expect(style.fontSize, 17.0);
expect(style.letterSpacing, letterSpacing ?? -0.24);
expect(style.letterSpacing, -0.41);
count += index;
return Container();
}

View file

@ -13,9 +13,11 @@ Future<void> startTransitionBetween(
String fromTitle,
String toTitle,
TextDirection textDirection = TextDirection.ltr,
CupertinoThemeData theme,
}) async {
await tester.pumpWidget(
CupertinoApp(
theme: theme,
builder: (BuildContext context, Widget navigator) {
return Directionality(
textDirection: textDirection,
@ -79,19 +81,15 @@ Finder flying(WidgetTester tester, Finder finder) {
});
assert(
find
.descendant(
of: lastOverlayFinder,
matching: find.byWidgetPredicate(
(Widget widget) =>
widget.runtimeType.toString() ==
'_NavigationBarTransition',
),
)
.evaluate()
.length ==
1,
'The last overlay in the navigator was not a flying hero',);
find.descendant(
of: lastOverlayFinder,
matching: find.byWidgetPredicate(
(Widget widget) =>
widget.runtimeType.toString() == '_NavigationBarTransition',
),
).evaluate().length == 1,
'The last overlay in the navigator was not a flying hero',
);
return find.descendant(
of: lastOverlayFinder,
@ -103,28 +101,25 @@ void checkBackgroundBoxHeight(WidgetTester tester, double height) {
final Widget transitionBackgroundBox =
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0];
expect(
tester
.widget<SizedBox>(
find.descendant(
of: find.byWidget(transitionBackgroundBox),
matching: find.byType(SizedBox),
),
)
.height,
tester.widget<SizedBox>(
find.descendant(
of: find.byWidget(transitionBackgroundBox),
matching: find.byType(SizedBox),
),
).height,
height,
);
}
void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
expect(
tester
.renderObject<RenderAnimatedOpacity>(find.ancestor(
of: finder,
matching: find.byType(FadeTransition),
))
.opacity
.value,
opacity,
tester.renderObject<RenderAnimatedOpacity>(
find.ancestor(
of: finder,
matching: find.byType(FadeTransition),
),
).opacity.value,
moreOrLessEquals(opacity, epsilon: 0.000000001),
);
}
@ -144,11 +139,11 @@ void main() {
// place.
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(331.0724935531616, 13.5),
const Offset(332.0129337310791, 13.5),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(331.0724935531616, 13.5),
const Offset(332.0129337310791, 13.5),
);
});
@ -167,11 +162,11 @@ void main() {
// Same as LTR but more to the right now.
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(366.9275064468384, 13.5),
const Offset(367.9870662689209, 13.5),
);
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(366.9275064468384, 13.5),
const Offset(367.9870662689209, 13.5),
);
});
@ -187,8 +182,8 @@ void main() {
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w600);
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text');
expect(bottomMiddle.text.style.letterSpacing, -0.08952957153320312);
expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity(
tester, flying(tester, find.text('Page 1')).first, 0.8609542846679688);
@ -199,8 +194,8 @@ void main() {
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(topBackLabel.text.style.fontWeight, FontWeight.w600);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
expect(topBackLabel.text.style.letterSpacing, -0.08952957153320312);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
@ -208,20 +203,69 @@ void main() {
await tester.pump(const Duration(milliseconds: 200));
expect(bottomMiddle.text.style.color, const Color(0xFF0073F0));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w400);
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text');
expect(bottomMiddle.text.style.letterSpacing, -0.231169798374176);
expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
expect(topBackLabel.text.style.letterSpacing, -0.231169798374176);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity(
tester, flying(tester, find.text('Page 1')).last, 0.8733493089675903);
});
testWidgets('Font transitions respect themes',
(WidgetTester tester) async {
await startTransitionBetween(
tester,
fromTitle: 'Page 1',
theme: const CupertinoThemeData(brightness: Brightness.dark),
);
// Be mid-transition.
await tester.pump(const Duration(milliseconds: 50));
// The transition's stack is ordered. The bottom middle is inserted first.
final RenderParagraph bottomMiddle =
tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style.color, const Color(0xFFFFF8EF));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w600);
expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.8609542846679688);
// The top back label is styled exactly the same way. But the opacity tweens
// are flipped.
final RenderParagraph topBackLabel =
tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style.color, const Color(0xFFFFF8EF));
expect(topBackLabel.text.style.fontWeight, FontWeight.w600);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
// Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200));
expect(bottomMiddle.text.style.color, const Color(0xFFFF9A0E));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w400);
expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
expect(topBackLabel.text.style.color, const Color(0xFFFF9A0E));
expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.8733493089675903);
});
testWidgets('Fullscreen dialogs do not create heroes',
(WidgetTester tester) async {
await tester.pumpWidget(
@ -295,7 +339,7 @@ void main() {
expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(331.0724935531616, 13.5),
const Offset(332.0129337310791, 13.5),
);
// The top back label is styled exactly the same way. But the opacity tweens
@ -305,7 +349,7 @@ void main() {
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(331.0724935531616, 13.5),
const Offset(332.0129337310791, 13.5),
);
}
@ -341,7 +385,7 @@ void main() {
expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(366.9275064468384, 13.5),
const Offset(367.9870662689209, 13.5),
);
// The top back label is styled exactly the same way. But the opacity tweens
@ -351,7 +395,7 @@ void main() {
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last),
const Offset(366.9275064468384, 13.5),
const Offset(367.9870662689209, 13.5),
);
}
@ -731,14 +775,14 @@ void main() {
checkOpacity(tester, flying(tester, find.text('custom')), 0.8393326997756958);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(683.0, 13.5),
const Offset(684.0, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('custom'))),
const Offset(683.0, 13.5),
const Offset(684.0, 13.5),
);
});
@ -915,7 +959,7 @@ void main() {
expect(bottomLargeTitle.text.style.color, const Color(0xFF00070F));
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w700);
expect(bottomLargeTitle.text.style.fontFamily, '.SF Pro Display');
expect(bottomLargeTitle.text.style.letterSpacing, 0.21141128540039061);
expect(bottomLargeTitle.text.style.letterSpacing, 0.36116094589233394);
// The top back label is styled exactly the same way.
final RenderParagraph topBackLabel =
@ -923,19 +967,19 @@ void main() {
expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(topBackLabel.text.style.fontWeight, FontWeight.w700);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Display');
expect(topBackLabel.text.style.letterSpacing, 0.21141128540039061);
expect(topBackLabel.text.style.letterSpacing, 0.36116094589233394);
// Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200));
expect(bottomLargeTitle.text.style.color, const Color(0xFF0073F0));
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w400);
expect(bottomLargeTitle.text.style.fontFamily, '.SF UI Text');
expect(bottomLargeTitle.text.style.letterSpacing, -0.2135093951225281);
expect(bottomLargeTitle.text.style.fontFamily, '.SF Pro Text');
expect(bottomLargeTitle.text.style.letterSpacing, -0.3647452166676521);
expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text');
expect(topBackLabel.text.style.letterSpacing, -0.2135093951225281);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.3647452166676521);
});
testWidgets('Top middle fades in and slides in from the right',
@ -952,7 +996,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(725.1760711669922, 13.5),
const Offset(726.1760711669922, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
@ -960,7 +1004,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703);
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(408.02137756347656, 13.5),
const Offset(409.02137756347656, 13.5),
);
});
@ -979,7 +1023,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(74.82392883300781, 13.5),
const Offset(73.82392883300781, 13.5),
);
await tester.pump(const Duration(milliseconds: 150));
@ -987,7 +1031,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703);
expect(
tester.getTopRight(flying(tester, find.text('Page 2'))),
const Offset(391.97862243652344, 13.5),
const Offset(390.97862243652344, 13.5),
);
});
@ -1115,7 +1159,7 @@ void main() {
// Page 2, which is the middle of the top route, start to fly back to the right.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(352.5802058875561, 13.5),
const Offset(353.5802058875561, 13.5),
);
// Page 1 is in transition in 2 places. Once as the top back label and once
@ -1131,12 +1175,12 @@ void main() {
// Transition continues.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(654.2055835723877, 13.5),
const Offset(655.2055835723877, 13.5),
);
await tester.pump(const Duration(milliseconds: 50));
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(720.8727767467499, 13.5),
const Offset(721.8727767467499, 13.5),
);
await tester.pump(const Duration(milliseconds: 500));
@ -1171,7 +1215,7 @@ void main() {
// Page 2, which is the middle of the top route, start to fly back to the right.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(352.5802058875561, 13.5),
const Offset(353.5802058875561, 13.5),
);
await gesture.up();
@ -1180,12 +1224,12 @@ void main() {
// Transition continues from the point we let off.
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(352.5802058875561, 13.5),
const Offset(353.5802058875561, 13.5),
);
await tester.pump(const Duration(milliseconds: 50));
expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(350.00985169410706, 13.5),
const Offset(351.00985169410706, 13.5),
);
// Finish the snap back animation.

View file

@ -142,7 +142,7 @@ void main() {
}
});
testWidgets('Children, onValueChanged, and color arguments can not be null',
testWidgets('Children and onValueChanged arguments can not be null',
(WidgetTester tester) async {
try {
await tester.pumpWidget(
@ -175,21 +175,6 @@ void main() {
} on AssertionError catch (e) {
expect(e.toString(), contains('onValueChanged'));
}
try {
await tester.pumpWidget(
boilerplate(
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {},
unselectedColor: null,
),
),
);
fail('Should not be possible to create segmented control with null unselectedColor');
} on AssertionError catch (e) {
expect(e.toString(), contains('unselectedColor'));
}
});
testWidgets('Widgets have correct default text/icon styles, change correctly on selection',
@ -236,6 +221,52 @@ void main() {
expect(iconTheme.data.color, CupertinoColors.white);
});
testWidgets(
'Segmented controls respects themes',
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Icon(IconData(1));
int sharedValue = 0;
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return CupertinoSegmentedControl<int>(
children: children,
onValueChanged: (int newValue) {
setState(() {
sharedValue = newValue;
});
},
groupValue: sharedValue,
);
},
),
),
);
DefaultTextStyle textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first);
IconTheme iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
expect(textStyle.style.color, CupertinoColors.black);
expect(iconTheme.data.color, CupertinoColors.activeOrange);
await tester.tap(find.widgetWithIcon(IconTheme, const IconData(1)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first);
iconTheme = tester.widget(find.widgetWithIcon(IconTheme, const IconData(1)));
expect(textStyle.style.color, CupertinoColors.activeOrange);
expect(iconTheme.data.color, CupertinoColors.black);
},
);
testWidgets('SegmentedControl is correct when user provides custom colors',
(WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{};
@ -1171,13 +1202,13 @@ void main() {
await tester.tap(find.text('B'));
await tester.pump();
expect(getBackgroundColor(tester, 0), const Color(0xff007aff));
expect(getBackgroundColor(tester, 1), const Color(0x33007aff));
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff));
expect(getBackgroundColor(tester, 1), const Color(0x64007aff));
expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 150));

View file

@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
@ -359,4 +360,57 @@ void main() {
handle.dispose();
});
testWidgets('Slider respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoSlider(
onChanged: (double value) {},
value: 0.5,
),
),
),
);
expect(
find.byType(CupertinoSlider),
// First line it paints is blue.
paints..rrect(color: CupertinoColors.activeBlue),
);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoSlider(
onChanged: (double value) {},
value: 0.5,
),
),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: CupertinoColors.activeOrange),
);
});
testWidgets('Themes can be overridden', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoSlider(
activeColor: CupertinoColors.activeGreen,
onChanged: (double value) {},
value: 0.5,
),
),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: CupertinoColors.activeGreen),
);
});
}

View file

@ -254,6 +254,64 @@ void main() {
expect(tabsPainted, <int>[0, 1, 0]);
expect(selectedTabs, <int>[0]);
});
testWidgets('Tab bar respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const Placeholder();
},
),
),
);
BoxDecoration tabDecoration = tester.widget<DecoratedBox>(find.descendant(
of: find.byType(CupertinoTabBar),
matching: find.byType(DecoratedBox),
)).decoration;
expect(tabDecoration.color, const Color(0xCCF8F8F8));
await tester.tap(find.text('Tab 2'));
await tester.pump();
// Pump again but with dark theme.
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(
brightness: Brightness.dark,
primaryColor: CupertinoColors.destructiveRed,
),
home: CupertinoTabScaffold(
tabBar: _buildTabBar(),
tabBuilder: (BuildContext context, int index) {
return const Placeholder();
},
),
),
);
tabDecoration = tester.widget<DecoratedBox>(find.descendant(
of: find.byType(CupertinoTabBar),
matching: find.byType(DecoratedBox),
)).decoration;
expect(tabDecoration.color, const Color(0xB7212121));
final RichText tab1 = tester.widget(find.descendant(
of: find.text('Tab 1'),
matching: find.byType(RichText),
));
// Tab 2 should still be selected after changing theme.
expect(tab1.text.style.color, CupertinoColors.inactiveGray);
final RichText tab2 = tester.widget(find.descendant(
of: find.text('Tab 2'),
matching: find.byType(RichText),
));
expect(tab2.text.style.color, CupertinoColors.destructiveRed);
});
}
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
@ -268,7 +326,6 @@ CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
title: Text('Tab 2'),
),
],
backgroundColor: CupertinoColors.white,
currentIndex: selectedTab,
onTap: (int newTab) => selectedTabs.add(newTab),
);

View file

@ -1098,4 +1098,42 @@ void main() {
expect(find.byType(CupertinoButton), findsNWidgets(3));
},
);
testWidgets(
'text field respects theme',
(WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
theme: CupertinoThemeData(
brightness: Brightness.dark,
),
home: Center(
child: CupertinoTextField(),
),
),
);
final BoxDecoration decoration = tester.widget<DecoratedBox>(
find.descendant(
of: find.byType(CupertinoTextField),
matching: find.byType(DecoratedBox)
),
).decoration;
expect(
decoration.border.bottom.color,
CupertinoColors.lightBackgroundGray, // Border color is the same regardless.
);
await tester.enterText(find.byType(CupertinoTextField), 'smoked meat');
await tester.pump();
expect(
tester.renderObject<RenderEditable>(
find.byElementPredicate((Element element) => element.renderObject is RenderEditable)
).text.style.color,
CupertinoColors.white,
);
},
);
}

View file

@ -0,0 +1,126 @@
// Copyright 2018 The Chromium 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/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
int buildCount;
CupertinoThemeData actualTheme;
final Widget singletonThemeSubtree = Builder(
builder: (BuildContext context) {
buildCount++;
actualTheme = CupertinoTheme.of(context);
return const Placeholder();
},
);
Future<CupertinoThemeData> testTheme(WidgetTester tester, CupertinoThemeData theme) async {
await tester.pumpWidget(
CupertinoTheme(
data: theme,
child: singletonThemeSubtree,
),
);
return actualTheme;
}
void main() {
setUp(() {
buildCount = 0;
actualTheme = null;
});
testWidgets('Default theme has defaults', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData());
expect(theme.brightness, Brightness.light);
expect(theme.primaryColor, CupertinoColors.activeBlue);
expect(theme.textTheme.textStyle.fontSize, 17.0);
});
testWidgets('Theme attributes cascade', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData(
primaryColor: CupertinoColors.destructiveRed,
));
expect(theme.textTheme.actionTextStyle.color, CupertinoColors.destructiveRed);
});
testWidgets('Dependent attribute can be overridden from cascaded value', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData(
brightness: Brightness.dark,
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(color: CupertinoColors.black),
)
));
// The brightness still cascaded down to the background color.
expect(theme.scaffoldBackgroundColor, CupertinoColors.black);
// But not to the font color which we overrode.
expect(theme.textTheme.textStyle.color, CupertinoColors.black);
});
testWidgets(
'Reading themes creates dependencies',
(WidgetTester tester) async {
// Reading the theme creates a dependency.
CupertinoThemeData theme = await testTheme(tester, const CupertinoThemeData(
// Default brightness is light,
barBackgroundColor: Color(0x11223344),
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(fontFamily: 'Skeuomorphic'),
),
));
expect(buildCount, 1);
expect(theme.textTheme.textStyle.fontFamily, 'Skeuomorphic');
// Changing another property also triggers a rebuild.
theme = await testTheme(tester, const CupertinoThemeData(
brightness: Brightness.light,
barBackgroundColor: Color(0x11223344),
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(fontFamily: 'Skeuomorphic'),
),
));
expect(buildCount, 2);
// Re-reading the same value doesn't change anything.
expect(theme.textTheme.textStyle.fontFamily, 'Skeuomorphic');
theme = await testTheme(tester, const CupertinoThemeData(
brightness: Brightness.light,
barBackgroundColor: Color(0x11223344),
textTheme: CupertinoTextThemeData(
textStyle: TextStyle(fontFamily: 'Flat'),
),
));
expect(buildCount, 3);
expect(theme.textTheme.textStyle.fontFamily, 'Flat');
},
);
testWidgets(
'copyWith works',
(WidgetTester tester) async {
const CupertinoThemeData originalTheme = CupertinoThemeData(
brightness: Brightness.dark,
);
final CupertinoThemeData theme = await testTheme(tester, originalTheme.copyWith(
primaryColor: CupertinoColors.activeGreen,
));
expect(theme.brightness, Brightness.dark);
expect(theme.primaryColor, CupertinoColors.activeGreen);
// Now check calculated derivatives.
expect(theme.textTheme.actionTextStyle.color, CupertinoColors.activeGreen);
expect(theme.scaffoldBackgroundColor, CupertinoColors.black);
},
);
}

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
@ -412,6 +413,227 @@ void main() {
expect(theme.textTheme.display4.debugLabel, '(englishLike display4 2014).merge(blackMountainView display4)');
});
group('Cupertino theme', () {
int buildCount;
CupertinoThemeData actualTheme;
final Widget singletonThemeSubtree = Builder(
builder: (BuildContext context) {
buildCount++;
actualTheme = CupertinoTheme.of(context);
return const Placeholder();
},
);
Future<CupertinoThemeData> testTheme(WidgetTester tester, ThemeData theme) async {
await tester.pumpWidget(
Theme(
data: theme,
child: singletonThemeSubtree,
),
);
return actualTheme;
}
setUp(() {
buildCount = 0;
actualTheme = null;
});
testWidgets('Default theme has defaults', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, ThemeData.light());
expect(theme.brightness, Brightness.light);
expect(theme.primaryColor, Colors.blue);
expect(theme.scaffoldBackgroundColor, Colors.grey[50]);
expect(theme.primaryContrastingColor, Colors.white);
expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
expect(theme.textTheme.textStyle.fontSize, 17.0);
});
testWidgets('Dark theme has defaults', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, ThemeData.dark());
expect(theme.brightness, Brightness.dark);
expect(theme.primaryColor, Colors.blue);
expect(theme.primaryContrastingColor, Colors.white);
expect(theme.scaffoldBackgroundColor, Colors.grey[850]);
expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
expect(theme.textTheme.textStyle.fontSize, 17.0);
});
testWidgets('Can override material theme', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, ThemeData(
cupertinoOverrideTheme: const CupertinoThemeData(
scaffoldBackgroundColor: CupertinoColors.lightBackgroundGray,
),
));
expect(theme.brightness, Brightness.light);
// We took the scaffold background override but the rest are still cascaded
// to the material theme.
expect(theme.primaryColor, Colors.blue);
expect(theme.primaryContrastingColor, Colors.white);
expect(theme.scaffoldBackgroundColor, CupertinoColors.lightBackgroundGray);
expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
expect(theme.textTheme.textStyle.fontSize, 17.0);
});
testWidgets('Can override properties that are independent of material', (WidgetTester tester) async {
final CupertinoThemeData theme = await testTheme(tester, ThemeData(
cupertinoOverrideTheme: const CupertinoThemeData(
// The bar colors ignore all things material except brightness.
barBackgroundColor: CupertinoColors.black,
),
));
expect(theme.primaryColor, Colors.blue);
// MaterialBasedCupertinoThemeData should also function like a normal CupertinoThemeData.
expect(theme.barBackgroundColor, CupertinoColors.black);
});
testWidgets('Changing material theme triggers rebuilds', (WidgetTester tester) async {
CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.red,
));
expect(buildCount, 1);
expect(theme.primaryColor, Colors.red);
theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.orange,
));
expect(buildCount, 2);
expect(theme.primaryColor, Colors.orange);
});
testWidgets(
'Changing cupertino theme override triggers rebuilds',
(WidgetTester tester) async {
CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryColor: CupertinoColors.activeOrange,
),
));
expect(buildCount, 1);
expect(theme.primaryColor, CupertinoColors.activeOrange);
theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryColor: CupertinoColors.activeGreen,
),
));
expect(buildCount, 2);
expect(theme.primaryColor, CupertinoColors.activeGreen);
},
);
testWidgets(
'Cupertino theme override blocks derivative changes',
(WidgetTester tester) async {
CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryColor: CupertinoColors.activeOrange,
),
));
expect(buildCount, 1);
expect(theme.primaryColor, CupertinoColors.activeOrange);
// Change the upstream material primary color.
theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.blue,
cupertinoOverrideTheme: const CupertinoThemeData(
// But the primary material color is preempted by the override.
primaryColor: CupertinoColors.activeOrange,
),
));
expect(buildCount, 2);
expect(theme.primaryColor, CupertinoColors.activeOrange);
},
);
testWidgets(
'Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden',
(WidgetTester tester) async {
CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryContrastingColor: CupertinoColors.destructiveRed,
),
));
expect(buildCount, 1);
expect(theme.textTheme.actionTextStyle.color, Colors.purple);
expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);
theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.green,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryContrastingColor: CupertinoColors.destructiveRed,
),
));
expect(buildCount, 2);
expect(theme.textTheme.actionTextStyle.color, Colors.green);
expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);
},
);
testWidgets(
'copyWith only copies the overrides, not the material or cupertino derivatives',
(WidgetTester tester) async {
final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
cupertinoOverrideTheme: const CupertinoThemeData(
primaryContrastingColor: CupertinoColors.activeOrange,
),
));
final CupertinoThemeData copiedTheme = originalTheme.copyWith(
barBackgroundColor: CupertinoColors.destructiveRed,
);
final CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.blue,
cupertinoOverrideTheme: copiedTheme,
));
expect(theme.primaryColor, Colors.blue);
expect(theme.primaryContrastingColor, CupertinoColors.activeOrange);
expect(theme.barBackgroundColor, CupertinoColors.destructiveRed);
},
);
testWidgets(
"Material themes with no cupertino overrides can also be copyWith'ed",
(WidgetTester tester) async {
final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData(
primarySwatch: Colors.purple,
));
final CupertinoThemeData copiedTheme = originalTheme.copyWith(
primaryContrastingColor: CupertinoColors.destructiveRed,
);
final CupertinoThemeData theme = await testTheme(tester, ThemeData(
primarySwatch: Colors.blue,
cupertinoOverrideTheme: copiedTheme,
));
expect(theme.primaryColor, Colors.blue);
expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);
},
);
});
}
int testBuildCalled;

View file

@ -190,6 +190,22 @@ void main() {
expect(find.text('a: 2 b: 2 c: 3'), findsOneWidget);
});
testWidgets('Looking up an non existent InherintedModel ancestor returns null', (WidgetTester tester) async {
ABCModel inheritedModel;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
inheritedModel = InheritedModel.inheritFrom(context);
return Container();
},
),
);
// Shouldn't crash first of all.
expect(inheritedModel, null);
});
testWidgets('Inner InheritedModel shadows the outer one', (WidgetTester tester) async {
int _a = 0;
int _b = 1;