Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 6fb43e7

Browse files
committed
Make ImageCarousel shared across various UI theme
1 parent dabb254 commit 6fb43e7

File tree

4 files changed

+400
-0
lines changed

4 files changed

+400
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
/// An enumerable class for specifying preference of indicator in [ImageCarousel].
4+
///
5+
/// It inherited by two classes with custom definitions of [style]
6+
/// to satisify apperences which namely [DotPageIndication]
7+
/// and [TextPageIndication]. Then, assign one of them into
8+
/// [ImageCarouselPreferences.pageIndication].
9+
@immutable
10+
sealed class ImageCarouselPageIndication<
11+
T extends ImageCarouselPageIndicationStyle
12+
> {
13+
/// Style preference of applied indication widgets.
14+
///
15+
/// Generic parameter [T] must be extended from [ImageCarouselPageIndicationStyle].
16+
final T style;
17+
18+
const ImageCarouselPageIndication._(this.style);
19+
}
20+
21+
/// Specify styles apperences for specific [ImageCarouselPageIndication].
22+
///
23+
/// It only comes with two nullable [Color] properties
24+
/// depending on applied preferences according to
25+
/// child classes documentations.
26+
@immutable
27+
abstract final class ImageCarouselPageIndicationStyle {
28+
/// A [Color] used for indicating activated or foreground colour.
29+
final Color? primaryColour;
30+
31+
/// A [Color] used for indicating inactive or background colour.
32+
final Color? secondaryColour;
33+
34+
/// Construct foundation preferences themeing for [ImageCarouselPageIndication].
35+
const ImageCarouselPageIndicationStyle({
36+
this.primaryColour,
37+
this.secondaryColour,
38+
});
39+
}
40+
41+
/// One of the [ImageCarouselPageIndication] preference which display pages
42+
/// indication as dot view.
43+
///
44+
/// When it assigned into [ImageCarouselPreferences.pageIndication],
45+
/// the indicator which rendered at the bottom of [ImageCarousel]
46+
/// will be adopted by [PageViewDotIndicator].
47+
final class DotPageIndication
48+
extends ImageCarouselPageIndication<DotPageIndicationStyle> {
49+
/// Specify [ImageCarousel] to render page indicator with
50+
/// [PageViewDotIndicator] with applied [style].
51+
///
52+
/// For applied [Color] in given [style], [DotPageIndicationStyle.primaryColour]
53+
/// uses as [PageViewDotIndicator.selectedColor] and
54+
/// [DotPageIndicationStyle.secondaryColour] uses as [PageViewDotIndicator.unselectedColor]
55+
/// respectedly.
56+
const DotPageIndication({
57+
DotPageIndicationStyle style = const DotPageIndicationStyle(),
58+
}) : super._(style);
59+
}
60+
61+
/// Style preference for [DotPageIndication] which applied from
62+
/// [PageViewDotIndicator].
63+
final class DotPageIndicationStyle extends ImageCarouselPageIndicationStyle {
64+
static const Size _DEFAULT_SIZE = Size(12, 12);
65+
66+
/// Set [Size] of indicator when reaching current index.
67+
///
68+
/// Default value is `12*12`.
69+
final Size size;
70+
71+
/// Apply theme preference in [DotPageIndication].
72+
const DotPageIndicationStyle({
73+
super.primaryColour,
74+
super.secondaryColour,
75+
this.size = _DEFAULT_SIZE
76+
});
77+
}
78+
79+
/// One of the [ImageCarouselPageIndication] preference which display pages
80+
/// indication as dot view.
81+
///
82+
/// When it assigned into [ImageCarouselPreferences.pageIndication],
83+
/// the indicator which rendered at the bottom of [ImageCarousel]
84+
/// will be adopted by [Text] which wrapped by [Container] for
85+
/// additional theme preferences.
86+
final class TextPageIndication
87+
extends ImageCarouselPageIndication<TextPageIndicationStyle> {
88+
/// Specify [ImageCarousel] to render page indicator with
89+
/// [Text] and [Container] with applied [style].
90+
///
91+
/// For applied [Color] in given [style], [TextPageIndicationStyle.primaryColour]
92+
/// uses as [TextStyle.color] of the [Text] and
93+
/// [DotPageIndicationStyle.secondaryColour] uses as
94+
/// background colour of [Container].
95+
const TextPageIndication({
96+
TextPageIndicationStyle style = const TextPageIndicationStyle(),
97+
}) : super._(style);
98+
}
99+
100+
/// Style preference for [TextPageIndication] which applied from
101+
/// [PageViewDotIndicator].
102+
final class TextPageIndicationStyle extends ImageCarouselPageIndicationStyle {
103+
/// Determine size of the font.
104+
///
105+
/// Default value is `10`.
106+
final double fontSize;
107+
108+
/// Specify weight of the font.
109+
///
110+
/// Default value is [FontWeight.w300].
111+
final FontWeight fontWeight;
112+
113+
/// Opacity of [Container].
114+
///
115+
/// If the original [Color] applied [Color.withValues] already,
116+
/// it will be overridden to [opacity] value.
117+
///
118+
/// Default value is `0.7`.
119+
final double opacity;
120+
121+
/// Specify padding of [Text] inside the [Container].
122+
final EdgeInsetsGeometry padding;
123+
124+
/// Apply theme preference in [TextPageIndication].
125+
const TextPageIndicationStyle({
126+
super.primaryColour,
127+
super.secondaryColour,
128+
this.fontSize = 10,
129+
this.fontWeight = FontWeight.w300,
130+
this.opacity = 0.7,
131+
this.padding = const EdgeInsets.symmetric(horizontal: 18, vertical: 4),
132+
});
133+
}
134+
135+
/// Define preferences for visualizing [ImageCarousel].
136+
@immutable
137+
class ImageCarouselPreferences {
138+
/// Define size of control icon for changing pages.
139+
///
140+
/// Default value is `18`.
141+
final double controlIconSize;
142+
143+
/// Specify spaces of control buttons from nearest sides.
144+
///
145+
/// Default defines as `5`.
146+
final double controlButtonsSpacing;
147+
148+
/// Specify [Duration] of animating page changes when event
149+
/// triggered.
150+
///
151+
/// Default value is `500 ms`.
152+
final Duration pageChangeDuration;
153+
154+
/// Define animation behaviour during [pageChangeDuration].
155+
///
156+
/// By default, it uses [Curves.easeInOut].
157+
final Curve pageChangeCurve;
158+
159+
/// Override a [Color] for displaying control icons if applied.
160+
final Color? controlIconColour;
161+
162+
/// Set [Duration] for perform animations to show or hide
163+
/// control widgets.
164+
///
165+
/// Default value is 250 miliseconds.
166+
final Duration showControlDuration;
167+
168+
/// Determine [Curve] of fading control widgets.
169+
///
170+
/// Default value is [Curves.easeOutSine].
171+
final Curve showControlCurve;
172+
173+
/// Determine visible [Duration] of control widgets if
174+
/// it triggered by tapping [ImageCarousel] area.
175+
///
176+
/// Default value is 5 seconds.
177+
final Duration controlVisibleDuration;
178+
179+
/// Determine which widget used for displaying
180+
/// page indication.
181+
///
182+
/// The possible value is either [DotPageIndication] (as default)
183+
/// or [TextPageIndication].
184+
final ImageCarouselPageIndication pageIndication;
185+
186+
/// Create preference of visualizing [ImageCarousel].
187+
const ImageCarouselPreferences({
188+
this.controlIconSize = 18,
189+
this.controlButtonsSpacing = 5,
190+
this.pageChangeDuration = const Duration(milliseconds: 500),
191+
this.pageChangeCurve = Curves.easeInOut,
192+
this.showControlDuration = const Duration(milliseconds: 250),
193+
this.showControlCurve = Curves.easeOutSine,
194+
this.controlVisibleDuration = const Duration(seconds: 5),
195+
this.pageIndication = const DotPageIndication(),
196+
this.controlIconColour,
197+
});
198+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/widgets.dart';
5+
6+
import 'package:oghref_model/model.dart' as oghref;
7+
8+
import '../image_loading_event.dart';
9+
import 'style.dart';
10+
11+
/// Reviewing multiple images provided from [oghref.ImageInfo] and
12+
/// display all images in a single carousel [Widget].
13+
///
14+
/// Every images appeared in [ImageCarousel] will be cached already
15+
/// that reducing duration of redownloading assets when same images
16+
/// content will be displayed again.
17+
abstract base class ImageCarousel extends StatefulWidget {
18+
/// Images metadata from [MetaInfo.images] in a single site.
19+
final List<oghref.ImageInfo> images;
20+
21+
/// Uses [oghref.ImageInfo.secureUrl] instead of default value if available.
22+
///
23+
/// This options is enabled by default in case with fulfillment
24+
/// of browsers' security policy when deploying Flutter web with
25+
/// `HTTPS` hosting. Eventhough there is no difference for running
26+
/// in native platform, it is recommended that keep this options
27+
/// enabled to prevents man-in-middle attack.
28+
final bool preferHTTPS;
29+
30+
/// Determine render preferences applied into this [ImageCarousel].
31+
final ImageCarouselPreferences preferences;
32+
33+
/// Construct a new carousel [Widget] for displaying [images].
34+
ImageCarousel(
35+
this.images, {
36+
this.preferHTTPS = true,
37+
this.preferences = const ImageCarouselPreferences(),
38+
super.key,
39+
});
40+
41+
@override
42+
State<ImageCarousel> createState();
43+
}
44+
45+
enum _ShowControlMode {
46+
mouseHover(true),
47+
tapping(true),
48+
none(false);
49+
50+
final bool showControl;
51+
52+
const _ShowControlMode(this.showControl);
53+
}
54+
55+
/// [State] of [ImageCarousel] for implemeting carousel widget under various UI design.
56+
///
57+
///
58+
abstract base class ImageCarouselState<T extends ImageCarousel> extends State<T>
59+
implements ImageLoadingEvent {
60+
/// A controller uses for changing carousel content.
61+
late final PageController controller;
62+
63+
/// A [Future] wrapper of [controller] that it avoids build error due to multiple build request.
64+
///
65+
/// The value of this [Future] object is identical with [controller].
66+
@protected
67+
late final Future<PageController> deferredCtrl;
68+
69+
late _ShowControlMode _showControlMode;
70+
71+
Timer? _tappedFadeoutCallback;
72+
73+
@mustCallSuper
74+
@override
75+
void initState() {
76+
controller = PageController();
77+
super.initState();
78+
_showControlMode = _ShowControlMode.none;
79+
deferredCtrl = Future.value(controller);
80+
}
81+
82+
@mustCallSuper
83+
@override
84+
void dispose() {
85+
controller.dispose();
86+
super.dispose();
87+
}
88+
89+
void movePrevious() async {
90+
await controller.previousPage(
91+
duration: widget.preferences.pageChangeDuration,
92+
curve: widget.preferences.pageChangeCurve,
93+
);
94+
setState(() {});
95+
}
96+
97+
void moveNext() async {
98+
await controller.nextPage(
99+
duration: widget.preferences.pageChangeDuration,
100+
curve: widget.preferences.pageChangeCurve,
101+
);
102+
setState(() {});
103+
}
104+
105+
@protected
106+
@nonVirtual
107+
Widget buildSingleImage(BuildContext context, oghref.ImageInfo imgInfo) {
108+
Uri? destination = imgInfo.url;
109+
110+
if (widget.preferHTTPS && imgInfo.secureUrl != null) {
111+
destination = imgInfo.secureUrl;
112+
}
113+
114+
return Image.network(
115+
destination!.toString(),
116+
fit: BoxFit.contain,
117+
headers: {"user-agent": oghref.MetaFetch.userAgentString},
118+
errorBuilder: onErrorImageLoading,
119+
loadingBuilder: onLoadingImage,
120+
semanticLabel: imgInfo.alt,
121+
);
122+
}
123+
124+
@protected
125+
AnimatedOpacity buildControlWidgets(
126+
BuildContext context, {
127+
required Widget child,
128+
}) {
129+
return AnimatedOpacity(
130+
opacity: _showControlMode.showControl ? 1 : 0,
131+
duration: widget.preferences.showControlDuration,
132+
curve: widget.preferences.showControlCurve,
133+
child: child,
134+
);
135+
}
136+
137+
@protected
138+
FutureBuilder<PageController> buildWithDeferredCtrl(
139+
BuildContext context, {
140+
required AsyncWidgetBuilder<PageController> builder,
141+
}) {
142+
return FutureBuilder(future: deferredCtrl, builder: builder);
143+
}
144+
145+
@protected
146+
Widget buildCarouselContext(BuildContext context, int maxImgIdx);
147+
148+
@override
149+
Widget build(BuildContext context) {
150+
final int maxImgIdx = widget.images.length - 1;
151+
152+
return FocusableActionDetector(
153+
onShowHoverHighlight: (isHover) {
154+
// If fadeout function callback is assigned, cancel immediately.
155+
_tappedFadeoutCallback?.cancel();
156+
setState(() {
157+
_showControlMode = isHover
158+
? _ShowControlMode.mouseHover
159+
: _ShowControlMode.none;
160+
});
161+
},
162+
child: GestureDetector(
163+
onTap: () {
164+
if (_showControlMode == _ShowControlMode.mouseHover) {
165+
// Do not process if shown due to mouse hovering.
166+
return;
167+
}
168+
169+
_tappedFadeoutCallback?.cancel();
170+
setState(() {
171+
_showControlMode = _ShowControlMode.tapping;
172+
_tappedFadeoutCallback = Timer(
173+
widget.preferences.controlVisibleDuration,
174+
() {
175+
setState(() {
176+
_showControlMode = _ShowControlMode.none;
177+
});
178+
},
179+
);
180+
});
181+
},
182+
child: buildCarouselContext(context, maxImgIdx),
183+
),
184+
);
185+
}
186+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
abstract interface class ImageLoadingEvent {
4+
@protected
5+
Widget onLoadingImage(
6+
BuildContext context, Widget child, ImageChunkEvent? loadingProcess);
7+
8+
@protected
9+
Widget onErrorImageLoading(
10+
BuildContext context, Object error, StackTrace? stackTrace);
11+
}

0 commit comments

Comments
 (0)