299 lines
10 KiB
Dart
299 lines
10 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Flutter widget for displaying fading edge at start/end of scroll views
|
|
class SCFadingEdgeScrollView extends StatefulWidget {
|
|
/// child widget
|
|
final Widget child;
|
|
|
|
/// scroll controller of child widget
|
|
///
|
|
/// Look for more documentation at [ScrollView.scrollController]
|
|
final ScrollController scrollController;
|
|
|
|
/// Whether the scroll view scrolls in the reading direction.
|
|
///
|
|
/// Look for more documentation at [ScrollView.reverse]
|
|
final bool reverse;
|
|
|
|
/// The axis along which child view scrolls
|
|
///
|
|
/// Look for more documentation at [ScrollView.scrollDirection]
|
|
final Axis scrollDirection;
|
|
|
|
/// what part of screen on start half should be covered by fading edge gradient
|
|
/// [gradientFractionOnStart] must be 0 <= [gradientFractionOnStart] <= 1
|
|
/// 0 means no gradient,
|
|
/// 1 means gradients on start half of widget fully covers it
|
|
final double gradientFractionOnStart;
|
|
|
|
/// what part of screen on end half should be covered by fading edge gradient
|
|
/// [gradientFractionOnEnd] must be 0 <= [gradientFractionOnEnd] <= 1
|
|
/// 0 means no gradient,
|
|
/// 1 means gradients on start half of widget fully covers it
|
|
final double gradientFractionOnEnd;
|
|
|
|
/// set to true if you want scrollController passed to widget to be disposed when widget's state is disposed
|
|
final bool shouldDisposeScrollController;
|
|
|
|
const SCFadingEdgeScrollView._internal({
|
|
Key? key,
|
|
required this.child,
|
|
required this.scrollController,
|
|
required this.reverse,
|
|
required this.scrollDirection,
|
|
required this.gradientFractionOnStart,
|
|
required this.gradientFractionOnEnd,
|
|
required this.shouldDisposeScrollController,
|
|
}) : assert(child != null),
|
|
assert(scrollController != null),
|
|
assert(reverse != null),
|
|
assert(scrollDirection != null),
|
|
assert(gradientFractionOnStart >= 0 && gradientFractionOnStart <= 1),
|
|
assert(gradientFractionOnEnd >= 0 && gradientFractionOnEnd <= 1),
|
|
super(key: key);
|
|
|
|
/// Constructor for creating [SCFadingEdgeScrollView] with [ScrollView] as child
|
|
/// child must have [ScrollView.controller] set
|
|
factory SCFadingEdgeScrollView.fromScrollView({
|
|
Key? key,
|
|
required ScrollView child,
|
|
double gradientFractionOnStart = 0.1,
|
|
double gradientFractionOnEnd = 0.1,
|
|
bool shouldDisposeScrollController = false,
|
|
}) {
|
|
assert(child.controller != null, "Child must have controller set");
|
|
|
|
return SCFadingEdgeScrollView._internal(
|
|
key: key,
|
|
child: child,
|
|
scrollController: child.controller!,
|
|
scrollDirection: child.scrollDirection,
|
|
reverse: child.reverse,
|
|
gradientFractionOnStart: gradientFractionOnStart,
|
|
gradientFractionOnEnd: gradientFractionOnEnd,
|
|
shouldDisposeScrollController: shouldDisposeScrollController,
|
|
);
|
|
}
|
|
|
|
/// Constructor for creating [SCFadingEdgeScrollView] with [SingleChildScrollView] as child
|
|
/// child must have [SingleChildScrollView.controller] set
|
|
factory SCFadingEdgeScrollView.fromSingleChildScrollView({
|
|
Key? key,
|
|
required SingleChildScrollView child,
|
|
double gradientFractionOnStart = 0.1,
|
|
double gradientFractionOnEnd = 0.1,
|
|
bool shouldDisposeScrollController = false,
|
|
}) {
|
|
assert(child.controller != null, "Child must have controller set");
|
|
|
|
return SCFadingEdgeScrollView._internal(
|
|
key: key,
|
|
child: child,
|
|
scrollController: child.controller!,
|
|
scrollDirection: child.scrollDirection,
|
|
reverse: child.reverse,
|
|
gradientFractionOnStart: gradientFractionOnStart,
|
|
gradientFractionOnEnd: gradientFractionOnEnd,
|
|
shouldDisposeScrollController: shouldDisposeScrollController,
|
|
);
|
|
}
|
|
|
|
/// Constructor for creating [SCFadingEdgeScrollView] with [PageView] as child
|
|
/// child must have [PageView.controller] set
|
|
factory SCFadingEdgeScrollView.fromPageView({
|
|
Key? key,
|
|
required PageView child,
|
|
double gradientFractionOnStart = 0.1,
|
|
double gradientFractionOnEnd = 0.1,
|
|
bool shouldDisposeScrollController = false,
|
|
}) {
|
|
assert(child.controller != null, "Child must have controller set");
|
|
|
|
return SCFadingEdgeScrollView._internal(
|
|
key: key,
|
|
child: child,
|
|
scrollController: child.controller!,
|
|
scrollDirection: child.scrollDirection,
|
|
reverse: child.reverse,
|
|
gradientFractionOnStart: gradientFractionOnStart,
|
|
gradientFractionOnEnd: gradientFractionOnEnd,
|
|
shouldDisposeScrollController: shouldDisposeScrollController,
|
|
);
|
|
}
|
|
|
|
/// Constructor for creating [SCFadingEdgeScrollView] with [AnimatedList] as child
|
|
/// child must have [AnimatedList.controller] set
|
|
factory SCFadingEdgeScrollView.fromAnimatedList({
|
|
Key? key,
|
|
required AnimatedList child,
|
|
double gradientFractionOnStart = 0.1,
|
|
double gradientFractionOnEnd = 0.1,
|
|
bool shouldDisposeScrollController = false,
|
|
}) {
|
|
assert(child.controller != null, "Child must have controller set");
|
|
|
|
return SCFadingEdgeScrollView._internal(
|
|
key: key,
|
|
child: child,
|
|
scrollController: child.controller!,
|
|
scrollDirection: child.scrollDirection,
|
|
reverse: child.reverse,
|
|
gradientFractionOnStart: gradientFractionOnStart,
|
|
gradientFractionOnEnd: gradientFractionOnEnd,
|
|
shouldDisposeScrollController: shouldDisposeScrollController,
|
|
);
|
|
}
|
|
|
|
@override
|
|
_FadingEdgeScrollViewState createState() => _FadingEdgeScrollViewState();
|
|
}
|
|
|
|
class _FadingEdgeScrollViewState extends State<SCFadingEdgeScrollView>
|
|
with WidgetsBindingObserver {
|
|
late ScrollController _controller;
|
|
bool _isScrolledToStart = false;
|
|
bool _isScrolledToEnd = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_controller = widget.scrollController;
|
|
_isScrolledToStart = _controller.initialScrollOffset == 0;
|
|
_controller.addListener(_onScroll);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (_controller == null) {
|
|
return;
|
|
}
|
|
|
|
if (_isScrolledToEnd == null &&
|
|
_controller.position.maxScrollExtent == 0) {
|
|
setState(() {
|
|
_isScrolledToEnd = true;
|
|
});
|
|
}
|
|
});
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
_controller.removeListener(_onScroll);
|
|
if (widget.shouldDisposeScrollController) {
|
|
_controller.dispose();
|
|
}
|
|
}
|
|
|
|
void _onScroll() {
|
|
final offset = _controller.offset;
|
|
final minOffset = _controller.position.minScrollExtent;
|
|
final maxOffset = _controller.position.maxScrollExtent;
|
|
|
|
final isScrolledToEnd = offset >= maxOffset;
|
|
final isScrolledToStart = offset <= minOffset;
|
|
|
|
if (isScrolledToEnd != _isScrolledToEnd ||
|
|
isScrolledToStart != _isScrolledToStart) {
|
|
setState(() {
|
|
_isScrolledToEnd = isScrolledToEnd;
|
|
_isScrolledToStart = isScrolledToStart;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeMetrics() {
|
|
super.didChangeMetrics();
|
|
setState(() {
|
|
// Add the shading or remove it when the screen resize (web/desktop) or mobile is rotated
|
|
if (_controller.hasClients) {
|
|
final offset = _controller.offset;
|
|
final maxOffset = _controller.position.maxScrollExtent;
|
|
if (maxOffset == 0 && offset == 0) {
|
|
// Not scrollable
|
|
_isScrolledToStart = true;
|
|
_isScrolledToEnd = true;
|
|
} else if (maxOffset == offset) {
|
|
// Scrollable but at end
|
|
_isScrolledToStart = false;
|
|
_isScrolledToEnd = true;
|
|
} else if (maxOffset > 0 && offset == 0) {
|
|
// Scrollable but at start
|
|
_isScrolledToStart = true;
|
|
_isScrolledToEnd = false;
|
|
} else {
|
|
// Scroll in progress/not are either end
|
|
_isScrolledToStart = false;
|
|
_isScrolledToEnd = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isScrolledToStart == null && _controller.hasClients) {
|
|
final offset = _controller.offset;
|
|
final minOffset = _controller.position.minScrollExtent;
|
|
final maxOffset = _controller.position.maxScrollExtent;
|
|
|
|
_isScrolledToEnd = offset >= maxOffset;
|
|
_isScrolledToStart = offset <= minOffset;
|
|
}
|
|
|
|
return ShaderMask(
|
|
shaderCallback: (bounds) => LinearGradient(
|
|
begin: _gradientStart,
|
|
end: _gradientEnd,
|
|
stops: [
|
|
0,
|
|
widget.gradientFractionOnStart * 0.5,
|
|
1 - widget.gradientFractionOnEnd * 0.5,
|
|
1,
|
|
],
|
|
colors: _getColors(
|
|
widget.gradientFractionOnStart > 0 && !(_isScrolledToStart ?? true),
|
|
widget.gradientFractionOnEnd > 0 && !(_isScrolledToEnd ?? false)),
|
|
).createShader(
|
|
bounds.shift(Offset(-bounds.left, -bounds.top)),
|
|
textDirection: Directionality.of(context),
|
|
),
|
|
child: widget.child,
|
|
blendMode: BlendMode.dstIn,
|
|
);
|
|
}
|
|
|
|
AlignmentGeometry get _gradientStart =>
|
|
widget.scrollDirection == Axis.vertical
|
|
? _verticalStart
|
|
: _horizontalStart;
|
|
|
|
AlignmentGeometry get _gradientEnd =>
|
|
widget.scrollDirection == Axis.vertical ? _verticalEnd : _horizontalEnd;
|
|
|
|
Alignment get _verticalStart =>
|
|
widget.reverse ? Alignment.bottomCenter : Alignment.topCenter;
|
|
|
|
Alignment get _verticalEnd =>
|
|
widget.reverse ? Alignment.topCenter : Alignment.bottomCenter;
|
|
|
|
AlignmentDirectional get _horizontalStart => widget.reverse
|
|
? AlignmentDirectional.centerEnd
|
|
: AlignmentDirectional.centerStart;
|
|
|
|
AlignmentDirectional get _horizontalEnd => widget.reverse
|
|
? AlignmentDirectional.centerStart
|
|
: AlignmentDirectional.centerEnd;
|
|
|
|
List<Color> _getColors(bool isStartEnabled, bool isEndEnabled) => [
|
|
(isStartEnabled ? Colors.transparent : Colors.white),
|
|
Colors.white,
|
|
Colors.white,
|
|
(isEndEnabled ? Colors.transparent : Colors.white)
|
|
];
|
|
}
|