diff --git a/lib/ui/expansion_card.dart b/lib/ui/expansion_card.dart new file mode 100644 index 000000000..bcc69cd81 --- /dev/null +++ b/lib/ui/expansion_card.dart @@ -0,0 +1,231 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; + +const Duration _kExpand = Duration(milliseconds: 200); + +/// A single-line [ListTile] with a trailing button that expands or collapses +/// the tile to reveal or hide the [children]. +/// +/// This widget is typically used with [ListView] to create an +/// "expand / collapse" list entry. When used with scrolling widgets like +/// [ListView], a unique [PageStorageKey] must be specified to enable the +/// [ExpansionTile] to save and restore its expanded state when it is scrolled +/// in and out of view. +/// +/// See also: +/// +/// * [ListTile], useful for creating expansion tile [children] when the +/// expansion tile represents a sublist. +/// * The "Expand/collapse" section of +/// . +class ExpansionCard extends StatefulWidget { + /// Creates a single-line [ListTile] with a trailing button that expands or collapses + /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must + /// be non-null. + const ExpansionCard({ + Key key, + this.leading, + @required this.title, + this.background, + this.backgroundColor, + this.margin = const EdgeInsets.only(top: 30), + this.borderRadius = 30.0, + this.onExpansionChanged, + this.children = const [], + this.trailing, + this.initiallyExpanded = false, + this.color, + }) : assert(initiallyExpanded != null), + super(key: key); + + /// Adds margin to content of the card. + final EdgeInsets margin; + + /// Provides CircularRadius to the border of the card. + final double borderRadius; + + /// A widget to add background. + /// it can be a gif or image. + final Widget background; + + /// A widget to display before the title. + /// + /// Typically a [CircleAvatar] widget. + final Widget leading; + + /// The primary content of the list item. + /// + /// Typically a [Text] widget. + final Widget title; + + /// Called when the tile expands or collapses. + /// + /// When the tile starts expanding, this function is called with the value + /// true. When the tile starts collapsing, this function is called with + /// the value false. + final ValueChanged onExpansionChanged; + + /// The widgets that are displayed when the tile expands. + /// + /// Typically [ListTile] widgets. + final List children; + + /// The color to display behind the sublist when expanded. + final Color backgroundColor; + + /// A widget to display instead of a rotating arrow icon. + final Widget trailing; + + /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default). + final bool initiallyExpanded; + + /// Color of the expanded heading and icon + final Color color; + + @override + _ExpansionTileState createState() => _ExpansionTileState(); +} + +class _ExpansionTileState extends State + with SingleTickerProviderStateMixin { + static final Animatable _easeOutTween = + CurveTween(curve: Curves.easeOut); + static final Animatable _easeInTween = + CurveTween(curve: Curves.easeIn); + static final Animatable _halfTween = + Tween(begin: 0.0, end: 0.5); + + final ColorTween _borderColorTween = ColorTween(); + final ColorTween _headerColorTween = ColorTween(); + final ColorTween _iconColorTween = ColorTween(); + final ColorTween _backgroundColorTween = ColorTween(); + + AnimationController _controller; + Animation _iconTurns; + Animation _heightFactor; + Animation _headerColor; + Animation _iconColor; + Animation _backgroundColor; + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController(duration: _kExpand, vsync: this); + _heightFactor = _controller.drive(_easeInTween); + _iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); + _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween)); + _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween)); + _backgroundColor = + _controller.drive(_backgroundColorTween.chain(_easeOutTween)); + + _isExpanded = + PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded; + if (_isExpanded) _controller.value = 1.0; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTap() { + setState(() { + _isExpanded = !_isExpanded; + if (_isExpanded) { + _controller.forward(); + } else { + _controller.reverse().then((void value) { + if (!mounted) return; + setState(() { + // Rebuild without widget.children. + }); + }); + } + PageStorage.of(context)?.writeState(context, _isExpanded); + }); + if (widget.onExpansionChanged != null) + widget.onExpansionChanged(_isExpanded); + } + + Widget _buildChildren(BuildContext context, Widget child) { + final Color borderSideColor = Colors.transparent; // _borderColor.value ?? + + return Stack( + children: [ + widget.background == null + ? Container() + : ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: Align( + heightFactor: + _heightFactor.value < 0.5 ? 0.5 : _heightFactor.value, + child: widget.background, + ), + ), + Container( + decoration: BoxDecoration( + color: _backgroundColor.value ?? Colors.transparent, + border: Border( + top: BorderSide(color: borderSideColor), + bottom: BorderSide(color: borderSideColor), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTileTheme.merge( + iconColor: _iconColor.value, + textColor: _headerColor.value, + child: Container( + margin: widget.margin, + child: ListTile( + onTap: _handleTap, + leading: widget.leading, + title: widget.title, + trailing: widget.trailing ?? + RotationTransition( + turns: _iconTurns, + child: const Icon(Icons.expand_more), + ), + ), + )), + ClipRect( + child: Align( + heightFactor: _heightFactor.value, + child: child, + ), + ), + ], + ), + ) + ], + ); + } + + @override + void didChangeDependencies() { + final ThemeData theme = Theme.of(context); + _borderColorTween..end = theme.dividerColor; + _headerColorTween + ..begin = Colors.white + ..end = widget.color ?? Color(0xff60c9df); + _iconColorTween + ..begin = Colors.white + ..end = widget.color ?? Color(0xff60c9df); + _backgroundColorTween..end = widget.backgroundColor; + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + final bool closed = !_isExpanded && _controller.isDismissed; + return AnimatedBuilder( + animation: _controller.view, + builder: _buildChildren, + child: closed ? null : Column(children: widget.children), + ); + } +} diff --git a/lib/ui/subscription_page.dart b/lib/ui/subscription_page.dart index 21ad6376d..4aa5966c2 100644 --- a/lib/ui/subscription_page.dart +++ b/lib/ui/subscription_page.dart @@ -2,13 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:expansion_card/expansion_card.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:logging/logging.dart'; +import 'package:photos/ui/expansion_card.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -296,20 +295,24 @@ class _SubscriptionPageState extends State { ), ]); } else { - widgets.addAll([ - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: EdgeInsets.all(12), - child: Text( - "we offer a 14 day free trial, you can cancel anytime", - style: TextStyle( - color: Colors.white, - height: 1.2, + if (_currentSubscription == null) { + widgets.addAll([ + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + "we offer a 14 day free trial, you can cancel anytime", + style: TextStyle( + color: Colors.white, + height: 1.2, + ), ), ), ), - ), + ]); + } + widgets.addAll([ Expanded(child: Container()), Align( alignment: Alignment.center, @@ -390,21 +393,7 @@ class _BillingQuestionsWidgetState extends State { ), )); for (final faq in snapshot.data) { - faqs.add(ExpansionCard( - margin: EdgeInsets.only(bottom: 2), - title: Text(faq.q), - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, right: 16), - child: Text( - faq.a, - style: TextStyle( - height: 1.5, - ), - ), - ) - ], - )); + faqs.add(FaqWidget(faq: faq)); } faqs.add(Padding( padding: EdgeInsets.all(16), @@ -424,6 +413,34 @@ class _BillingQuestionsWidgetState extends State { } } +class FaqWidget extends StatelessWidget { + const FaqWidget({ + Key key, + @required this.faq, + }) : super(key: key); + + final faq; + + @override + Widget build(BuildContext context) { + return ExpansionCard( + title: Text(faq.q), + color: Theme.of(context).accentColor, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Text( + faq.a, + style: TextStyle( + height: 1.5, + ), + ), + ) + ], + ); + } +} + class FaqItem { final String q; final String a;