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) { Widget build(BuildContext context) {
return CupertinoPageScaffold( return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar( navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Back', // We're specifying a back label here because the previous page is a
middle: const Text('Cupertino Activity Indicator'), // Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
middle: const Text('Activity Indicator'),
trailing: CupertinoDemoDocumentationButton(routeName), trailing: CupertinoDemoDocumentationButton(routeName),
), ),
child: const Center( child: const Center(

View file

@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart'; import '../../gallery/demo.dart';
@ -15,181 +14,193 @@ class CupertinoAlertDemo extends StatefulWidget {
} }
class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> { class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); String lastSelectedValue;
void showDemoDialog<T>({BuildContext context, Widget child}) { void showDemoDialog({BuildContext context, Widget child}) {
showDialog<T>( showCupertinoDialog<String>(
context: context, context: context,
barrierDismissible: false,
builder: (BuildContext context) => child, builder: (BuildContext context) => child,
).then<void>((T value) { ).then((String value) {
// The value passed to Navigator.pop() or null.
if (value != null) { if (value != null) {
_scaffoldKey.currentState.showSnackBar( setState(() { lastSelectedValue = value; });
SnackBar(
content: Text('You selected: $value'),
),
);
} }
}); });
} }
void showDemoActionSheet<T>({BuildContext context, Widget child}) { void showDemoActionSheet({BuildContext context, Widget child}) {
showCupertinoModalPopup<T>( showCupertinoModalPopup<String>(
context: context, context: context,
builder: (BuildContext context) => child, builder: (BuildContext context) => child,
).then<void>((T value) { ).then((String value) {
if (value != null) { if (value != null) {
_scaffoldKey.currentState.showSnackBar( setState(() { lastSelectedValue = value; });
SnackBar(
content: Text('You selected: $value'),
),
);
} }
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
key: _scaffoldKey, navigationBar: CupertinoNavigationBar(
appBar: AppBar( middle: const Text('Alerts'),
title: const Text('Cupertino Alerts'), // We're specifying a back label here because the previous page is a
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoAlertDemo.routeName)], // Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoAlertDemo.routeName),
), ),
body: ListView( child: DefaultTextStyle(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0), style: CupertinoTheme.of(context).textTheme.textStyle,
children: <Widget>[ child: Builder(
CupertinoButton( builder: (BuildContext context) {
child: const Text('Alert'), final List<Widget> stackChildren = <Widget>[
color: CupertinoColors.activeBlue, ListView(
onPressed: () { // Add more padding to the normal safe area.
showDemoDialog<String>( padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
context: context, + MediaQuery.of(context).padding,
child: CupertinoAlertDialog( children: <Widget>[
title: const Text('Discard draft?'), CupertinoButton.filled(
actions: <Widget>[ child: const Text('Alert'),
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,
onPressed: () { 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. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart'; import '../../gallery/demo.dart';
@ -19,64 +18,71 @@ class _CupertinoButtonDemoState extends State<CupertinoButtonsDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
appBar: AppBar( navigationBar: CupertinoNavigationBar(
title: const Text('Cupertino Buttons'), middle: const Text('Buttons'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoButtonsDemo.routeName)], // 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( child: DefaultTextStyle(
children: <Widget> [ style: CupertinoTheme.of(context).textTheme.textStyle,
const Padding( child: SafeArea(
padding: EdgeInsets.all(16.0), child: Column(
child: Text( children: <Widget>[
'iOS themed buttons are flat. They can have borders or backgrounds but ' const Padding(
'only when necessary.' padding: EdgeInsets.all(16.0),
), child: Text(
), 'iOS themed buttons are flat. They can have borders or backgrounds but '
Expanded( 'only when necessary.'
child: Column( ),
mainAxisAlignment: MainAxisAlignment.center, ),
children: <Widget> [ Expanded(
Text(_pressedCount > 0 child: Column(
? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}' mainAxisAlignment: MainAxisAlignment.center,
: ' '), children: <Widget> [
const Padding(padding: EdgeInsets.all(12.0)), Text(_pressedCount > 0
Align( ? 'Button pressed $_pressedCount time${_pressedCount == 1 ? "" : "s"}'
alignment: const Alignment(0.0, -0.2), : ' '),
child: Row( const Padding(padding: EdgeInsets.all(12.0)),
mainAxisSize: MainAxisSize.min, Align(
children: <Widget>[ alignment: const Alignment(0.0, -0.2),
CupertinoButton( child: Row(
child: const Text('Cupertino Button'), mainAxisSize: MainAxisSize.min,
onPressed: () { children: <Widget>[
setState(() { _pressedCount += 1; }); CupertinoButton(
} child: const Text('Cupertino Button'),
onPressed: () {
setState(() { _pressedCount += 1; });
}
),
const CupertinoButton(
child: Text('Disabled'),
onPressed: null,
),
],
), ),
const CupertinoButton( ),
child: Text('Disabled'), const Padding(padding: EdgeInsets.all(12.0)),
onPressed: null, 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. // Prevent swipe popping of this page. Use explicit exit buttons only.
onWillPop: () => Future<bool>.value(true), onWillPop: () => Future<bool>.value(true),
child: DefaultTextStyle( child: DefaultTextStyle(
style: const TextStyle( style: CupertinoTheme.of(context).textTheme.textStyle,
fontFamily: '.SF UI Text',
fontSize: 17.0,
color: CupertinoColors.black,
),
child: CupertinoTabScaffold( child: CupertinoTabScaffold(
tabBar: CupertinoTabBar( tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
@ -241,7 +237,6 @@ class Tab1RowItem extends StatelessWidget {
CupertinoButton( CupertinoButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.plus_circled, child: const Icon(CupertinoIcons.plus_circled,
color: CupertinoColors.activeBlue,
semanticLabel: 'Add', semanticLabel: 'Add',
), ),
onPressed: () { }, onPressed: () { },
@ -249,7 +244,6 @@ class Tab1RowItem extends StatelessWidget {
CupertinoButton( CupertinoButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.share, child: const Icon(CupertinoIcons.share,
color: CupertinoColors.activeBlue,
semanticLabel: 'Share', semanticLabel: 'Share',
), ),
onPressed: () { }, onPressed: () { },
@ -352,8 +346,7 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
CupertinoButton( CupertinoButton.filled(
color: CupertinoColors.activeBlue,
minSize: 30.0, minSize: 30.0,
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32.0),
@ -367,12 +360,11 @@ class Tab1ItemPageState extends State<Tab1ItemPage> {
), ),
onPressed: () { }, onPressed: () { },
), ),
CupertinoButton( CupertinoButton.filled(
color: CupertinoColors.activeBlue,
minSize: 30.0, minSize: 30.0,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(32.0),
child: const Icon(CupertinoIcons.ellipsis, color: CupertinoColors.white), child: const Icon(CupertinoIcons.ellipsis),
onPressed: () { }, onPressed: () { },
), ),
], ],
@ -722,7 +714,11 @@ class CupertinoDemoTab3 extends StatelessWidget {
trailing: trailingButtons, trailing: trailingButtons,
), ),
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)), decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: ListView( child: ListView(
children: <Widget>[ children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)), const Padding(padding: EdgeInsets.only(top: 32.0)),
@ -736,9 +732,9 @@ class CupertinoDemoTab3 extends StatelessWidget {
); );
}, },
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: CupertinoColors.white, color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: Border( border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0), top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: 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, top: false,
bottom: false, bottom: false,
child: Row( child: Row(
children: const <Widget>[ children: <Widget>[
Text( Text(
'Sign in', '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), color: Color(0xFF646464),
), ),
const Padding(padding: EdgeInsets.only(top: 18.0)), const Padding(padding: EdgeInsets.only(top: 18.0)),
CupertinoButton( CupertinoButton.filled(
color: CupertinoColors.activeBlue,
child: const Text('Sign in'), child: const Text('Sign in'),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);

View file

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../gallery/demo.dart'; import '../../gallery/demo.dart';
@ -34,9 +33,9 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
Widget _buildMenu(List<Widget> children) { Widget _buildMenu(List<Widget> children) {
return Container( return Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: CupertinoColors.white, color: CupertinoTheme.of(context).scaffoldBackgroundColor,
border: Border( border: const Border(
top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0), top: BorderSide(color: Color(0xFFBCBBC1), width: 0.0),
bottom: 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( child: SafeArea(
top: false, top: false,
bottom: false, bottom: false,
child: DefaultTextStyle( child: Row(
style: const TextStyle( mainAxisAlignment: MainAxisAlignment.spaceBetween,
letterSpacing: -0.24, children: children,
fontSize: 17.0,
color: CupertinoColors.black,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
), ),
), ),
), ),
@ -249,19 +241,23 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
appBar: AppBar( navigationBar: CupertinoNavigationBar(
title: const Text('Cupertino Picker'), middle: const Text('Picker'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoPickerDemo.routeName)], // 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( child: DefaultTextStyle(
style: const TextStyle( style: CupertinoTheme.of(context).textTheme.textStyle,
fontFamily: '.SF UI Text',
fontSize: 17.0,
color: CupertinoColors.black,
),
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)), decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: ListView( child: ListView(
children: <Widget>[ children: <Widget>[
const Padding(padding: EdgeInsets.only(top: 32.0)), const Padding(padding: EdgeInsets.only(top: 32.0)),

View file

@ -39,19 +39,21 @@ class _CupertinoRefreshControlDemoState extends State<CupertinoRefreshControlDem
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTextStyle( return DefaultTextStyle(
style: const TextStyle( style: CupertinoTheme.of(context).textTheme.textStyle,
fontFamily: '.SF UI Text',
inherit: false,
fontSize: 17.0,
color: CupertinoColors.black,
),
child: CupertinoPageScaffold( child: CupertinoPageScaffold(
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0xFFEFEFF4)), decoration: BoxDecoration(
color: CupertinoTheme.of(context).brightness == Brightness.light
? CupertinoColors.extraLightBackgroundGray
: CupertinoColors.darkBackgroundGray,
),
child: CustomScrollView( child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverNavigationBar( 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', previousPageTitle: 'Cupertino',
trailing: CupertinoDemoDocumentationButton(CupertinoRefreshControlDemo.routeName), trailing: CupertinoDemoDocumentationButton(CupertinoRefreshControlDemo.routeName),
), ),
@ -152,7 +154,7 @@ class _ListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
color: CupertinoColors.white, color: CupertinoTheme.of(context).scaffoldBackgroundColor,
height: 60.0, height: 60.0,
padding: const EdgeInsets.only(top: 9.0), padding: const EdgeInsets.only(top: 9.0),
child: Row( child: Row(
@ -215,11 +217,11 @@ class _ListItem extends StatelessWidget {
letterSpacing: -0.41, letterSpacing: -0.41,
), ),
), ),
const Padding( Padding(
padding: EdgeInsets.only(left: 9.0), padding: const EdgeInsets.only(left: 9.0),
child: Icon( child: Icon(
CupertinoIcons.info, CupertinoIcons.info,
color: CupertinoColors.activeBlue color: CupertinoTheme.of(context).primaryColor,
), ),
), ),
], ],

View file

@ -50,68 +50,77 @@ class _CupertinoSegmentedControlDemoState extends State<CupertinoSegmentedContro
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
appBar: AppBar( navigationBar: CupertinoNavigationBar(
title: const Text('Segmented Control'), middle: const Text('Segmented Control'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSegmentedControlDemo.routeName)], // 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( child: DefaultTextStyle(
children: <Widget>[ style: CupertinoTheme.of(context).textTheme.textStyle,
const Padding( child: SafeArea(
padding: EdgeInsets.all(16.0), child: Column(
), children: <Widget>[
SizedBox( const Padding(
width: 500.0, padding: EdgeInsets.all(16.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: Container( SizedBox(
padding: const EdgeInsets.symmetric( width: 500.0,
vertical: 64.0, child: CupertinoSegmentedControl<int>(
horizontal: 16.0, 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. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart'; import '../../gallery/demo.dart';
@ -20,49 +19,58 @@ class _CupertinoSliderDemoState extends State<CupertinoSliderDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
appBar: AppBar( navigationBar: CupertinoNavigationBar(
title: const Text('Cupertino Sliders'), middle: const Text('Sliders'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSliderDemo.routeName)], // 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: DefaultTextStyle(
child: Column( style: CupertinoTheme.of(context).textTheme.textStyle,
mainAxisAlignment: MainAxisAlignment.spaceAround, child: SafeArea(
children: <Widget>[ child: Center(
Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget> [ children: <Widget>[
CupertinoSlider( Column(
value: _value, mainAxisSize: MainAxisSize.min,
min: 0.0, children: <Widget> [
max: 100.0, CupertinoSlider(
onChanged: (double value) { value: _value,
setState(() { min: 0.0,
_value = value; 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> [
Column( CupertinoSlider(
mainAxisSize: MainAxisSize.min, value: _discreteValue,
children: <Widget> [ min: 0.0,
CupertinoSlider( max: 100.0,
value: _discreteValue, divisions: 5,
min: 0.0, onChanged: (double value) {
max: 100.0, setState(() {
divisions: 5, _discreteValue = value;
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. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery/demo.dart'; import '../../gallery/demo.dart';
@ -20,62 +19,71 @@ class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return CupertinoPageScaffold(
appBar: AppBar( navigationBar: CupertinoNavigationBar(
title: const Text('Cupertino Switch'), middle: const Text('Switch'),
actions: <Widget>[MaterialDemoDocumentationButton(CupertinoSwitchDemo.routeName)], // 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: DefaultTextStyle(
child: Column( style: CupertinoTheme.of(context).textTheme.textStyle,
mainAxisAlignment: MainAxisAlignment.spaceAround, child: SafeArea(
children: <Widget>[ child: Center(
Semantics( child: Column(
container: true, mainAxisAlignment: MainAxisAlignment.spaceAround,
child: Column( children: <Widget>[
children: <Widget>[ Semantics(
CupertinoSwitch( container: true,
value: _switchValue, child: Column(
onChanged: (bool value) { children: <Widget>[
setState(() { CupertinoSwitch(
_switchValue = value; 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( child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar( 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', previousPageTitle: 'Cupertino',
middle: Text('Text Fields'), middle: Text('Text Fields'),
), ),

View file

@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation; import 'package:flutter/scheduler.dart' show timeDilation;
@ -125,7 +126,6 @@ class _GalleryAppState extends State<GalleryApp> {
child: home, child: home,
); );
} }
return MaterialApp( return MaterialApp(
theme: _options.theme.data.copyWith(platform: _options.platform), theme: _options.theme.data.copyWith(platform: _options.platform),
title: 'Flutter Gallery', title: 'Flutter Gallery',
@ -137,7 +137,17 @@ class _GalleryAppState extends State<GalleryApp> {
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return Directionality( return Directionality(
textDirection: _options.textDirection, 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, home: home,

View file

@ -30,5 +30,7 @@ export 'src/cupertino/tab_scaffold.dart';
export 'src/cupertino/tab_view.dart'; export 'src/cupertino/tab_view.dart';
export 'src/cupertino/text_field.dart'; export 'src/cupertino/text_field.dart';
export 'src/cupertino/text_selection.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 'src/cupertino/thumb_painter.dart';
export 'widgets.dart'; export 'widgets.dart';

View file

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

View file

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

View file

@ -5,28 +5,11 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'theme.dart';
const Color _kDisabledBackground = Color(0xFFA9A9A9); const Color _kDisabledBackground = Color(0xFFA9A9A9);
const Color _kDisabledForeground = Color(0xFFC4C4C4); // Measured against iOS 12 in Xcode.
const Color _kDisabledForeground = Color(0xFFD1D1D1);
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,
);
const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0); const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric( const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
@ -53,7 +36,26 @@ class CupertinoButton extends StatefulWidget {
this.pressedOpacity = 0.1, this.pressedOpacity = 0.1,
this.borderRadius = const BorderRadius.all(Radius.circular(8.0)), this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
@required this.onPressed, @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. /// The widget below this widget in the tree.
/// ///
@ -68,6 +70,9 @@ class CupertinoButton extends StatefulWidget {
/// The color of the button's background. /// The color of the button's background.
/// ///
/// Defaults to null which produces a button with no background or border. /// 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; final Color color;
/// The color of the button's background when the button is disabled. /// 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. /// Defaults to round corners of 8 logical pixels.
final BorderRadius borderRadius; final BorderRadius borderRadius;
final bool _filled;
/// Whether the button is enabled or disabled. Buttons are disabled by default. To /// 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. /// enable a button, set its [onPressed] property to a non-null value.
bool get enabled => onPressed != null; bool get enabled => onPressed != null;
@ -198,7 +205,15 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool enabled = widget.enabled; 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( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@ -232,12 +247,11 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
widthFactor: 1.0, widthFactor: 1.0,
heightFactor: 1.0, heightFactor: 1.0,
child: DefaultTextStyle( child: DefaultTextStyle(
style: backgroundColor != null style: textStyle,
? _kBackgroundButtonTextStyle child: IconTheme(
: enabled data: IconThemeData(color: foregroundColor),
? _kButtonTextStyle child: widget.child,
: _kDisabledButtonTextStyle, ),
child: widget.child,
), ),
), ),
), ),

View file

@ -11,15 +11,27 @@ class CupertinoColors {
/// iOS 10's default blue color. Used to indicate active elements such as /// iOS 10's default blue color. Used to indicate active elements such as
/// buttons, selected tabs and your own chat bubbles. /// buttons, selected tabs and your own chat bubbles.
///
/// This is SystemBlue in the iOS palette.
static const Color activeBlue = Color(0xFF007AFF); static const Color activeBlue = Color(0xFF007AFF);
/// iOS 10's default green color. Used to indicate active accents such as /// 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 /// the switch in its on state and some accent buttons such as the call button
/// and Apple Map's 'Go' button. /// and Apple Map's 'Go' button.
///
/// This is SystemGreen in the iOS palette.
static const Color activeGreen = Color(0xFF4CD964); 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. /// Opaque white color. Used for backgrounds and fonts against dark backgrounds.
/// ///
/// This is SystemWhiteColor in the iOS palette.
///
/// See also: /// See also:
/// ///
/// * [material.Colors.white], the same color, in the material design palette. /// * [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. /// Opaque black color. Used for texts against light backgrounds.
/// ///
/// This is SystemBlackColor in the iOS palette.
///
/// See also: /// See also:
/// ///
/// * [material.Colors.black], the same color, in the material design palette. /// * [material.Colors.black], the same color, in the material design palette.
@ -35,12 +49,26 @@ class CupertinoColors {
static const Color black = Color(0xFF000000); static const Color black = Color(0xFF000000);
/// Used in iOS 10 for light background fills such as the chat bubble background. /// 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); 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 /// Used in iOS 11 for unselected selectables such as tab bar items in their
/// inactive state or de-emphasized subtitles and details text. /// inactive state or de-emphasized subtitles and details text.
/// ///
/// Not the same gray as disabled buttons etc. /// Not the same gray as disabled buttons etc.
///
/// This is SystemGrayColor in the iOS palette.
static const Color inactiveGray = Color(0xFF8E8E93); static const Color inactiveGray = Color(0xFF8E8E93);
/// Used for iOS 10 for destructive actions such as the delete actions in /// 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 /// Not the same red as the camera shutter or springboard icon notifications
/// or the foreground red theme in various native apps such as HealthKit. /// 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); static const Color destructiveRed = Color(0xFFFF3B30);
} }

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'theme.dart';
import 'thumb_painter.dart'; import 'thumb_painter.dart';
// Examples can assume: // 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. /// 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; final Color activeColor;
@override @override
@ -228,7 +228,7 @@ class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderSt
return _CupertinoSliderRenderObjectWidget( return _CupertinoSliderRenderObjectWidget(
value: (widget.value - widget.min) / (widget.max - widget.min), value: (widget.value - widget.min) / (widget.max - widget.min),
divisions: widget.divisions, divisions: widget.divisions,
activeColor: widget.activeColor ?? CupertinoColors.activeBlue, activeColor: widget.activeColor ?? CupertinoTheme.of(context).primaryColor,
onChanged: widget.onChanged != null ? _handleChanged : null, onChanged: widget.onChanged != null ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
@ -485,15 +485,14 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
final double trackActive = offset.dx + _thumbCenter; final double trackActive = offset.dx + _thumbCenter;
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
final Paint paint = Paint();
if (visualPosition > 0.0) { 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); canvas.drawRRect(RRect.fromLTRBXY(trackLeft, trackTop, trackActive, trackBottom, 1.0, 1.0), paint);
} }
if (visualPosition < 1.0) { 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); 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. /// 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; final Color activeColor;
@override @override

View file

@ -4,6 +4,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'bottom_tab_bar.dart'; import 'bottom_tab_bar.dart';
import 'theme.dart';
/// Implements a tabbed iOS application's root layout and behavior structure. /// 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. // TODO(xster): Use real size after partial layout instead of preferred size.
// https://github.com/flutter/flutter/issues/12912 // https://github.com/flutter/flutter/issues/12912
final double bottomPadding = widget.tabBar.preferredSize.height final double bottomPadding =
+ existingMediaQuery.padding.bottom; widget.tabBar.preferredSize.height + existingMediaQuery.padding.bottom;
// If tab bar opaque, directly stop the main content higher. If // If tab bar opaque, directly stop the main content higher. If
// translucent, let main content draw behind the tab bar but hint the // translucent, let main content draw behind the tab bar but hint the
// obstructed area. // obstructed area.
if (widget.tabBar.opaque) { if (widget.tabBar.opaque(context)) {
content = Padding( content = Padding(
padding: EdgeInsets.only(bottom: bottomPadding), padding: EdgeInsets.only(bottom: bottomPadding),
child: content, child: content,
@ -219,8 +220,13 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
)); ));
} }
return Stack( return DecoratedBox(
children: stacked, 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 'colors.dart';
import 'icons.dart'; import 'icons.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization; export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization;
@ -32,15 +33,6 @@ const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4.0)), 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. // Value extracted via color reader from iOS simulator.
const Color _kSelectionHighlightColor = Color(0x667FAACF); const Color _kSelectionHighlightColor = Color(0x667FAACF);
const Color _kInactiveTextColor = Color(0xFFC2C2C2); const Color _kInactiveTextColor = Color(0xFFC2C2C2);
@ -160,7 +152,7 @@ class CupertinoTextField extends StatefulWidget {
TextInputType keyboardType, TextInputType keyboardType,
this.textInputAction, this.textInputAction,
this.textCapitalization = TextCapitalization.none, this.textCapitalization = TextCapitalization.none,
this.style = _kDefaultTextStyle, this.style,
this.textAlign = TextAlign.start, this.textAlign = TextAlign.start,
this.autofocus = false, this.autofocus = false,
this.obscureText = false, this.obscureText = false,
@ -271,7 +263,7 @@ class CupertinoTextField extends StatefulWidget {
/// ///
/// Also serves as a base for the [placeholder] text's style. /// 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; final TextStyle style;
/// {@macro flutter.widgets.editableText.textAlign} /// {@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 // If there are no surrounding widgets, just return the core editable text
// part. // part.
if (widget.placeholder == null && if (widget.placeholder == null &&
@ -593,7 +587,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
widget.placeholder, widget.placeholder,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: widget.style.merge( style: textStyle.merge(
const TextStyle( const TextStyle(
color: _kInactiveTextColor, color: _kInactiveTextColor,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
@ -637,14 +631,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
final Brightness keyboardAppearance = widget.keyboardAppearance;
final TextEditingController controller = _effectiveController; final TextEditingController controller = _effectiveController;
final FocusNode focusNode = _effectiveFocusNode;
final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[]; final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
final bool enabled = widget.enabled ?? true; final bool enabled = widget.enabled ?? true;
if (widget.maxLength != null && widget.maxLengthEnforced) { if (widget.maxLength != null && widget.maxLengthEnforced) {
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength)); formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
} }
final TextStyle textStyle = widget.style ?? CupertinoTheme.of(context).textTheme.textStyle;
final Widget paddedEditable = Padding( final Widget paddedEditable = Padding(
padding: widget.padding, padding: widget.padding,
@ -652,11 +645,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
child: EditableText( child: EditableText(
key: _editableTextKey, key: _editableTextKey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction, textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
style: widget.style, style: textStyle,
textAlign: widget.textAlign, textAlign: widget.textAlign,
autofocus: widget.autofocus, autofocus: widget.autofocus,
obscureText: widget.obscureText, obscureText: widget.obscureText,
@ -674,7 +667,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
backgroundCursorColor: CupertinoColors.inactiveGray, backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding, scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance, keyboardAppearance: widget.keyboardAppearance,
), ),
), ),
); );
@ -692,14 +685,18 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
decoration: widget.decoration, decoration: widget.decoration,
// The main decoration and the disabled scrim exists separately. // The main decoration and the disabled scrim exists separately.
child: Container( child: Container(
color: enabled ? null : _kDisabledBackground, color: enabled
? null
: CupertinoTheme.of(context).brightness == Brightness.light
? _kDisabledBackground
: CupertinoColors.darkBackgroundGray,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTapDown: _handleTapDown, onTapDown: _handleTapDown,
onTapUp: _handleTapUp, onTapUp: _handleTapUp,
onLongPress: _handleLongPress, onLongPress: _handleLongPress,
excludeFromSemantics: true, 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, mini: true,
); );
}, },
) ),
); );
assert(() { assert(() {

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -143,7 +144,15 @@ class Theme extends StatelessWidget {
theme: this, theme: this,
child: IconTheme( child: IconTheme(
data: data.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 'dart:ui' show Color, hashValues;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -153,6 +154,7 @@ class ThemeData extends Diagnosticable {
ColorScheme colorScheme, ColorScheme colorScheme,
DialogTheme dialogTheme, DialogTheme dialogTheme,
Typography typography, Typography typography,
CupertinoThemeData cupertinoOverrideTheme
}) { }) {
brightness ??= Brightness.light; brightness ??= Brightness.light;
final bool isDark = brightness == Brightness.dark; final bool isDark = brightness == Brightness.dark;
@ -246,6 +248,7 @@ class ThemeData extends Diagnosticable {
labelStyle: textTheme.body2, labelStyle: textTheme.body2,
); );
dialogTheme ??= const DialogTheme(); dialogTheme ??= const DialogTheme();
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
return ThemeData.raw( return ThemeData.raw(
brightness: brightness, brightness: brightness,
@ -294,11 +297,13 @@ class ThemeData extends Diagnosticable {
colorScheme: colorScheme, colorScheme: colorScheme,
dialogTheme: dialogTheme, dialogTheme: dialogTheme,
typography: typography, typography: typography,
cupertinoOverrideTheme: cupertinoOverrideTheme,
); );
} }
/// Create a [ThemeData] given a set of exact values. All the values /// Create a [ThemeData] given a set of exact values. All the values must be
/// must be specified. /// specified. They all must also be non-null except for
/// [cupertinoOverrideTheme].
/// ///
/// This will rarely be used directly. It is used by [lerp] to /// This will rarely be used directly. It is used by [lerp] to
/// create intermediate themes based on two themes created with the /// create intermediate themes based on two themes created with the
@ -353,6 +358,7 @@ class ThemeData extends Diagnosticable {
@required this.colorScheme, @required this.colorScheme,
@required this.dialogTheme, @required this.dialogTheme,
@required this.typography, @required this.typography,
@required this.cupertinoOverrideTheme,
}) : assert(brightness != null), }) : assert(brightness != null),
assert(primaryColor != null), assert(primaryColor != null),
assert(primaryColorBrightness != null), assert(primaryColorBrightness != null),
@ -630,6 +636,18 @@ class ThemeData extends Diagnosticable {
/// [primaryTextTheme], and [accentTextTheme]. /// [primaryTextTheme], and [accentTextTheme].
final Typography typography; 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. /// Creates a copy of this theme but with the given fields replaced with the new values.
ThemeData copyWith({ ThemeData copyWith({
Brightness brightness, Brightness brightness,
@ -678,7 +696,9 @@ class ThemeData extends Diagnosticable {
ColorScheme colorScheme, ColorScheme colorScheme,
DialogTheme dialogTheme, DialogTheme dialogTheme,
Typography typography, Typography typography,
CupertinoThemeData cupertinoOverrideTheme,
}) { }) {
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
return ThemeData.raw( return ThemeData.raw(
brightness: brightness ?? this.brightness, brightness: brightness ?? this.brightness,
primaryColor: primaryColor ?? this.primaryColor, primaryColor: primaryColor ?? this.primaryColor,
@ -726,6 +746,7 @@ class ThemeData extends Diagnosticable {
colorScheme: colorScheme ?? this.colorScheme, colorScheme: colorScheme ?? this.colorScheme,
dialogTheme: dialogTheme ?? this.dialogTheme, dialogTheme: dialogTheme ?? this.dialogTheme,
typography: typography ?? this.typography, typography: typography ?? this.typography,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
); );
} }
@ -853,6 +874,7 @@ class ThemeData extends Diagnosticable {
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t), colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t), dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
typography: Typography.lerp(a.typography, b.typography, 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.pageTransitionsTheme == pageTransitionsTheme) &&
(otherData.colorScheme == colorScheme) && (otherData.colorScheme == colorScheme) &&
(otherData.dialogTheme == dialogTheme) && (otherData.dialogTheme == dialogTheme) &&
(otherData.typography == typography); (otherData.typography == typography) &&
(otherData.cupertinoOverrideTheme == cupertinoOverrideTheme);
} }
@override @override
@ -967,6 +990,7 @@ class ThemeData extends Diagnosticable {
colorScheme, colorScheme,
dialogTheme, dialogTheme,
typography, typography,
cupertinoOverrideTheme,
), ),
), ),
); );
@ -1019,6 +1043,109 @@ class ThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme)); properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme)); properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography)); 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: // Proof by contradiction:
// //
// If we introduce a loop, this retained layer must be appended to one of // 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 // changed so A's _needsAddToScene is true. This contradicts
// _subtreeNeedsAddToScene being false. // _subtreeNeedsAddToScene being false.
if (!_subtreeNeedsAddToScene && _engineLayer != null) { if (!_subtreeNeedsAddToScene && _engineLayer != null) {

View file

@ -46,7 +46,7 @@ import 'framework.dart';
/// ///
/// When the inherited model is rebuilt the [updateShouldNotify] and /// When the inherited model is rebuilt the [updateShouldNotify] and
/// [updateShouldNotifyDependent] methods are used to decide what /// [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 /// inherited model's [updateShouldNotifyDependent] method is tested for
/// each dependent and the set of aspect objects it depends on. /// each dependent and the set of aspect objects it depends on.
/// The [updateShouldNotifyDependent] method must compare the set of aspect /// 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 /// If [aspect] is null this method is the same as
/// `context.inheritFromWidgetOfExactType(T)`. /// `context.inheritFromWidgetOfExactType(T)`.
///
/// If no ancestor of type T exists, null is returned.
static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) { static T inheritFrom<T extends InheritedModel<Object>>(BuildContext context, { Object aspect }) {
if (aspect == null) if (aspect == null)
return context.inheritFromWidgetOfExactType(T); 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 // Create a dependency on all of the type T ancestor models up until
// a model is found for which isSupportedAspect(aspect) is true. // a model is found for which isSupportedAspect(aspect) is true.
final List<InheritedElement> models = _findModels<T>(context, aspect).toList(); final List<InheritedElement> models = _findModels<T>(context, aspect).toList();
if (models.isEmpty) {
return null;
}
final InheritedElement lastModel = models.last; final InheritedElement lastModel = models.last;
for (InheritedElement model in models) { for (InheritedElement model in models) {
final T value = context.inheritFromElement(model, aspect: aspect); 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] /// * [AnimatedBuilder], which also triggers rebuilds from a [Listenable]
/// without passing back a specific value from a [ValueListenable]. /// without passing back a specific value from a [ValueListenable].
/// * [NotificationListener], which lets you rebuild based on [Notification] /// * [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. /// you have a direct reference to.
/// * [StreamBuilder], where a builder can depend on a [Stream] rather than /// * [StreamBuilder], where a builder can depend on a [Stream] rather than
/// a [ValueListenable] for more advanced use cases. /// a [ValueListenable] for more advanced use cases.

View file

@ -68,6 +68,70 @@ void main() {
expect(actualActive.text.style.color, const Color(0xFF123456)); 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 { testWidgets('Use active icon', (WidgetTester tester) async {
const TestImageProvider activeIcon = TestImageProvider(16, 16); const TestImageProvider activeIcon = TestImageProvider(16, 16);
const TestImageProvider inactiveIcon = TestImageProvider(24, 24); const TestImageProvider inactiveIcon = TestImageProvider(24, 24);

View file

@ -12,6 +12,7 @@ import '../widgets/semantics_tester.dart';
const TextStyle testStyle = TextStyle( const TextStyle testStyle = TextStyle(
fontFamily: 'Ahem', fontFamily: 'Ahem',
fontSize: 10.0, fontSize: 10.0,
letterSpacing: 0.0,
); );
void main() { void main() {
@ -226,6 +227,80 @@ void main() {
expect(boxDecoration.color, const Color(0x00FF00)); 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 }) { Widget boilerplate({ Widget child }) {

View file

@ -148,15 +148,62 @@ void main() {
expect(tester.getCenter(find.text('Title')).dx, 400.0); 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; count = 0x000000;
await tester.pumpWidget( await tester.pumpWidget(
const CupertinoApp( CupertinoApp(
home: CupertinoNavigationBar( home: CupertinoNavigationBar(
leading: _ExpectStyles(color: Color(0xFF001122), index: 0x000001), leading: CupertinoButton(
middle: _ExpectStyles(color: Color(0xFF000000), letterSpacing: -0.08, index: 0x000100), onPressed: () {},
trailing: _ExpectStyles(color: Color(0xFF001122), index: 0x010000), child: const _ExpectStyles(color: CupertinoColors.activeBlue, index: 0x000001),
actionsForegroundColor: Color(0xFF001122), ),
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 { class _ExpectStyles extends StatelessWidget {
const _ExpectStyles({ this.color, this.letterSpacing, this.index }); const _ExpectStyles({ this.color, this.index });
final Color color; final Color color;
final double letterSpacing;
final int index; final int index;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextStyle style = DefaultTextStyle.of(context).style; final TextStyle style = DefaultTextStyle.of(context).style;
expect(style.color, color); expect(style.color, color);
expect(style.fontFamily, '.SF Pro Text');
expect(style.fontSize, 17.0); expect(style.fontSize, 17.0);
expect(style.letterSpacing, letterSpacing ?? -0.24); expect(style.letterSpacing, -0.41);
count += index; count += index;
return Container(); return Container();
} }

View file

@ -13,9 +13,11 @@ Future<void> startTransitionBetween(
String fromTitle, String fromTitle,
String toTitle, String toTitle,
TextDirection textDirection = TextDirection.ltr, TextDirection textDirection = TextDirection.ltr,
CupertinoThemeData theme,
}) async { }) async {
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
theme: theme,
builder: (BuildContext context, Widget navigator) { builder: (BuildContext context, Widget navigator) {
return Directionality( return Directionality(
textDirection: textDirection, textDirection: textDirection,
@ -79,19 +81,15 @@ Finder flying(WidgetTester tester, Finder finder) {
}); });
assert( assert(
find find.descendant(
.descendant( of: lastOverlayFinder,
of: lastOverlayFinder, matching: find.byWidgetPredicate(
matching: find.byWidgetPredicate( (Widget widget) =>
(Widget widget) => widget.runtimeType.toString() == '_NavigationBarTransition',
widget.runtimeType.toString() == ),
'_NavigationBarTransition', ).evaluate().length == 1,
), 'The last overlay in the navigator was not a flying hero',
) );
.evaluate()
.length ==
1,
'The last overlay in the navigator was not a flying hero',);
return find.descendant( return find.descendant(
of: lastOverlayFinder, of: lastOverlayFinder,
@ -103,28 +101,25 @@ void checkBackgroundBoxHeight(WidgetTester tester, double height) {
final Widget transitionBackgroundBox = final Widget transitionBackgroundBox =
tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0]; tester.widget<Stack>(flying(tester, find.byType(Stack))).children[0];
expect( expect(
tester tester.widget<SizedBox>(
.widget<SizedBox>( find.descendant(
find.descendant( of: find.byWidget(transitionBackgroundBox),
of: find.byWidget(transitionBackgroundBox), matching: find.byType(SizedBox),
matching: find.byType(SizedBox), ),
), ).height,
)
.height,
height, height,
); );
} }
void checkOpacity(WidgetTester tester, Finder finder, double opacity) { void checkOpacity(WidgetTester tester, Finder finder, double opacity) {
expect( expect(
tester tester.renderObject<RenderAnimatedOpacity>(
.renderObject<RenderAnimatedOpacity>(find.ancestor( find.ancestor(
of: finder, of: finder,
matching: find.byType(FadeTransition), matching: find.byType(FadeTransition),
)) ),
.opacity ).opacity.value,
.value, moreOrLessEquals(opacity, epsilon: 0.000000001),
opacity,
); );
} }
@ -144,11 +139,11 @@ void main() {
// place. // place.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first), tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(331.0724935531616, 13.5), const Offset(332.0129337310791, 13.5),
); );
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last), 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. // Same as LTR but more to the right now.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first), tester.getTopLeft(flying(tester, find.text('Page 1')).first),
const Offset(366.9275064468384, 13.5), const Offset(367.9870662689209, 13.5),
); );
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last), 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); tester.renderObject(flying(tester, find.text('Page 1')).first);
expect(bottomMiddle.text.style.color, const Color(0xFF00070F)); expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w600); expect(bottomMiddle.text.style.fontWeight, FontWeight.w600);
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text'); expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.08952957153320312); expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity( checkOpacity(
tester, flying(tester, find.text('Page 1')).first, 0.8609542846679688); 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); tester.renderObject(flying(tester, find.text('Page 1')).last);
expect(topBackLabel.text.style.color, const Color(0xFF00070F)); expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(topBackLabel.text.style.fontWeight, FontWeight.w600); expect(topBackLabel.text.style.fontWeight, FontWeight.w600);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text'); expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.08952957153320312); expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0); checkOpacity(tester, flying(tester, find.text('Page 1')).last, 0.0);
@ -208,20 +203,69 @@ void main() {
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(bottomMiddle.text.style.color, const Color(0xFF0073F0)); expect(bottomMiddle.text.style.color, const Color(0xFF0073F0));
expect(bottomMiddle.text.style.fontWeight, FontWeight.w400); expect(bottomMiddle.text.style.fontWeight, FontWeight.w400);
expect(bottomMiddle.text.style.fontFamily, '.SF UI Text'); expect(bottomMiddle.text.style.fontFamily, '.SF Pro Text');
expect(bottomMiddle.text.style.letterSpacing, -0.231169798374176); expect(bottomMiddle.text.style.letterSpacing, -0.41);
checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0); checkOpacity(tester, flying(tester, find.text('Page 1')).first, 0.0);
expect(topBackLabel.text.style.color, const Color(0xFF0073F0)); expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
expect(topBackLabel.text.style.fontWeight, FontWeight.w400); expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text'); expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.231169798374176); expect(topBackLabel.text.style.letterSpacing, -0.41);
checkOpacity( checkOpacity(
tester, flying(tester, find.text('Page 1')).last, 0.8733493089675903); 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', testWidgets('Fullscreen dialogs do not create heroes',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
@ -295,7 +339,7 @@ void main() {
expect(bottomMiddle.text.style.color, const Color(0xFF00070F)); expect(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first), 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 // 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(topBackLabel.text.style.color, const Color(0xFF00070F));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last), 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(bottomMiddle.text.style.color, const Color(0xFF00070F));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).first), 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 // 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(topBackLabel.text.style.color, const Color(0xFF00070F));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 1')).last), 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); checkOpacity(tester, flying(tester, find.text('custom')), 0.8393326997756958);
expect( expect(
tester.getTopLeft(flying(tester, find.text('custom'))), 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)); await tester.pump(const Duration(milliseconds: 150));
checkOpacity(tester, flying(tester, find.text('custom')), 0.0); checkOpacity(tester, flying(tester, find.text('custom')), 0.0);
expect( expect(
tester.getTopLeft(flying(tester, find.text('custom'))), 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.color, const Color(0xFF00070F));
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w700); expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w700);
expect(bottomLargeTitle.text.style.fontFamily, '.SF Pro Display'); 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. // The top back label is styled exactly the same way.
final RenderParagraph topBackLabel = final RenderParagraph topBackLabel =
@ -923,19 +967,19 @@ void main() {
expect(topBackLabel.text.style.color, const Color(0xFF00070F)); expect(topBackLabel.text.style.color, const Color(0xFF00070F));
expect(topBackLabel.text.style.fontWeight, FontWeight.w700); expect(topBackLabel.text.style.fontWeight, FontWeight.w700);
expect(topBackLabel.text.style.fontFamily, '.SF Pro Display'); 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. // Move animation further a bit.
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(bottomLargeTitle.text.style.color, const Color(0xFF0073F0)); expect(bottomLargeTitle.text.style.color, const Color(0xFF0073F0));
expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w400); expect(bottomLargeTitle.text.style.fontWeight, FontWeight.w400);
expect(bottomLargeTitle.text.style.fontFamily, '.SF UI Text'); expect(bottomLargeTitle.text.style.fontFamily, '.SF Pro Text');
expect(bottomLargeTitle.text.style.letterSpacing, -0.2135093951225281); expect(bottomLargeTitle.text.style.letterSpacing, -0.3647452166676521);
expect(topBackLabel.text.style.color, const Color(0xFF0073F0)); expect(topBackLabel.text.style.color, const Color(0xFF0073F0));
expect(topBackLabel.text.style.fontWeight, FontWeight.w400); expect(topBackLabel.text.style.fontWeight, FontWeight.w400);
expect(topBackLabel.text.style.fontFamily, '.SF UI Text'); expect(topBackLabel.text.style.fontFamily, '.SF Pro Text');
expect(topBackLabel.text.style.letterSpacing, -0.2135093951225281); expect(topBackLabel.text.style.letterSpacing, -0.3647452166676521);
}); });
testWidgets('Top middle fades in and slides in from the right', 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); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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)); await tester.pump(const Duration(milliseconds: 150));
@ -960,7 +1004,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703);
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.0);
expect( expect(
tester.getTopRight(flying(tester, find.text('Page 2'))), 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)); await tester.pump(const Duration(milliseconds: 150));
@ -987,7 +1031,7 @@ void main() {
checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703); checkOpacity(tester, flying(tester, find.text('Page 2')), 0.6972532719373703);
expect( expect(
tester.getTopRight(flying(tester, find.text('Page 2'))), 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. // Page 2, which is the middle of the top route, start to fly back to the right.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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 // Page 1 is in transition in 2 places. Once as the top back label and once
@ -1131,12 +1175,12 @@ void main() {
// Transition continues. // Transition continues.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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)); await tester.pump(const Duration(milliseconds: 50));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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)); 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. // Page 2, which is the middle of the top route, start to fly back to the right.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), tester.getTopLeft(flying(tester, find.text('Page 2'))),
const Offset(352.5802058875561, 13.5), const Offset(353.5802058875561, 13.5),
); );
await gesture.up(); await gesture.up();
@ -1180,12 +1224,12 @@ void main() {
// Transition continues from the point we let off. // Transition continues from the point we let off.
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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)); await tester.pump(const Duration(milliseconds: 50));
expect( expect(
tester.getTopLeft(flying(tester, find.text('Page 2'))), 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. // 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 { (WidgetTester tester) async {
try { try {
await tester.pumpWidget( await tester.pumpWidget(
@ -175,21 +175,6 @@ void main() {
} on AssertionError catch (e) { } on AssertionError catch (e) {
expect(e.toString(), contains('onValueChanged')); 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', testWidgets('Widgets have correct default text/icon styles, change correctly on selection',
@ -236,6 +221,52 @@ void main() {
expect(iconTheme.data.color, CupertinoColors.white); 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', testWidgets('SegmentedControl is correct when user provides custom colors',
(WidgetTester tester) async { (WidgetTester tester) async {
final Map<int, Widget> children = <int, Widget>{}; final Map<int, Widget> children = <int, Widget>{};
@ -1171,13 +1202,13 @@ void main() {
await tester.tap(find.text('B')); await tester.tap(find.text('B'));
await tester.pump(); await tester.pump();
expect(getBackgroundColor(tester, 0), const Color(0xff007aff)); expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), const Color(0x33007aff)); expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 3), CupertinoColors.white); expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 40)); await tester.pump(const Duration(milliseconds: 40));
expect(getBackgroundColor(tester, 0), const Color(0xff3d9aff)); expect(getBackgroundColor(tester, 0), CupertinoColors.white);
expect(getBackgroundColor(tester, 1), const Color(0x64007aff)); expect(getBackgroundColor(tester, 1), CupertinoColors.activeBlue);
expect(getBackgroundColor(tester, 3), CupertinoColors.white); expect(getBackgroundColor(tester, 3), CupertinoColors.white);
await tester.pump(const Duration(milliseconds: 150)); 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/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
@ -359,4 +360,57 @@ void main() {
handle.dispose(); 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(tabsPainted, <int>[0, 1, 0]);
expect(selectedTabs, <int>[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 }) { CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
@ -268,7 +326,6 @@ CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
title: Text('Tab 2'), title: Text('Tab 2'),
), ),
], ],
backgroundColor: CupertinoColors.white,
currentIndex: selectedTab, currentIndex: selectedTab,
onTap: (int newTab) => selectedTabs.add(newTab), onTap: (int newTab) => selectedTabs.add(newTab),
); );

View file

@ -1098,4 +1098,42 @@ void main() {
expect(find.byType(CupertinoButton), findsNWidgets(3)); 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. // found in the LICENSE file.
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/src/foundation/diagnostics.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)'); 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; int testBuildCalled;

View file

@ -190,6 +190,22 @@ void main() {
expect(find.text('a: 2 b: 2 c: 3'), findsOneWidget); 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 { testWidgets('Inner InheritedModel shadows the outer one', (WidgetTester tester) async {
int _a = 0; int _a = 0;
int _b = 1; int _b = 1;