210 lines
5.0 KiB
Dart
210 lines
5.0 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
mixin BannerCarouselMixin<T extends StatefulWidget>
|
|
on State<T>, WidgetsBindingObserver {
|
|
final PageController pageController = PageController();
|
|
Timer? _autoPlayTimer;
|
|
Timer? _resumeTimer;
|
|
int currentIndex = 0;
|
|
bool hovering = false;
|
|
bool _manualPaused = false;
|
|
bool _appPaused = false;
|
|
bool _autoPageChangeInProgress = false;
|
|
double? lastBannerWidth;
|
|
bool _tickerVisible = true;
|
|
|
|
static const autoInterval = Duration(seconds: 5);
|
|
static const _manualPauseDuration = Duration(seconds: 8);
|
|
static const slideDuration = Duration(milliseconds: 450);
|
|
static const slideCurve = Curves.easeInOutCubic;
|
|
|
|
int get itemCount;
|
|
bool get isActive;
|
|
void emitActiveItem();
|
|
void precacheAdjacentSlides();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
emitActiveItem();
|
|
syncAutoPlay();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_autoPlayTimer?.cancel();
|
|
_resumeTimer?.cancel();
|
|
pageController.dispose();
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
final visible = TickerMode.of(context);
|
|
if (visible == _tickerVisible) return;
|
|
_tickerVisible = visible;
|
|
if (visible) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted || !_tickerVisible) return;
|
|
emitActiveItem();
|
|
precacheAdjacentSlides();
|
|
});
|
|
syncAutoPlay();
|
|
} else {
|
|
stopAutoPlay();
|
|
_resumeTimer?.cancel();
|
|
_manualPaused = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
final paused =
|
|
state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.inactive ||
|
|
state == AppLifecycleState.hidden;
|
|
if (paused == _appPaused) return;
|
|
_appPaused = paused;
|
|
paused ? stopAutoPlay() : syncAutoPlay();
|
|
}
|
|
|
|
bool get shouldAutoPlay =>
|
|
mounted &&
|
|
isActive &&
|
|
_tickerVisible &&
|
|
!_appPaused &&
|
|
!hovering &&
|
|
!_manualPaused &&
|
|
itemCount > 1;
|
|
|
|
void syncAutoPlay() {
|
|
if (!shouldAutoPlay) {
|
|
stopAutoPlay();
|
|
return;
|
|
}
|
|
_autoPlayTimer ??= Timer.periodic(
|
|
autoInterval,
|
|
(_) => nextPage(manual: false),
|
|
);
|
|
}
|
|
|
|
void stopAutoPlay() {
|
|
_autoPlayTimer?.cancel();
|
|
_autoPlayTimer = null;
|
|
}
|
|
|
|
|
|
void setHovering(bool value) {
|
|
hovering = value;
|
|
value ? stopAutoPlay() : syncAutoPlay();
|
|
if (mounted) setState(() {});
|
|
}
|
|
|
|
void pauseForManual() {
|
|
if (!_manualPaused) setState(() => _manualPaused = true);
|
|
stopAutoPlay();
|
|
_resumeTimer?.cancel();
|
|
_resumeTimer = Timer(_manualPauseDuration, () {
|
|
if (!mounted) return;
|
|
setState(() => _manualPaused = false);
|
|
syncAutoPlay();
|
|
});
|
|
}
|
|
|
|
void onPageChanged(int index) {
|
|
setState(() => currentIndex = index);
|
|
emitActiveItem();
|
|
precacheAdjacentSlides();
|
|
if (_autoPageChangeInProgress) {
|
|
_autoPageChangeInProgress = false;
|
|
return;
|
|
}
|
|
pauseForManual();
|
|
}
|
|
|
|
void goToPage(int index) {
|
|
if (index < 0 || index >= itemCount) return;
|
|
if (!pageController.hasClients) return;
|
|
pageController.animateToPage(
|
|
index,
|
|
duration: slideDuration,
|
|
curve: slideCurve,
|
|
);
|
|
pauseForManual();
|
|
}
|
|
|
|
void nextPage({bool manual = true}) {
|
|
if (itemCount <= 1) return;
|
|
if (!pageController.hasClients) return;
|
|
final nextIndex = (currentIndex + 1) % itemCount;
|
|
if (!manual) _autoPageChangeInProgress = true;
|
|
pageController.animateToPage(
|
|
nextIndex,
|
|
duration: slideDuration,
|
|
curve: slideCurve,
|
|
);
|
|
if (manual) pauseForManual();
|
|
}
|
|
|
|
void prevPage() {
|
|
if (itemCount <= 1) return;
|
|
if (!pageController.hasClients) return;
|
|
final previousIndex = (currentIndex - 1 + itemCount) % itemCount;
|
|
pageController.animateToPage(
|
|
previousIndex,
|
|
duration: slideDuration,
|
|
curve: slideCurve,
|
|
);
|
|
pauseForManual();
|
|
}
|
|
|
|
int? bannerCacheWidth() {
|
|
final width = lastBannerWidth;
|
|
if (width == null || !width.isFinite || width <= 0) return null;
|
|
return (width * MediaQuery.devicePixelRatioOf(context)).ceil();
|
|
}
|
|
|
|
|
|
void resetCarouselItems() {
|
|
stopAutoPlay();
|
|
_resumeTimer?.cancel();
|
|
_manualPaused = false;
|
|
if (itemCount > 0 && pageController.hasClients) {
|
|
pageController.jumpToPage(0);
|
|
}
|
|
currentIndex = 0;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
emitActiveItem();
|
|
precacheAdjacentSlides();
|
|
syncAutoPlay();
|
|
});
|
|
}
|
|
|
|
|
|
void activateCarousel() {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
emitActiveItem();
|
|
precacheAdjacentSlides();
|
|
syncAutoPlay();
|
|
});
|
|
}
|
|
|
|
|
|
void deactivateCarousel() {
|
|
stopAutoPlay();
|
|
_resumeTimer?.cancel();
|
|
_manualPaused = false;
|
|
}
|
|
}
|