Skip to content

Commit 912f9aa

Browse files
author
igor.khramcov
committed
added: AnimatedVisibilityWithReversedDuration widget
added: FeedImage widget to unify result of shown widget in postWidget and feed list added: ability to refresh image on fail (it also do it automatically on switching) changed: platformIconButton onPressed can be null
1 parent 14976b9 commit 912f9aa

File tree

5 files changed

+174
-46
lines changed

5 files changed

+174
-46
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:devour/presentation/widgets/platform/platform_icon_button.dart';
2+
import 'package:flutter/cupertino.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:lottie/lottie.dart';
6+
import 'package:octo_image/octo_image.dart';
7+
8+
/// A widget to show image from network or cache with correct size
9+
class FeedImage extends StatelessWidget {
10+
/// Construct FeedImag
11+
const FeedImage({
12+
Key? key,
13+
this.constraints,
14+
required this.imageProvider,
15+
this.onRefreshPressed,
16+
}) : super(key: key);
17+
18+
final BoxConstraints? constraints;
19+
final ImageProvider imageProvider;
20+
/// Callback fired, when user taps on error builder, when image is failed to load
21+
final VoidCallback? onRefreshPressed;
22+
23+
@override
24+
Widget build(BuildContext context) {
25+
return ConstrainedBox(
26+
constraints: BoxConstraints(
27+
minHeight: 200,
28+
maxWidth: constraints?.maxWidth ?? double.infinity,
29+
),
30+
child: OctoImage(
31+
// key: widget.key,
32+
image: imageProvider,
33+
fit: BoxFit.fitWidth,
34+
progressIndicatorBuilder: (ctx, prgrs) => SizedBox(
35+
height: 200,
36+
child: Lottie.asset(
37+
'lib/assets/animations/space_loader.json',
38+
),
39+
),
40+
errorBuilder: (ctx, error, stack) => SizedBox(
41+
height: 200,
42+
child: Center(
43+
child: PlatformIconButton(
44+
icon: Icons.refresh,
45+
onPressed: onRefreshPressed,
46+
color: CupertinoColors.white,
47+
size: 24,
48+
text: 'failed to load 🤬',
49+
),
50+
),
51+
),
52+
),
53+
);
54+
}
55+
}

lib/presentation/screens/feed/feed_widget.dart

+18-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import 'dart:math';
21
import 'dart:ui';
32

43
import 'package:cached_network_image/cached_network_image.dart';
54
import 'package:devour/application/feed/bloc/feed_bloc.dart';
65
import 'package:devour/injection.dart';
6+
import 'package:devour/presentation/screens/feed/feed_image_widget.dart';
77
import 'package:devour/presentation/screens/feed/feed_scroll_physics.dart';
88
import 'package:devour/presentation/screens/feed/post_widget.dart';
9+
import 'package:devour/presentation/widgets/animations/animated_opacity_reverse.dart';
910
import 'package:flutter/cupertino.dart';
1011
import 'package:flutter/material.dart';
1112
import 'package:flutter/rendering.dart';
@@ -14,7 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
1415
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
1516
import 'package:fpdart/fpdart.dart' show Option;
1617
import 'package:lottie/lottie.dart';
17-
import 'package:octo_image/octo_image.dart';
1818
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
1919
import 'package:collection/collection.dart';
2020

@@ -111,7 +111,6 @@ class _FeedWidgetState extends State<FeedWidget> {
111111
state.memes[index].imageLink,
112112
cacheManager: cacheManager,
113113
);
114-
115114
final key = renderedMemesKeys.putIfAbsent(
116115
index,
117116
() => GlobalKey(),
@@ -126,20 +125,22 @@ class _FeedWidgetState extends State<FeedWidget> {
126125
renderedMemes[index] = Size(cnstr.maxWidth, 200);
127126
}
128127

129-
return ConstrainedBox(
130-
constraints: BoxConstraints(
131-
minHeight: 200,
132-
// maxHeight: maxHeight,
133-
maxWidth: cnstr.maxWidth,
134-
),
135-
child: OctoImage(
128+
// Using [AnimatedVisibilityWithReversedDuration] to
129+
// hide with animation selected post and show immediately
130+
// if its not. Its because of postWidget, it will re-render
131+
// any error images, so they will reload, and this and post
132+
// widgets will have different state, but overlaping each other
133+
return AnimatedVisibilityWithReversedDuration(
134+
duration: const Duration(milliseconds: 300),
135+
reversedDuration: Duration.zero,
136+
visibility: index != state.iterator,
137+
child: FeedImage(
136138
key: key,
137-
image: imageProvider,
138-
fit: BoxFit.fitWidth,
139-
progressIndicatorBuilder: (context, progress) =>
140-
Lottie.asset(
141-
'lib/assets/animations/space_loader.json',
142-
),
139+
imageProvider: imageProvider,
140+
constraints: cnstr,
141+
onRefreshPressed: () => setState(() {
142+
renderedMemesKeys[index] = GlobalKey();
143+
}),
143144
),
144145
);
145146
},
@@ -215,6 +216,7 @@ class _FeedWidgetState extends State<FeedWidget> {
215216
WidgetsBinding.instance!.addPostFrameCallback(
216217
(_) => setState(() {
217218
renderedMemes[index] = size;
219+
print('index $index is $size');
218220
imageProvider
219221
.resolve(ImageConfiguration.empty)
220222
.removeListener(listener);

lib/presentation/screens/feed/post_widget.dart

+32-29
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import 'package:devour/application/feed/bloc/feed_bloc.dart';
55
import 'package:devour/domain/meme/abstract_meme_model.dart';
66
import 'package:devour/domain/misc/helper.dart';
77
import 'package:devour/injection.dart';
8+
import 'package:devour/presentation/screens/feed/feed_image_widget.dart';
89
import 'package:devour/presentation/widgets/platform/platform_icon_button.dart';
910
import 'package:flutter/cupertino.dart';
1011
import 'package:flutter/material.dart';
1112
import 'package:flutter/widgets.dart';
1213
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
13-
import 'package:octo_image/octo_image.dart';
1414

15-
class PostWidget extends StatelessWidget {
15+
/// Widget to show post information like description and likes and image,
16+
/// to create illusion of selecting meme from list.
17+
class PostWidget extends StatefulWidget {
18+
/// Constructs PostWidget
1619
const PostWidget(
1720
this.state, {
1821
required this.constraints,
@@ -22,13 +25,19 @@ class PostWidget extends StatelessWidget {
2225
final FeedState state;
2326
final BoxConstraints constraints;
2427

28+
@override
29+
State<PostWidget> createState() => _PostWidgetState();
30+
}
31+
32+
class _PostWidgetState extends State<PostWidget> {
2533
@override
2634
Widget build(BuildContext context) {
27-
if (state.isLoading) {
35+
if (widget.state.isLoading) {
2836
return Container();
2937
}
3038

31-
final key = state.currentMemeWidget.toNullable();
39+
final key = widget.state.currentMemeWidget.toNullable();
40+
// key.currentWidget
3241
final box = key?.currentContext?.findRenderObject() as RenderBox?;
3342
final pos = box?.localToGlobal(Offset.zero) ?? Offset.zero;
3443

@@ -41,41 +50,35 @@ class PostWidget extends StatelessWidget {
4150
height: box?.size.height,
4251
child: IgnorePointer(
4352
child: AnimatedSwitcher(
44-
duration: const Duration(milliseconds: 100),
45-
layoutBuilder: (curr, prev) {
46-
return Stack(
47-
alignment: Alignment.center,
48-
children: <Widget>[
49-
// ...prev,
50-
if (curr != null) curr,
51-
],
52-
);
53-
},
54-
child: ConstrainedBox(
55-
key: Key(state.currentMemeModel.imageLink),
56-
constraints: BoxConstraints(
57-
minWidth: constraints.maxWidth,
58-
),
59-
child: OctoImage(
60-
image: CachedNetworkImageProvider(
61-
state.currentMemeModel.imageLink,
62-
errorListener: () =>
63-
print('error (${state.currentMemeModel.imageLink})'),
53+
duration: const Duration(milliseconds: 100),
54+
layoutBuilder: (curr, prev) {
55+
return Stack(
56+
alignment: Alignment.center,
57+
children: <Widget>[
58+
// ...prev,
59+
if (curr != null) curr,
60+
],
61+
);
62+
},
63+
child: FeedImage(
64+
key: Key(widget.state.currentMemeModel.imageLink),
65+
imageProvider: CachedNetworkImageProvider(
66+
widget.state.currentMemeModel.imageLink,
67+
errorListener: () => print(
68+
'error (${widget.state.currentMemeModel.imageLink})'),
6469
cacheManager: serviceLocator<CacheManager>(),
6570
),
66-
fit: BoxFit.fitWidth,
67-
width: constraints.maxWidth,
71+
constraints: widget.constraints,
6872
),
6973
),
70-
),
7174
),
7275
),
7376
Positioned.fill(
7477
top: 100,
7578
child: Row(
7679
mainAxisAlignment: MainAxisAlignment.end,
7780
children: [
78-
PostActionsWidget(currentPost: state.currentMemeModel),
81+
PostActionsWidget(currentPost: widget.state.currentMemeModel),
7982
],
8083
),
8184
),
@@ -84,7 +87,7 @@ class PostWidget extends StatelessWidget {
8487
bottom: 100,
8588
width: 400,
8689
child: PostDescriptionWidget(
87-
currentPost: state.currentMemeModel,
90+
currentPost: widget.state.currentMemeModel,
8891
),
8992
),
9093
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// Like [AnimatedOpacity], but with control over [reversedDuration]
4+
class AnimatedVisibilityWithReversedDuration extends StatefulWidget {
5+
/// Constructs AnimatedVisibilityWithReversedDuration
6+
const AnimatedVisibilityWithReversedDuration({
7+
Key? key,
8+
required this.duration,
9+
required this.visibility,
10+
required this.child,
11+
Duration? reversedDuration,
12+
}) : reversedDuration = reversedDuration ?? duration,
13+
super(key: key);
14+
15+
final Duration duration;
16+
/// Reversed duration. If null - [duration] will be used
17+
final Duration reversedDuration;
18+
final bool visibility;
19+
final Widget child;
20+
21+
@override
22+
State<AnimatedVisibilityWithReversedDuration> createState() =>
23+
_AnimatedVisibilityWithReversedDurationState();
24+
}
25+
26+
class _AnimatedVisibilityWithReversedDurationState
27+
extends State<AnimatedVisibilityWithReversedDuration>
28+
with TickerProviderStateMixin {
29+
late AnimationController controller;
30+
31+
@override
32+
void initState() {
33+
super.initState();
34+
controller = AnimationController(
35+
vsync: this,
36+
duration: widget.reversedDuration,
37+
reverseDuration: widget.duration,
38+
value: 1.0,
39+
);
40+
}
41+
42+
@override
43+
void didUpdateWidget(
44+
covariant AnimatedVisibilityWithReversedDuration oldWidget,
45+
) {
46+
if (oldWidget.visibility != widget.visibility) {
47+
if (widget.visibility) {
48+
controller.forward();
49+
} else {
50+
controller.reverse();
51+
}
52+
}
53+
super.didUpdateWidget(oldWidget);
54+
}
55+
56+
@override
57+
Widget build(BuildContext context) => AnimatedBuilder(
58+
animation: controller,
59+
child: widget.child,
60+
builder: (context, child) {
61+
return Opacity(
62+
opacity: controller.value,
63+
child: child,
64+
);
65+
},
66+
);
67+
}

lib/presentation/widgets/platform/platform_icon_button.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class PlatformIconButton
1616
final double size;
1717
final Color color;
1818
final String text;
19-
final VoidCallback onPressed;
19+
final VoidCallback? onPressed;
2020

2121
@override
2222
CupertinoButton buildCupertino(BuildContext context) {
@@ -52,6 +52,7 @@ class PlatformIconButton
5252
];
5353

5454
return Column(
55+
mainAxisSize: MainAxisSize.min,
5556
children: [
5657
Text(
5758
String.fromCharCode(icon.codePoint),

0 commit comments

Comments
 (0)