improved UI & UX & DX

This commit is contained in:
Myzel394 2022-08-15 22:04:39 +02:00
parent b146957c69
commit 4bcafd991d
9 changed files with 305 additions and 65 deletions

View File

@ -1 +1,3 @@
const DURATION_INFINITY = Duration(days: 999);
const SECONDARY_BUTTONS_DURATION_MULTIPLIER = 1.8;
const PHOTO_SHOW_AFTER_CREATION_DURATION = Duration(milliseconds: 500);

View File

@ -2,6 +2,7 @@ import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:share_location/constants/apis.dart';
import 'package:share_location/screens/grant_permission_screen.dart';
import 'package:share_location/screens/login_screen.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:share_location/screens/timeline_screen.dart';
@ -60,6 +61,7 @@ class MyApp extends StatelessWidget {
MainScreen.ID: (context) => const MainScreen(),
LoginScreen.ID: (context) => const LoginScreen(),
TimelineScreen.ID: (context) => const TimelineScreen(),
GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(),
},
initialRoute: initialPage,
);

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:share_location/screens/main_screen.dart';
import 'main_screen/permissions_required_page.dart';
class GrantPermissionScreen extends StatelessWidget {
static const ID = 'grant_permission';
const GrantPermissionScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Grant Permission'),
),
body: Center(
child: PermissionsRequiredPage(
onPermissionsGranted: () {
Navigator.pushReplacementNamed(context, MainScreen.ID);
},
),
),
);
}
}

View File

@ -4,14 +4,16 @@ import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/constants/values.dart';
import 'package:share_location/extensions/snackbar.dart';
import 'package:share_location/managers/file_manager.dart';
import 'package:share_location/managers/global_values_manager.dart';
import 'package:share_location/screens/main_screen/permissions_required_page.dart';
import 'package:share_location/utils/auth_required.dart';
import 'package:share_location/utils/loadable.dart';
import 'package:share_location/widgets/animate_in_builder.dart';
import 'package:share_location/widgets/camera_button.dart';
import 'package:share_location/widgets/change_camera_button.dart';
import 'package:share_location/widgets/fade_and_move_in_animation.dart';
import 'package:share_location/widgets/today_photo_button.dart';
import 'package:share_location/widgets/uploading_photo.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@ -27,7 +29,6 @@ class MainScreen extends StatefulWidget {
class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
bool isRecording = false;
bool hasGrantedPermissions = false;
bool lockCamera = false;
List? lastPhoto;
Uint8List? uploadingPhotoAnimation;
@ -45,6 +46,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
super.initState();
callWithLoading(getLastPhoto);
onNewCameraSelected(GlobalValuesManager.cameras[0]);
}
@override
@ -55,6 +57,10 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_updateCamera(state);
}
void _updateCamera(final AppLifecycleState state) {
final CameraController? cameraController = controller;
// App state changed before we got the chance to initialize.
@ -109,6 +115,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
if (!mounted) {
return;
}
setState(() {});
});
}
@ -203,20 +210,6 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
backgroundColor: Colors.black,
body: SafeArea(
child: () {
if (!hasGrantedPermissions) {
return Center(
child: PermissionsRequiredPage(
onPermissionsGranted: () {
onNewCameraSelected(GlobalValuesManager.cameras[0]);
setState(() {
hasGrantedPermissions = true;
});
},
),
);
}
if (isLoading) {
return const Center(
child: CircularProgressIndicator(),
@ -229,19 +222,42 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
child: Stack(
alignment: Alignment.center,
children: <Widget>[
controller!.buildPreview(),
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
AnimateInBuilder(
builder: (showPreview) => AnimatedOpacity(
opacity: showPreview ? 1.0 : 0.0,
duration: const Duration(milliseconds: 1100),
curve: Curves.easeOutQuad,
child: ClipRRect(
borderRadius: BorderRadius.circular(SMALL_SPACE),
child: AspectRatio(
aspectRatio: 1 / controller!.value.aspectRatio,
child: controller!.buildPreview(),
),
),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(LARGE_SPACE),
padding: const EdgeInsets.symmetric(
horizontal: LARGE_SPACE),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ChangeCameraButton(
FadeAndMoveInAnimation(
translationDuration:
DEFAULT_TRANSLATION_DURATION *
SECONDARY_BUTTONS_DURATION_MULTIPLIER,
opacityDuration: DEFAULT_OPACITY_DURATION *
SECONDARY_BUTTONS_DURATION_MULTIPLIER,
child: ChangeCameraButton(
onChangeCamera: () {
final currentCameraIndex = GlobalValuesManager
.cameras
final currentCameraIndex =
GlobalValuesManager.cameras
.indexOf(controller!.description);
final availableCameras =
GlobalValuesManager.cameras.length;
@ -253,7 +269,9 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
);
},
),
CameraButton(
),
FadeAndMoveInAnimation(
child: CameraButton(
disabled: lockCamera,
active: isRecording,
onVideoBegin: () async {
@ -271,17 +289,28 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
onVideoEnd: takeVideo,
onPhotoShot: takePhoto,
),
lastPhoto == null
),
FadeAndMoveInAnimation(
translationDuration:
DEFAULT_TRANSLATION_DURATION *
SECONDARY_BUTTONS_DURATION_MULTIPLIER,
opacityDuration: DEFAULT_OPACITY_DURATION *
SECONDARY_BUTTONS_DURATION_MULTIPLIER,
child: lastPhoto == null
? const TodayPhotoButton()
: TodayPhotoButton(
data: lastPhoto![0],
type: lastPhoto![1],
),
),
],
),
)
],
),
),
],
),
if (uploadingPhotoAnimation != null)
UploadingPhoto(
data: uploadingPhotoAnimation!,

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/managers/startup_page_manager.dart';
import 'package:share_location/screens/main_screen.dart';
import 'package:share_location/widgets/logo.dart';
import 'grant_permission_screen.dart';
class WelcomeScreen extends StatelessWidget {
static const ID = 'welcome';
@ -43,7 +44,10 @@ class WelcomeScreen extends StatelessWidget {
icon: const Icon(Icons.arrow_right),
label: const Text('Start'),
onPressed: () {
StartupPageManager.navigateToNewPage(context, MainScreen.ID);
StartupPageManager.navigateToNewPage(
context,
GrantPermissionScreen.ID,
);
},
),
],

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class AnimateInBuilder extends StatefulWidget {
final Widget Function(bool isActive) builder;
const AnimateInBuilder({
Key? key,
required this.builder,
}) : super(key: key);
@override
State<AnimateInBuilder> createState() => _AnimateInBuilderState();
}
class _AnimateInBuilderState extends State<AnimateInBuilder> {
bool isActive = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
isActive = true;
});
});
}
@override
Widget build(BuildContext context) {
return widget.builder(isActive);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class DelayRender extends StatefulWidget {
final Widget child;
final Widget placeholder;
final Duration delay;
const DelayRender({
Key? key,
required this.child,
this.placeholder = const SizedBox(),
this.delay = const Duration(milliseconds: 120),
}) : super(key: key);
@override
State<DelayRender> createState() => _DelayRenderState();
}
class _DelayRenderState extends State<DelayRender> {
bool allowRendering = false;
@override
void initState() {
super.initState();
startTimer();
}
Future<void> startTimer() async {
await Future.delayed(widget.delay);
if (!mounted) {
return;
}
setState(() {
allowRendering = true;
});
}
@override
Widget build(BuildContext context) {
if (!allowRendering) {
return widget.placeholder;
}
return widget.child;
}
}

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
const DEFAULT_TRANSLATION_DURATION = Duration(milliseconds: 500);
const DEFAULT_OPACITY_DURATION = Duration(milliseconds: 800);
class FadeAndMoveInAnimation extends StatefulWidget {
final Widget child;
final bool active;
final Offset translationOffset;
final Duration translationDuration;
final Curve translationCurve;
final Duration opacityDuration;
final Curve opacityCurve;
const FadeAndMoveInAnimation({
Key? key,
required this.child,
this.active = true,
this.translationOffset = const Offset(0, 60),
this.translationDuration = DEFAULT_TRANSLATION_DURATION,
this.translationCurve = Curves.easeOutQuad,
this.opacityDuration = DEFAULT_OPACITY_DURATION,
this.opacityCurve = Curves.linearToEaseOut,
}) : super(key: key);
@override
State<FadeAndMoveInAnimation> createState() => _FadeAndMoveInAnimationState();
}
class _FadeAndMoveInAnimationState extends State<FadeAndMoveInAnimation>
with TickerProviderStateMixin {
late final AnimationController translationController;
late final Animation<double> translationAnimation;
bool opacityEnabled = false;
@override
void initState() {
super.initState();
translationController = AnimationController(
vsync: this,
duration: widget.translationDuration,
);
translationAnimation = Tween<double>(
begin: 1,
end: 0,
).animate(
CurvedAnimation(
parent: translationController,
curve: widget.translationCurve,
),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.active) {
translationController.forward();
} else {
translationController.reverse();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
opacityEnabled = widget.active;
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: translationController,
child: widget.child,
builder: (context, child) => Transform.translate(
offset: Offset(
widget.translationOffset.dx * translationAnimation.value,
widget.translationOffset.dy * translationAnimation.value,
),
child: AnimatedOpacity(
duration: widget.opacityDuration,
curve: widget.opacityCurve,
opacity: opacityEnabled ? 1 : 0,
child: child,
),
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
import 'package:share_location/constants/values.dart';
class UploadingPhoto extends StatefulWidget {
final Uint8List data;
@ -39,7 +40,7 @@ class _UploadingPhotoState extends State<UploadingPhoto>
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(const Duration(milliseconds: 500), () {
Future.delayed(PHOTO_SHOW_AFTER_CREATION_DURATION, () {
if (mounted) {
widget.onDone();
}
@ -67,9 +68,9 @@ class _UploadingPhotoState extends State<UploadingPhoto>
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 15,
width: 12,
),
borderRadius: BorderRadius.circular(SMALL_SPACE),
borderRadius: BorderRadius.circular(MEDIUM_SPACE),
),
child: Image.memory(widget.data, fit: BoxFit.cover),
),