import 'dart:async'; import 'package:checker/resources.dart'; import 'package:flutter/material.dart'; import 'package:checker/common.dart'; import 'package:checker/consts.dart'; import 'package:checker/screens/settings.dart'; import 'package:checker/screens/faq.dart'; import 'package:checker/strings.dart'; import 'package:checker/db.dart'; import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; abstract class BaseState extends State { /// Класс для работы с бд. SqliteHelper helper; /// Тип сборки. Определяет, какие брать ресурсы (цвета, картинки) String app; /// Ожидание ответа от сервера. bool loading = false; /// Текст ошибки, подставляется в подсказку, либо появляется над текстовым полем. String error; /// Введенное пользователем значение. String merchantID = ''; BaseState(this.helper, this.app); Widget getMainWidget() { return new Scaffold(appBar: getAppBar(), body: new Stack(children: [ getScreenContent(), new Center(child: loading ? new CircularProgressIndicator() : null) ])); } Widget getBackground() { return new Container( decoration: new BoxDecoration( image: new DecorationImage( image: new ExactAssetImage(Resources.getSplash(app)), fit: BoxFit.cover))); } /// Возвращает контейнер с всеми виджетами экрана. Widget getScreenContent(); bool isAutomaticallyImplyLeading(); /// Возвращает заголовок для AppBar String getTitle() { return null; } AppBar getAppBar() { return new AppBar(title: new Container( margin: new EdgeInsets.only(left: 16.0), child: new Text(getTitle(), style: new TextStyle(fontSize: 18.0))), automaticallyImplyLeading: isAutomaticallyImplyLeading(), backgroundColor: Resources.getPrimaryColor(app), actions: getMenuButtons()); } List getMenuButtons() { List menuItemList = []; menuItemList.add(new PopupMenuItem( value: 0, child: getMenuItem(settings_png, StringsLocalization.settings()) )); menuItemList.add(new PopupMenuItem( value: 1, child: getMenuItem(help_png, StringsLocalization.help()) )); if (Theme.of(context).platform != TargetPlatform.iOS) { menuItemList.add(new PopupMenuItem( value: 2, child: getMenuItem(exit_png, StringsLocalization.exit()) )); } return [ new PopupMenuButton( onSelected: onOptionsItemClick, itemBuilder: (BuildContext context) { return menuItemList; } ) ]; } void onOptionsItemClick(int index) { switch (index) { case 0: { new Future.delayed(const Duration(milliseconds: 200), () { var route = new MaterialPageRoute(builder: (BuildContext context) => new SettingsScreen(helper, app, false), fullscreenDialog: true); Navigator.of(context).push(route).then((token) { if (token != null) { Navigator.of(context).pop(token); } }); }); break; } case 1: { new Future.delayed(const Duration(milliseconds: 200), () { var route = new MaterialPageRoute(builder: (BuildContext context) => new FAQScreen(helper, app), fullscreenDialog: true); Navigator.of(context).push(route); }); break; } case 2: { platform.invokeMethod('finish'); break; } } } /// Возвращает пункт меню (Картинка с текстом) Widget getMenuItem(String image, String text) { return new Row(children: [ new Image.asset(image, width: 28.0, height: 28.0), new Container(padding: new EdgeInsets.only(left: 8.0), child: new Text(text)) ]); } /// Возврвщает контейнер, внутри которого Text с подсказкой. Widget getHintLabel() { double horizontalMargin = 8.0; return new Container(margin: new EdgeInsets.only(top: horizontalMargin, bottom: horizontalMargin, left: verticalMargin, right: verticalMargin), child: new Row(crossAxisAlignment: CrossAxisAlignment.start, children: [new Text(getHintOrError(), textAlign: TextAlign.left, style: new TextStyle(fontWeight: FontWeight.w300, color: error == null ? greyTextColor : Resources.getPrimaryColor(app), fontSize: 14.0))])); } /// Возвращает подсказку, либо ошибку, если введенные в поле ввода данные неверны. String getHintOrError() { if (merchantID.length == 0 && error == null) { return ' '; } else if (error != null) { return error; } else { return getHintString(); } } /// Возвращает текст подсказки для поля ввода. /// Должен быть переопределен на экранах, на которых есть поле ввода. String getHintString() { return null; } /// Смена состояния экрана при изменении текста в поле ввода. void handleUserInput(String text) { setState(() { merchantID = text; }); } /// Метод возвращает контейнер с полем ввода внутри. Widget getInputField() { return new Container(margin: new EdgeInsets.only(left: verticalMargin, right: verticalMargin), padding: getInputFieldContainerPadding(), decoration: getInputFieldContainerDecoration(), child: getTextWidget() ); } getItemToggle(String title, String description) { if (title.length == 0) { title = description.substring(0,30) + " ..."; } return new Container( margin: new EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0), child: new Card( child: new ExpansionTile( title: new Text( title, style: Theme.of(context).textTheme.button.copyWith( fontWeight: FontWeight.bold, color: faqTitlesColor ) ), children: [ new Container( margin: new EdgeInsets.only(left: 20.0, right: 20.0), padding: new EdgeInsets.only(top: 0.0, bottom: 10.0), child: new Text(description), ) ] ) ) ); } // Возвращает контейнер с раскрывающимся элементом и переключателем // title - название, description - описание, isSet - состояние переключаиеля // handler - обработчик события переключения getItemToggleSwitch(String title, String description, bool isSet, handler) { if (title.length == 0) { title = description.substring(0,25) + " ..."; } return new Container( key: new Key(new DateTime.now().millisecondsSinceEpoch.toString()), margin: new EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0), child: new Card( child: new ExpansionTile( title: new SwitchListTile( title: new Text( title, style: Theme.of(context).textTheme.button.copyWith( fontWeight: FontWeight.bold, color: faqTitlesColor ) ), value: isSet, onChanged: handler ), children: [ new Container( margin: new EdgeInsets.only(left: 20.0, right: 20.0), padding: new EdgeInsets.only(top: 0.0, bottom: 10.0), child: new Text(description), ), ] ) ) ); } /// Метод возвращает контейнер только с названием элемента Widget getItemTitle(String title) { return new Container( padding: new EdgeInsets.only( left: verticalMargin, right: verticalMargin, top: 18.0 ), child: new Column( children: [ new Row( crossAxisAlignment: CrossAxisAlignment.start, children: getDescriptionWidget(title) ), ] ) ); } /// Возвращает поле ввода. /// Переопределяется для использования на экранах регистрации и проведения покупки. Widget getTextWidget() { return null; } /// Возвращат паддинги для поля ввода. EdgeInsets getInputFieldContainerPadding() { const double verticalPadding = 12.0; const double horizontalPadding = 16.0; return new EdgeInsets.only(top: verticalPadding, bottom: verticalPadding, left: horizontalPadding, right: horizontalPadding); } /// Метод возвращает BoxDecoration для _getDecoratedInputField BoxDecoration getInputFieldContainerDecoration() { return new BoxDecoration(color: inputFieldBackground, border: new Border.all(color: textBorderColor, width: 1.0), borderRadius: new BorderRadius.all(new Radius.circular(4.0))); } /// Возвращает выпуклую залитую фирменным цветом кнопку Widget buildRaisedButton(String text, VoidCallback onPressed) { return new RaisedButton(child: new Text(text, style: new TextStyle(color: Colors.white)), onPressed: onPressed, color: Resources.getButtonColor(app)); } /// Метод возвращает контейнер с отступами, который содержит картинку с логотипом. Widget getLogo() { double containerHeight = 92.0; double imageWidth = 156.0; return new Container(height: containerHeight, child: new Image.asset(Resources.getLogo(app), width: imageWidth)); } /// Возвращает текстовое поле, с однострочным пояснением над ним. Widget getValueWithDescription(String title, String value) { return new Container(padding: new EdgeInsets.only(left: verticalMargin, right: verticalMargin, top: 18.0), child: new Column(children: [ new Row(crossAxisAlignment: CrossAxisAlignment.start, children: getDescriptionWidget(title)), new Row(crossAxisAlignment: CrossAxisAlignment.start, children: getValueWidget(value)) ])); } /// Возвращает список, единственный элемент которого - Text с заголовком для текстового поля. List getDescriptionWidget(String title) { return [new Text(title, textAlign: TextAlign.left, style: new TextStyle(color: greyTextColor, fontSize: 14.0))]; } /// Возвращает список, единственный элемент которого - Text с информацией (размер скидки, сумма проведенной покупки). List getValueWidget(String value) { return [new Expanded(child: new Text(value, textAlign: TextAlign.left, style: new TextStyle(color: Colors.black, fontSize: 20.0)))]; } /// Возвращает кнопку, обернутую набором специфичных контейнеров. Widget wrapButton(EdgeInsets margin, Widget widget) { return new Container(margin: margin, height: buttonHeight, child: new Row(children: [new Expanded(child: widget)])); } } class EnsureVisibleWhenFocused extends StatefulWidget { const EnsureVisibleWhenFocused({ Key key, @required this.child, @required this.focusNode, this.curve: Curves.ease, this.duration: const Duration(milliseconds: 100), }) : super(key: key); /// The node we will monitor to determine if the child is focused final FocusNode focusNode; /// The child widget that we are wrapping final Widget child; /// The curve we will use to scroll ourselves into view. /// /// Defaults to Curves.ease. final Curve curve; /// The duration we will use to scroll ourselves into view /// /// Defaults to 100 milliseconds. final Duration duration; EnsureVisibleWhenFocusedState createState() => new EnsureVisibleWhenFocusedState(); } class EnsureVisibleWhenFocusedState extends State { @override void initState() { super.initState(); widget.focusNode.addListener(_ensureVisible); } @override void dispose() { super.dispose(); widget.focusNode.removeListener(_ensureVisible); } Future _ensureVisible() async { // Wait for the keyboard to come into view // TODO: position doesn't seem to notify listeners when metrics change, // perhaps a NotificationListener around the scrollable could avoid // the need insert a delay here. await new Future.delayed(const Duration(milliseconds: 1000)); if (!widget.focusNode.hasFocus) return; final RenderObject object = context.findRenderObject(); final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); assert(viewport != null); ScrollableState scrollableState = Scrollable.of(context); assert(scrollableState != null); ScrollPosition position = scrollableState.position; double alignment; if (position.pixels > viewport.getOffsetToReveal(object, 0.0)) { // Move down to the top of the viewport alignment = 0.0; } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0)) { // Move up to the bottom of the viewport alignment = 1.0; } else { // No scrolling is necessary to reveal the child return; } position.ensureVisible( object, alignment: alignment, duration: widget.duration, curve: widget.curve, ); } Widget build(BuildContext context) => widget.child; }