diff --git a/lib/presentation/screens/feed/feed_image_widget.dart b/lib/presentation/screens/feed/feed_image_widget.dart new file mode 100644 index 0000000..e3e499d --- /dev/null +++ b/lib/presentation/screens/feed/feed_image_widget.dart @@ -0,0 +1,55 @@ +import 'package:devour/presentation/widgets/platform/platform_icon_button.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:lottie/lottie.dart'; +import 'package:octo_image/octo_image.dart'; + +/// A widget to show image from network or cache with correct size +class FeedImage extends StatelessWidget { + /// Construct FeedImag + const FeedImage({ + Key? key, + this.constraints, + required this.imageProvider, + this.onRefreshPressed, + }) : super(key: key); + + final BoxConstraints? constraints; + final ImageProvider imageProvider; + /// Callback fired, when user taps on error builder, when image is failed to load + final VoidCallback? onRefreshPressed; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: 200, + maxWidth: constraints?.maxWidth ?? double.infinity, + ), + child: OctoImage( + // key: widget.key, + image: imageProvider, + fit: BoxFit.fitWidth, + progressIndicatorBuilder: (ctx, prgrs) => SizedBox( + height: 200, + child: Lottie.asset( + 'lib/assets/animations/space_loader.json', + ), + ), + errorBuilder: (ctx, error, stack) => SizedBox( + height: 200, + child: Center( + child: PlatformIconButton( + icon: Icons.refresh, + onPressed: onRefreshPressed, + color: CupertinoColors.white, + size: 24, + text: 'failed to load 🤬', + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/feed/feed_widget.dart b/lib/presentation/screens/feed/feed_widget.dart index 45d9890..88a177a 100644 --- a/lib/presentation/screens/feed/feed_widget.dart +++ b/lib/presentation/screens/feed/feed_widget.dart @@ -1,11 +1,12 @@ -import 'dart:math'; import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:devour/application/feed/bloc/feed_bloc.dart'; import 'package:devour/injection.dart'; +import 'package:devour/presentation/screens/feed/feed_image_widget.dart'; import 'package:devour/presentation/screens/feed/feed_scroll_physics.dart'; import 'package:devour/presentation/screens/feed/post_widget.dart'; +import 'package:devour/presentation/widgets/animations/animated_opacity_reverse.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -14,7 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:fpdart/fpdart.dart' show Option; import 'package:lottie/lottie.dart'; -import 'package:octo_image/octo_image.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:collection/collection.dart'; @@ -111,7 +111,6 @@ class _FeedWidgetState extends State { state.memes[index].imageLink, cacheManager: cacheManager, ); - final key = renderedMemesKeys.putIfAbsent( index, () => GlobalKey(), @@ -126,20 +125,22 @@ class _FeedWidgetState extends State { renderedMemes[index] = Size(cnstr.maxWidth, 200); } - return ConstrainedBox( - constraints: BoxConstraints( - minHeight: 200, - // maxHeight: maxHeight, - maxWidth: cnstr.maxWidth, - ), - child: OctoImage( + // Using [AnimatedVisibilityWithReversedDuration] to + // hide with animation selected post and show immediately + // if its not. Its because of postWidget, it will re-render + // any error images, so they will reload, and this and post + // widgets will have different state, but overlaping each other + return AnimatedVisibilityWithReversedDuration( + duration: const Duration(milliseconds: 300), + reversedDuration: Duration.zero, + visibility: index != state.iterator, + child: FeedImage( key: key, - image: imageProvider, - fit: BoxFit.fitWidth, - progressIndicatorBuilder: (context, progress) => - Lottie.asset( - 'lib/assets/animations/space_loader.json', - ), + imageProvider: imageProvider, + constraints: cnstr, + onRefreshPressed: () => setState(() { + renderedMemesKeys[index] = GlobalKey(); + }), ), ); }, @@ -215,6 +216,7 @@ class _FeedWidgetState extends State { WidgetsBinding.instance!.addPostFrameCallback( (_) => setState(() { renderedMemes[index] = size; + print('index $index is $size'); imageProvider .resolve(ImageConfiguration.empty) .removeListener(listener); diff --git a/lib/presentation/screens/feed/post_widget.dart b/lib/presentation/screens/feed/post_widget.dart index bef6228..fcecf68 100644 --- a/lib/presentation/screens/feed/post_widget.dart +++ b/lib/presentation/screens/feed/post_widget.dart @@ -5,14 +5,17 @@ import 'package:devour/application/feed/bloc/feed_bloc.dart'; import 'package:devour/domain/meme/abstract_meme_model.dart'; import 'package:devour/domain/misc/helper.dart'; import 'package:devour/injection.dart'; +import 'package:devour/presentation/screens/feed/feed_image_widget.dart'; import 'package:devour/presentation/widgets/platform/platform_icon_button.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:octo_image/octo_image.dart'; -class PostWidget extends StatelessWidget { +/// Widget to show post information like description and likes and image, +/// to create illusion of selecting meme from list. +class PostWidget extends StatefulWidget { + /// Constructs PostWidget const PostWidget( this.state, { required this.constraints, @@ -22,13 +25,19 @@ class PostWidget extends StatelessWidget { final FeedState state; final BoxConstraints constraints; + @override + State createState() => _PostWidgetState(); +} + +class _PostWidgetState extends State { @override Widget build(BuildContext context) { - if (state.isLoading) { + if (widget.state.isLoading) { return Container(); } - final key = state.currentMemeWidget.toNullable(); + final key = widget.state.currentMemeWidget.toNullable(); + // key.currentWidget final box = key?.currentContext?.findRenderObject() as RenderBox?; final pos = box?.localToGlobal(Offset.zero) ?? Offset.zero; @@ -41,33 +50,27 @@ class PostWidget extends StatelessWidget { height: box?.size.height, child: IgnorePointer( child: AnimatedSwitcher( - duration: const Duration(milliseconds: 100), - layoutBuilder: (curr, prev) { - return Stack( - alignment: Alignment.center, - children: [ - // ...prev, - if (curr != null) curr, - ], - ); - }, - child: ConstrainedBox( - key: Key(state.currentMemeModel.imageLink), - constraints: BoxConstraints( - minWidth: constraints.maxWidth, - ), - child: OctoImage( - image: CachedNetworkImageProvider( - state.currentMemeModel.imageLink, - errorListener: () => - print('error (${state.currentMemeModel.imageLink})'), + duration: const Duration(milliseconds: 100), + layoutBuilder: (curr, prev) { + return Stack( + alignment: Alignment.center, + children: [ + // ...prev, + if (curr != null) curr, + ], + ); + }, + child: FeedImage( + key: Key(widget.state.currentMemeModel.imageLink), + imageProvider: CachedNetworkImageProvider( + widget.state.currentMemeModel.imageLink, + errorListener: () => print( + 'error (${widget.state.currentMemeModel.imageLink})'), cacheManager: serviceLocator(), ), - fit: BoxFit.fitWidth, - width: constraints.maxWidth, + constraints: widget.constraints, ), ), - ), ), ), Positioned.fill( @@ -75,7 +78,7 @@ class PostWidget extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - PostActionsWidget(currentPost: state.currentMemeModel), + PostActionsWidget(currentPost: widget.state.currentMemeModel), ], ), ), @@ -84,7 +87,7 @@ class PostWidget extends StatelessWidget { bottom: 100, width: 400, child: PostDescriptionWidget( - currentPost: state.currentMemeModel, + currentPost: widget.state.currentMemeModel, ), ), ], diff --git a/lib/presentation/widgets/animations/animated_opacity_reverse.dart b/lib/presentation/widgets/animations/animated_opacity_reverse.dart new file mode 100644 index 0000000..ae64309 --- /dev/null +++ b/lib/presentation/widgets/animations/animated_opacity_reverse.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +/// Like [AnimatedOpacity], but with control over [reversedDuration] +class AnimatedVisibilityWithReversedDuration extends StatefulWidget { + /// Constructs AnimatedVisibilityWithReversedDuration + const AnimatedVisibilityWithReversedDuration({ + Key? key, + required this.duration, + required this.visibility, + required this.child, + Duration? reversedDuration, + }) : reversedDuration = reversedDuration ?? duration, + super(key: key); + + final Duration duration; + /// Reversed duration. If null - [duration] will be used + final Duration reversedDuration; + final bool visibility; + final Widget child; + + @override + State createState() => + _AnimatedVisibilityWithReversedDurationState(); +} + +class _AnimatedVisibilityWithReversedDurationState + extends State + with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: widget.reversedDuration, + reverseDuration: widget.duration, + value: 1.0, + ); + } + + @override + void didUpdateWidget( + covariant AnimatedVisibilityWithReversedDuration oldWidget, + ) { + if (oldWidget.visibility != widget.visibility) { + if (widget.visibility) { + controller.forward(); + } else { + controller.reverse(); + } + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: controller, + child: widget.child, + builder: (context, child) { + return Opacity( + opacity: controller.value, + child: child, + ); + }, + ); +} diff --git a/lib/presentation/widgets/platform/platform_icon_button.dart b/lib/presentation/widgets/platform/platform_icon_button.dart index f3e22d4..8823165 100644 --- a/lib/presentation/widgets/platform/platform_icon_button.dart +++ b/lib/presentation/widgets/platform/platform_icon_button.dart @@ -16,7 +16,7 @@ class PlatformIconButton final double size; final Color color; final String text; - final VoidCallback onPressed; + final VoidCallback? onPressed; @override CupertinoButton buildCupertino(BuildContext context) { @@ -52,6 +52,7 @@ class PlatformIconButton ]; return Column( + mainAxisSize: MainAxisSize.min, children: [ Text( String.fromCharCode(icon.codePoint),