summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFelipe Erias <felipeerias@igalia.com>2021-12-06 14:51:07 +0900
committerFelipe Erias <felipeerias@igalia.com>2021-12-06 14:51:07 +0900
commit122c32589fb76afc4d04252930221e91c1d31a5f (patch)
tree4fa5a25c6658546e13007e1c2e900b64c208f2be
parentd7b7bfc0ecc141c97725448f17fda3512a9245a5 (diff)
Preserve state between page changes
Add more interactive widgets
-rw-r--r--lib/homescreen.dart76
-rw-r--r--lib/homescreen_model.dart68
-rw-r--r--lib/main.dart2
-rw-r--r--lib/page_hvac.dart363
-rw-r--r--lib/switchable_image.dart52
5 files changed, 344 insertions, 217 deletions
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<Homescreen> 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<double> animation) {
- if (child.key != ValueKey(_selectedIndex)) {
- return FadeTransition(
- opacity:
- Tween<double>(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<Offset>(begin: beginOffset, end: Offset.zero)
- .animate(animation),
- child: FadeTransition(
- opacity: Tween<double>(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<double> animation) {
+ if (child.key != ValueKey(_selectedIndex)) {
+ return FadeTransition(
+ opacity: Tween<double>(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<Offset>(begin: beginOffset, end: Offset.zero)
+ .animate(animation),
+ child: FadeTransition(
+ opacity: Tween<double>(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<SwitchId, bool>();
+
+ 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<TemperatureId, int>();
+
+ 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<HVACPage> 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<HVACPage> {
-// 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<HVACPage> {
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<HVACPage> {
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<HomescreenModel>(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<HVACPage> {
children: <Widget>[
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<HVACPage> {
}
// 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: <Widget>[
- NumberPicker(
- value: _currentValue,
+ return Consumer<HomescreenModel>(
+ 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<HomescreenModel>(
+ 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<HomescreenModel>(
+ 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<HomescreenModel>(
+ 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<HomescreenModel>(
+ 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(<double>[
+ 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(<double>[
+ 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,
+ ),
+ ),
+ ),
+ );
+ }
+}