// Copyright 2020 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.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, }) : 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 const SizedBox.shrink(); } } } /// 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, required this.color, required this.flattenAnimation, this.child, Key? key, }) : super(key: key); final VoidCallback? onPressed; final Color color; final Animation flattenAnimation; final Widget? child; @override State createState() => _PressableCardState(); } class _PressableCardState extends State with SingleTickerProviderStateMixin { bool pressed = false; late final AnimationController controller; late final Animation 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: () { widget.onPressed?.call(); }, // 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: const 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 { const HeroAnimatingSongCard({ required this.song, required this.color, required this.heroAnimation, this.onPressed, Key? key, }) : super(key: key); final String song; final Color color; final Animation 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: const EdgeInsets.symmetric(horizontal: 12), child: Text( song, style: const TextStyle( fontSize: 21, fontWeight: FontWeight.w500, ), ), ), ), // The play button grows in the hero animation. Padding( padding: const EdgeInsets.only(bottom: 45) * (1 - heroAnimation.value), child: Container( height: playButtonSize, width: playButtonSize, decoration: const 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 { const SongPlaceholderTile({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: 95, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), child: Row( children: [ Container( color: Theme.of(context).textTheme.bodyText2!.color, width: 130, ), const Padding( padding: EdgeInsets.only(left: 12), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 9, margin: const EdgeInsets.only(right: 60), color: Theme.of(context).textTheme.bodyText2!.color, ), Container( height: 9, margin: const EdgeInsets.only(right: 20, top: 8), color: Theme.of(context).textTheme.bodyText2!.color, ), Container( height: 9, margin: const EdgeInsets.only(right: 40, top: 8), color: Theme.of(context).textTheme.bodyText2!.color, ), Container( height: 9, margin: const EdgeInsets.only(right: 80, top: 8), color: Theme.of(context).textTheme.bodyText2!.color, ), Container( height: 9, margin: const EdgeInsets.only(right: 50, top: 8), color: Theme.of(context).textTheme.bodyText2!.color, ), ], ), ), ], ), ), ); } } // =========================================================================== // 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 choices) { switch (defaultTargetPlatform) { case TargetPlatform.android: showDialog( context: context, builder: (context) { int? selectedRadio = 1; return AlertDialog( contentPadding: const EdgeInsets.only(top: 12), content: StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, children: List.generate(choices.length, (index) { return RadioListTile( title: Text(choices[index]), value: index, groupValue: selectedRadio, onChanged: (value) { setState(() => selectedRadio = value); }, ); }), ); }, ), actions: [ TextButton( child: const Text('OK'), onPressed: () => Navigator.of(context).pop(), ), TextButton( child: const Text('CANCEL'), onPressed: () => Navigator.of(context).pop(), ), ], ); }, ); return; case TargetPlatform.iOS: showCupertinoModalPopup( context: context, builder: (context) { return SizedBox( height: 250, child: CupertinoPicker( backgroundColor: Theme.of(context).canvasColor, useMagnifier: true, magnification: 1.1, itemExtent: 40, scrollController: FixedExtentScrollController(initialItem: 1), children: List.generate(choices.length, (index) { return Center( child: Text( choices[index], style: const TextStyle( fontSize: 21, ), ), ); }), onSelectedItemChanged: (value) {}, ), ); }, ); return; default: assert(false, 'Unexpected platform $defaultTargetPlatform'); } }