mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Add a Material/Cupertino adaptive application example (#69)
This commit is contained in:
334
platform_design/lib/widgets.dart
Normal file
334
platform_design/lib/widgets.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A simple widget that builds different things on different platforms.
|
||||
class PlatformWidget extends StatelessWidget {
|
||||
const PlatformWidget({
|
||||
Key key,
|
||||
@required this.androidBuilder,
|
||||
@required this.iosBuilder,
|
||||
}) : assert(androidBuilder != null),
|
||||
assert(iosBuilder != null),
|
||||
super(key: key);
|
||||
|
||||
final WidgetBuilder androidBuilder;
|
||||
final WidgetBuilder iosBuilder;
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
return androidBuilder(context);
|
||||
case TargetPlatform.iOS:
|
||||
return iosBuilder(context);
|
||||
default:
|
||||
assert(false, 'Unexpected platform $defaultTargetPlatform');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A platform-agnostic card with a high elevation that reacts when tapped.
|
||||
///
|
||||
/// This is an example of a custom widget that an app developer might create for
|
||||
/// use on both iOS and Android as part of their brand's unique design.
|
||||
class PressableCard extends StatefulWidget {
|
||||
const PressableCard({
|
||||
this.onPressed,
|
||||
this.color,
|
||||
this.flattenAnimation,
|
||||
this.child,
|
||||
});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final Color color;
|
||||
final Animation<double> flattenAnimation;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => new _PressableCardState();
|
||||
}
|
||||
|
||||
class _PressableCardState extends State<PressableCard> with SingleTickerProviderStateMixin {
|
||||
bool pressed = false;
|
||||
AnimationController controller;
|
||||
Animation<double> elevationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 40),
|
||||
);
|
||||
elevationAnimation = controller.drive(CurveTween(curve: Curves.easeInOutCubic));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double get flatten => 1 - widget.flattenAnimation.value;
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return Listener(
|
||||
onPointerDown: (details) { if (widget.onPressed != null) { controller.forward(); } },
|
||||
onPointerUp: (details) { controller.reverse(); },
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (widget.onPressed != null) {
|
||||
widget.onPressed();
|
||||
}
|
||||
},
|
||||
// This widget both internally drives an animation when pressed and
|
||||
// responds to an external animation to flatten the card when in a
|
||||
// hero animation. You likely want to modularize them more in your own
|
||||
// app.
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([elevationAnimation, widget.flattenAnimation]),
|
||||
child: widget.child,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
// This is just a sample. You likely want to keep the math cleaner
|
||||
// in your own app.
|
||||
scale: 1 - elevationAnimation.value * 0.03,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16) * flatten,
|
||||
child: PhysicalModel(
|
||||
elevation: ((1 - elevationAnimation.value) * 10 + 10) * flatten,
|
||||
borderRadius: BorderRadius.circular(12 * flatten),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: widget.color,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A platform-agnostic card representing a song which can be in a card state,
|
||||
/// a flat state or anything in between.
|
||||
///
|
||||
/// When it's in a card state, it's pressable.
|
||||
///
|
||||
/// This is an example of a custom widget that an app developer might create for
|
||||
/// use on both iOS and Android as part of their brand's unique design.
|
||||
class HeroAnimatingSongCard extends StatelessWidget {
|
||||
HeroAnimatingSongCard({ this.song, this.color, this.heroAnimation, this.onPressed });
|
||||
|
||||
final String song;
|
||||
final Color color;
|
||||
final Animation<double> heroAnimation;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
double get playButtonSize => 50 + 50 * heroAnimation.value;
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
// This is an inefficient usage of AnimatedBuilder since it's rebuilding
|
||||
// the entire subtree instead of passing in a non-changing child and
|
||||
// building a transition widget in between.
|
||||
//
|
||||
// Left simple in this demo because this card doesn't have any real inner
|
||||
// content so this just rebuilds everything while animating.
|
||||
return AnimatedBuilder(
|
||||
animation: heroAnimation,
|
||||
builder: (context, child) {
|
||||
return PressableCard(
|
||||
onPressed: heroAnimation.value == 0 ? onPressed : null,
|
||||
color: color,
|
||||
flattenAnimation: heroAnimation,
|
||||
child: SizedBox(
|
||||
height: 250,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// The song title banner slides off in the hero animation.
|
||||
Positioned(
|
||||
bottom: - 80 * heroAnimation.value,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 80,
|
||||
color: Colors.black12,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
song,
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// The play button grows in the hero animation.
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 45) * (1 - heroAnimation.value),
|
||||
child: Container(
|
||||
height: playButtonSize,
|
||||
width: playButtonSize,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.black12,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.play_arrow, size: playButtonSize, color: Colors.black38),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A loading song tile's silhouette.
|
||||
///
|
||||
/// This is an example of a custom widget that an app developer might create for
|
||||
/// use on both iOS and Android as part of their brand's unique design.
|
||||
class SongPlaceholderTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 95,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.grey[400],
|
||||
width: 130,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 12),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 9,
|
||||
margin: EdgeInsets.only(right: 60),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Container(
|
||||
height: 9,
|
||||
margin: EdgeInsets.only(right: 20, top: 8),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Container(
|
||||
height: 9,
|
||||
margin: EdgeInsets.only(right: 40, top: 8),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Container(
|
||||
height: 9,
|
||||
margin: EdgeInsets.only(right: 80, top: 8),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
Container(
|
||||
height: 9,
|
||||
margin: EdgeInsets.only(right: 50, top: 8),
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Non-shared code below because different interfaces are shown to prompt
|
||||
// for a multiple-choice answer.
|
||||
//
|
||||
// This is a design choice and you may want to do something different in your
|
||||
// app.
|
||||
// ===========================================================================
|
||||
/// This uses a platform-appropriate mechanism to show users multiple choices.
|
||||
///
|
||||
/// On Android, it uses a dialog with radio buttons. On iOS, it uses a picker.
|
||||
void showChoices(BuildContext context, List<String> choices) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
int selectedRadio = 1;
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.only(top: 12),
|
||||
content: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List<Widget>.generate(choices.length, (index) {
|
||||
return RadioListTile(
|
||||
title: Text(choices[index]),
|
||||
value: index,
|
||||
groupValue: selectedRadio,
|
||||
onChanged: (value) {
|
||||
setState(() => selectedRadio = value);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('CANCEL'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
case TargetPlatform.iOS:
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SizedBox(
|
||||
height: 250,
|
||||
child: CupertinoPicker(
|
||||
useMagnifier: true,
|
||||
magnification: 1.1,
|
||||
itemExtent: 40,
|
||||
scrollController: FixedExtentScrollController(initialItem: 1),
|
||||
children: List<Widget>.generate(choices.length, (index) {
|
||||
return Center(child: Text(
|
||||
choices[index],
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
),
|
||||
));
|
||||
}),
|
||||
onSelectedItemChanged: (value) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
return;
|
||||
default:
|
||||
assert(false, 'Unexpected platform $defaultTargetPlatform');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user