From 122c32589fb76afc4d04252930221e91c1d31a5f Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Mon, 6 Dec 2021 14:51:07 +0900 Subject: Preserve state between page changes Add more interactive widgets --- lib/homescreen.dart | 76 +++++----- lib/homescreen_model.dart | 68 +++++++++ lib/main.dart | 2 +- lib/page_hvac.dart | 363 +++++++++++++++++++++++----------------------- lib/switchable_image.dart | 52 +++++++ 5 files changed, 344 insertions(+), 217 deletions(-) create mode 100644 lib/homescreen_model.dart create mode 100644 lib/switchable_image.dart (limited to 'lib') diff --git a/lib/homescreen.dart b/lib/homescreen.dart index dce45cd..edb2acc 100644 --- a/lib/homescreen.dart +++ b/lib/homescreen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_homescreen/homescreen_model.dart'; import 'package:flutter_homescreen/page_dashboard.dart'; import 'package:flutter_homescreen/page_home.dart'; import 'package:flutter_homescreen/page_hvac.dart'; @@ -8,9 +10,7 @@ import 'package:flutter_homescreen/widget_clock.dart'; enum PageIndex { home, dashboard, hvac, media, demo3d } class Homescreen extends StatefulWidget { - Homescreen({Key? key, required this.title}) : super(key: key); - - final String title; + Homescreen({Key? key}) : super(key: key); @override _HomescreenState createState() => _HomescreenState(); @@ -143,44 +143,44 @@ class _HomescreenState extends State with TickerProviderStateMixin { ), // This is the main content. Expanded( - // TODO This solution adds a nice animation but loses the state - // of the old page whenever a new one comes in. We could use - // IndexedStack to keep the state of each page, at the cost of not - // having nice animations between pages. Another option could be to - // move the state of each page upwards in the tree. - // See also: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (Widget child, Animation animation) { - if (child.key != ValueKey(_selectedIndex)) { - return FadeTransition( - opacity: - Tween(begin: 1.0, end: 1.0).animate(animation), - child: child, - ); - } - Offset beginOffset = new Offset( - 0.0, (_selectedIndex > _previousIndex ? 1.0 : -1.0)); - return SlideTransition( - position: Tween(begin: beginOffset, end: Offset.zero) - .animate(animation), - child: FadeTransition( - opacity: Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation( - parent: animation, - curve: Interval(0.5, 1.0), + child: ChangeNotifierProvider( + // See: https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple + // Also: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options + create: (context) => HomescreenModel(), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + reverseDuration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + if (child.key != ValueKey(_selectedIndex)) { + return FadeTransition( + opacity: Tween(begin: 1.0, end: 1.0) + .animate(animation), + child: child, + ); + } + Offset beginOffset = new Offset( + 0.0, (_selectedIndex > _previousIndex ? 1.0 : -1.0)); + return SlideTransition( + position: + Tween(begin: beginOffset, end: Offset.zero) + .animate(animation), + child: FadeTransition( + opacity: Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: animation, + curve: Interval(0.5, 1.0), + ), ), + child: child, ), - child: child, - ), - ); - }, - child: _childForIndex(_selectedIndex), + ); + }, + child: _childForIndex(_selectedIndex), + ), ), - ) + ), ], ), ); diff --git a/lib/homescreen_model.dart b/lib/homescreen_model.dart new file mode 100644 index 0000000..7c1a26f --- /dev/null +++ b/lib/homescreen_model.dart @@ -0,0 +1,68 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +enum SwitchId { + hvacLeftSeat, + hvacRigthSeat, + hvacAc, + hvacAuto, + hvacCirculation, + hvacFan, + hvacAirDown, + hvacAirUp, + hvacFront, + hvacRear, +} + +enum TemperatureId { leftSeat, rightSeat } + +class HomescreenModel extends ChangeNotifier { + // HVAC page + + // fan speed + double _fanSpeed = 20; + + double get fanSpeed => _fanSpeed; + + set fanSpeed(double newhvacFanSpeed) { + _fanSpeed = newhvacFanSpeed; + notifyListeners(); + } + + // switch buttons + HashMap _switches = new HashMap(); + + bool getSwitchState(SwitchId id) => _switches[id] ?? false; + + void setSwitchState(SwitchId id, bool newValue) { + _switches[id] = newValue; + notifyListeners(); + } + + void flipSwitch(SwitchId id) { + _switches[id] = !_switches[id]; + notifyListeners(); + } + + // temperatures + HashMap _temperatures = new HashMap(); + + int getTemperature(TemperatureId id) => _temperatures[id] ?? 22; + + void setTemperature(TemperatureId id, int newTemp) { + _temperatures[id] = newTemp; + notifyListeners(); + } + + HomescreenModel() { + // initialize the values + for (var id in SwitchId.values) { + _switches[id] = false; + } + for (var id in TemperatureId.values) { + _temperatures[id] = 22; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 5335eda..e85c7f6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,7 @@ class MyApp extends StatelessWidget { /* dark theme settings */ ), themeMode: ThemeMode.dark, - home: Homescreen(title: 'Flutter Homescreen'), + home: Homescreen(), ); } } diff --git a/lib/page_hvac.dart b/lib/page_hvac.dart index 488d140..65d77b6 100644 --- a/lib/page_hvac.dart +++ b/lib/page_hvac.dart @@ -1,30 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:flutter_homescreen/homescreen_model.dart'; import 'package:flutter_homescreen/layout_size_helper.dart'; +import 'package:flutter_homescreen/switchable_image.dart'; import 'package:numberpicker/numberpicker.dart'; +import 'package:provider/provider.dart'; -// The page for heating, ventilation, and air conditioning. -class HVACPage extends StatefulWidget { - const HVACPage({Key? key}) : super(key: key); - - @override - State createState() => _HVACPageState(); -} - -String leftChairOn = 'images/HMI_HVAC_Left_Chair_ON.png'; -String leftChairOff = 'images/HMI_HVAC_Left_Chair_OFF.png'; -String rightChairOn = 'images/HMI_HVAC_Right_Chair_ON.png'; -String rightChairOff = 'images/HMI_HVAC_Right_Chair_OFF.png'; -String circulationActive = 'images/HMI_HVAC_Circulation_Active.png'; -String circulationInactive = 'images/HMI_HVAC_Circulation_Inactive.png'; +// image assets +const String LEFT_SEAT = 'images/HMI_HVAC_Left_Chair_ON.png'; +const String RIGHT_SEAT = 'images/HMI_HVAC_Right_Chair_ON.png'; +const String CIRCULATION = 'images/HMI_HVAC_Circulation_Active.png'; +const String AIRDOWN = 'images/HMI_HVAC_AirDown_Active.png'; +const String AIRUP = 'images/HMI_HVAC_AirUp_Active.png'; +const String FRONT = 'images/HMI_HVAC_Front_Active.png'; +const String REAR = 'images/HMI_HVAC_Rear_Active.png'; -class _HVACPageState extends State { -// Get from API - bool leftChairSelected = true; - bool rightChairSelected = true; - bool acSelected = true; - bool autoSelected = false; - bool circulationSelected = false; - double fanSpeed = 20; +// The page for heating, ventilation, and air conditioning. +class HVACPage extends StatelessWidget { + HVACPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -38,67 +30,23 @@ class _HVACPageState extends State { child: Row( children: [ Expanded( - flex: 4, - child: HVACFanSpeed( - fanSpeed: fanSpeed, - onUpdateFanSpeed: (double newFanSpeed) { - setState(() { - fanSpeed = newFanSpeed; - }); - }, - )), - SizedBox(width: sizeHelper.defaultPadding), + flex: 3, + child: HVACFanSpeed(), + ), Expanded( flex: 1, - child: Image.asset('images/HMI_HVAC_Fan_Icon.png', + child: Container( + alignment: Alignment.centerLeft, + child: Image.asset('images/HMI_HVAC_Fan_Icon.png', width: sizeHelper.defaultIconSize, height: sizeHelper.defaultIconSize, fit: BoxFit.contain)), + ), ], - ), - ); - - Widget rightSeat = Container( - padding: EdgeInsets.all(sizeHelper.defaultPadding), - child: Column( - children: [ - IconButton( - iconSize: sizeHelper.largeIconSize, - icon: Image.asset(leftChairSelected ? leftChairOn : leftChairOff, - width: sizeHelper.largeIconSize, - height: sizeHelper.largeIconSize, - fit: BoxFit.contain), - onPressed: () { - setState(() { - leftChairSelected = !leftChairSelected; - }); - }, - ), - SizedBox(height: sizeHelper.defaultPadding), - _TemperatureSelector(), - ], - ), - ); - Widget leftSeat = Container( - padding: EdgeInsets.all(sizeHelper.defaultPadding), - child: Column( - children: [ - IconButton( - iconSize: sizeHelper.largeIconSize, - icon: Image.asset(rightChairSelected ? rightChairOn : rightChairOff, - width: sizeHelper.largeIconSize, - height: sizeHelper.largeIconSize, - fit: BoxFit.contain), - onPressed: () { - setState(() { - rightChairSelected = !rightChairSelected; - }); - }, - ), - SizedBox(height: sizeHelper.defaultPadding), - _TemperatureSelector(), - ], + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, ), ); @@ -107,62 +55,35 @@ class _HVACPageState extends State { child: Column( children: [ _HVACToggleButton( - label: 'A/C', - isSelected: acSelected, - onPressed: () { - setState(() { - acSelected = !acSelected; - }); - }), + label: 'A/C', + switchId: SwitchId.hvacAc, + ), _HVACToggleButton( - label: 'Auto', - isSelected: autoSelected, - onPressed: () { - setState(() { - autoSelected = !autoSelected; - }); - }), + label: 'Auto', + switchId: SwitchId.hvacAuto, + ), _HVACToggleButton( - child: Image.asset( - circulationSelected ? circulationActive : circulationInactive, - width: sizeHelper.defaultIconSize, - height: sizeHelper.defaultIconSize, - fit: BoxFit.contain), - isSelected: circulationSelected, - onPressed: () { - setState(() { - circulationSelected = !circulationSelected; - }); - }), + imageAssetId: CIRCULATION, + switchId: SwitchId.hvacCirculation, + ), ], ), ); - Widget actions = Container( - padding: EdgeInsets.all(sizeHelper.defaultPadding), - child: Column( + + Widget actions = + Consumer(builder: (context, model, child) { + return Column( children: [ - Image.asset('images/HMI_HVAC_AirDown_Inactive.png', - width: sizeHelper.defaultIconSize, - height: sizeHelper.defaultIconSize, - fit: BoxFit.contain), + _ActionButton(switchId: SwitchId.hvacAirDown, imageAssetId: AIRDOWN), SizedBox(height: sizeHelper.defaultPadding), - Image.asset('images/HMI_HVAC_AirUp_Inactive.png', - width: sizeHelper.defaultIconSize, - height: sizeHelper.defaultIconSize, - fit: BoxFit.contain), + _ActionButton(switchId: SwitchId.hvacAirUp, imageAssetId: AIRUP), SizedBox(height: sizeHelper.defaultPadding), - Image.asset('images/HMI_HVAC_Front_Inactive.png', - width: sizeHelper.defaultIconSize, - height: sizeHelper.defaultIconSize, - fit: BoxFit.contain), + _ActionButton(switchId: SwitchId.hvacFront, imageAssetId: FRONT), SizedBox(height: sizeHelper.defaultPadding), - Image.asset('images/HMI_HVAC_Rear_Active.png', - width: sizeHelper.defaultIconSize, - height: sizeHelper.defaultIconSize, - fit: BoxFit.contain), + _ActionButton(switchId: SwitchId.hvacRear, imageAssetId: REAR), ], - ), - ); + ); + }); return Container( decoration: BoxDecoration( @@ -175,9 +96,21 @@ class _HVACPageState extends State { children: [ fanSpeedControl, Row(children: [ - Expanded(flex: 1, child: rightSeat), + Expanded( + flex: 1, + child: _SeatButton( + switchId: SwitchId.hvacLeftSeat, + temperatureId: TemperatureId.leftSeat, + imageAssetId: LEFT_SEAT, + )), Expanded(flex: 1, child: centerView), - Expanded(flex: 1, child: leftSeat), + Expanded( + flex: 1, + child: _SeatButton( + switchId: SwitchId.hvacRigthSeat, + temperatureId: TemperatureId.rightSeat, + imageAssetId: RIGHT_SEAT, + )), Expanded(flex: 1, child: actions) ]) ], @@ -186,26 +119,22 @@ class _HVACPageState extends State { } // The temperature selector. -class _TemperatureSelector extends StatefulWidget { - _TemperatureSelector({Key? key}) : super(key: key); - - @override - _TemperatureSelectorState createState() => _TemperatureSelectorState(); -} +class _TemperatureSelector extends StatelessWidget { + final TemperatureId temperatureId; -class _TemperatureSelectorState extends State<_TemperatureSelector> { - int _currentValue = 22; // INIT FROM AGLJS wrapper + _TemperatureSelector({Key? key, required this.temperatureId}) + : super(key: key); @override Widget build(BuildContext context) { var sizeHelper = LayoutSizeHelper(context); - return Column( - children: [ - NumberPicker( - value: _currentValue, + return Consumer( + builder: (context, model, child) { + return NumberPicker( + value: model.getTemperature(temperatureId), minValue: 18, maxValue: 25, - onChanged: (value) => setState(() => _currentValue = value), + onChanged: (value) => model.setTemperature(temperatureId, value), textStyle: DefaultTextStyle.of(context).style.copyWith( color: Colors.teal.shade200, fontSize: sizeHelper.baseFontSize, @@ -215,20 +144,15 @@ class _TemperatureSelectorState extends State<_TemperatureSelector> { ), itemHeight: sizeHelper.baseFontSize * 3, itemWidth: sizeHelper.baseFontSize * 6, - ), - ], + ); + }, ); } } /// The fan speed control. class HVACFanSpeed extends StatelessWidget { - final double fanSpeed; - final Null Function(double) onUpdateFanSpeed; - - const HVACFanSpeed( - {Key? key, required this.fanSpeed, required this.onUpdateFanSpeed}) - : super(key: key); + const HVACFanSpeed({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -238,32 +162,73 @@ class HVACFanSpeed extends StatelessWidget { activeTrackColor: Colors.greenAccent.shade700, inactiveTrackColor: Colors.blueGrey.shade200, ), - child: Slider( - value: fanSpeed, - min: 0, - max: 300, - label: fanSpeed.round().toString(), - onChanged: (double value) { - onUpdateFanSpeed(value); + child: Consumer( + builder: (context, model, child) { + return Slider( + value: model.fanSpeed, + min: 0, + max: 300, + label: model.fanSpeed.round().toString(), + onChanged: (double newValue) { + model.fanSpeed = newValue; + }, + ); }, ), ); } } -// Each one of the toggle buttons in the UI. +// the button to enable A/C on each seat +class _SeatButton extends StatelessWidget { + final SwitchId switchId; + final TemperatureId temperatureId; + final String imageAssetId; + + const _SeatButton({ + Key? key, + required this.switchId, + required this.temperatureId, + required this.imageAssetId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var sizeHelper = LayoutSizeHelper(context); + return Container( + padding: EdgeInsets.all(sizeHelper.defaultPadding), + child: Column( + children: [ + Consumer( + builder: (context, model, child) { + return IconButton( + onPressed: () => model.flipSwitch(switchId), + iconSize: sizeHelper.largeIconSize, + icon: SwitchableImage( + value: model.getSwitchState(switchId), + imageAssetId: imageAssetId, + width: sizeHelper.largeIconSize, + height: sizeHelper.largeIconSize, + ), + ); + }, + ), + SizedBox(height: sizeHelper.defaultPadding), + _TemperatureSelector(temperatureId: temperatureId), + ], + ), + ); + } +} + +// Each one of the large toggle buttons in the UI. class _HVACToggleButton extends StatelessWidget { final String? label; - final Widget? child; - final bool isSelected; - final Null Function() onPressed; + final String? imageAssetId; + final SwitchId switchId; _HVACToggleButton( - {Key? key, - this.label, - this.child, - required this.isSelected, - required this.onPressed}) + {Key? key, required this.switchId, this.label, this.imageAssetId}) : super(key: key); @override @@ -282,25 +247,67 @@ class _HVACToggleButton extends StatelessWidget { width: sizeHelper.defaultButtonWidth, height: sizeHelper.defaultButtonHeight, margin: EdgeInsets.all(sizeHelper.defaultPadding), - child: OutlinedButton( - onPressed: onPressed, - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(sizeHelper.defaultButtonHeight / 4.0), - ), - side: BorderSide( - width: sizeHelper.defaultBorder, - color: isSelected ? Colors.green : Colors.grey, - style: BorderStyle.solid, - ), - ), - child: child ?? - Text( - label ?? '', - style: isSelected ? buttonTextStyle : unselectedButtonTextStyle, + child: Consumer( + builder: (context, model, child) { + return OutlinedButton( + onPressed: () => model.flipSwitch(switchId), + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(sizeHelper.defaultButtonHeight / 4.0), + ), + side: BorderSide( + width: sizeHelper.defaultBorder, + color: + model.getSwitchState(switchId) ? Colors.green : Colors.grey, + style: BorderStyle.solid, + ), ), + child: (imageAssetId != null) + ? SwitchableImage( + value: model.getSwitchState(switchId), + imageAssetId: imageAssetId ?? '', + width: sizeHelper.defaultIconSize, + height: sizeHelper.defaultIconSize, + ) + : Text( + label ?? '', + style: model.getSwitchState(switchId) + ? buttonTextStyle + : unselectedButtonTextStyle, + ), + ); + }, ), ); } } + +// Each one of the small action buttons. +class _ActionButton extends StatelessWidget { + final SwitchId switchId; + final String imageAssetId; + + const _ActionButton( + {Key? key, required this.switchId, required this.imageAssetId}) + : super(key: key); + + @override + Widget build(BuildContext context) { + var sizeHelper = LayoutSizeHelper(context); + return Consumer( + builder: (context, model, child) { + return IconButton( + onPressed: () => model.flipSwitch(switchId), + iconSize: sizeHelper.defaultIconSize, + icon: SwitchableImage( + value: model.getSwitchState(switchId), + imageAssetId: imageAssetId, + width: sizeHelper.defaultIconSize, + height: sizeHelper.defaultIconSize, + ), + ); + }, + ); + } +} diff --git a/lib/switchable_image.dart b/lib/switchable_image.dart new file mode 100644 index 0000000..97c9b02 --- /dev/null +++ b/lib/switchable_image.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +// https://api.flutter.dev/flutter/dart-ui/ColorFilter/ColorFilter.matrix.html +const ColorFilter _identity = ColorFilter.matrix([ + 1, 0, 0, 0, // identity matrix + 0, 0, 1, 0, + 0, 0, 0, 0, + 1, 0, 0, 0, + 0, 0, 1, 0, +]); + +const ColorFilter _greyscale = ColorFilter.matrix([ + 0.2126, 0.7152, 0.0722, 0, 0, // greyscale filter + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, +]); + +// This class is used to implement enabled/disabled icons with only one image: +// when the widget is disabled (value==false) the image is displayed in +// greyscale and at 50% opacity. +class SwitchableImage extends StatelessWidget { + final bool value; + final String imageAssetId; + final double width, height; + + const SwitchableImage({ + Key? key, + required this.value, + required this.imageAssetId, + required this.width, + required this.height, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: Opacity( + opacity: value ? 1.0 : 0.5, + child: ColorFiltered( + colorFilter: value ? _identity : _greyscale, + child: Image.asset( + imageAssetId, + width: width, + height: height, + fit: BoxFit.contain, + ), + ), + ), + ); + } +} -- cgit 1.2.3-korg