feat(#173): dégradés haut/bas sur la liste de bulles scrollable
Même logique que le slider enfants : fondu aux bords sauf au premier et au dernier élément. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,17 +22,32 @@ class CartesFeed extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CartesFeedState extends State<CartesFeed> {
|
class _CartesFeedState extends State<CartesFeed> {
|
||||||
|
static const double _fadeExtent = 0.08;
|
||||||
|
|
||||||
List<CarteBulle> _items = const [];
|
List<CarteBulle> _items = const [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
String? _error;
|
String? _error;
|
||||||
String? _busyCardId;
|
String? _busyCardId;
|
||||||
|
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
bool _isScrollable = false;
|
||||||
|
bool _fadeTop = false;
|
||||||
|
bool _fadeBottom = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_scrollController.addListener(_syncScrollFades);
|
||||||
_load();
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.removeListener(_syncScrollFades);
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant CartesFeed oldWidget) {
|
void didUpdateWidget(covariant CartesFeed oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@@ -41,6 +56,25 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _syncScrollFades() {
|
||||||
|
if (!_scrollController.hasClients) return;
|
||||||
|
final position = _scrollController.position;
|
||||||
|
final scrollable = position.maxScrollExtent > 0.0;
|
||||||
|
final threshold = position.viewportDimension * _fadeExtent / 2;
|
||||||
|
final fadeTop = scrollable && position.pixels > threshold;
|
||||||
|
final fadeBottom = scrollable &&
|
||||||
|
position.pixels < (position.maxScrollExtent - threshold);
|
||||||
|
if (scrollable != _isScrollable ||
|
||||||
|
fadeTop != _fadeTop ||
|
||||||
|
fadeBottom != _fadeBottom) {
|
||||||
|
setState(() {
|
||||||
|
_isScrollable = scrollable;
|
||||||
|
_fadeTop = fadeTop;
|
||||||
|
_fadeBottom = fadeBottom;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_loading = true;
|
_loading = true;
|
||||||
@@ -55,6 +89,7 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
_items = items;
|
_items = items;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _syncScrollFades());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -304,10 +339,52 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return NotificationListener<ScrollMetricsNotification>(
|
||||||
|
onNotification: (_) {
|
||||||
|
_syncScrollFades();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ShaderMask(
|
||||||
|
blendMode: BlendMode.dstIn,
|
||||||
|
shaderCallback: (Rect bounds) {
|
||||||
|
final stops = <double>[
|
||||||
|
0.0,
|
||||||
|
_fadeExtent,
|
||||||
|
1.0 - _fadeExtent,
|
||||||
|
1.0,
|
||||||
|
];
|
||||||
|
if (!_isScrollable) {
|
||||||
|
return const LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[
|
||||||
|
Colors.black,
|
||||||
|
Colors.black,
|
||||||
|
Colors.black,
|
||||||
|
Colors.black,
|
||||||
|
],
|
||||||
|
stops: [0.0, 0.08, 0.92, 1.0],
|
||||||
|
).createShader(bounds);
|
||||||
|
}
|
||||||
|
final topMask = _fadeTop ? Colors.transparent : Colors.black;
|
||||||
|
final bottomMask = _fadeBottom ? Colors.transparent : Colors.black;
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[
|
||||||
|
topMask,
|
||||||
|
Colors.black,
|
||||||
|
Colors.black,
|
||||||
|
bottomMask,
|
||||||
|
],
|
||||||
|
stops: stops,
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
child: RefreshIndicator(
|
||||||
onRefresh: _load,
|
onRefresh: _load,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||||
itemCount: _items.length,
|
itemCount: _items.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -321,6 +398,8 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user