Flutter how to create parallax effect for background image - flutter

I am trying to create a parallax effect for the background image of a container.
I followed this tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling.
However I would like for the background image to remain still in the viewport ad the user scrolls.
Basically I would like to achieve this behavior https://www.w3schools.com/howto/tryhow_css_parallax_demo.htm.
I believe that in order to achieve this I have to modify the paintChildren method inside ParallaxFlowDelegate
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.centerLeft(Offset.zero),
ancestor: scrollableBox);
// Determine the percent position of this list item within the
// scrollable area.
final viewportDimension = scrollable.position.viewportDimension;
final scrollFraction =
(listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);
// Convert the background alignment into a pixel offset for
// painting purposes.
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
// Calculate the vertical alignment of the background
// based on the scroll percent.
final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);
final listItemSize = context.size;
final childRect =
verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0.0, childRect.top)).transform,
);
}
but I can't figure out how.
Any help would be greatly appreciated :)

I was just messing, of course I can figure it out.
For anyone interested simply follow the flutter dev tutorial https://docs.flutter.dev/cookbook/effects/parallax-scrolling but replace their ParallaxFlowDelegate class with the following:
import 'package:flutter/material.dart';
class ParallaxFlowDelegate extends FlowDelegate {
final ScrollableState scrollable;
final BuildContext listItemContext;
final GlobalKey backgroundImageKey;
ParallaxFlowDelegate({
required this.scrollable,
required this.listItemContext,
required this.backgroundImageKey,
}) : super(repaint: scrollable.position);
#override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return BoxConstraints.expand(
width: constraints.maxWidth,
height: scrollable.position.viewportDimension
);
}
#override
void paintChildren(FlowPaintingContext context) {
// Calculate the position of this list item within the viewport.
final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
final listItemBox = listItemContext.findRenderObject() as RenderBox;
final listItemSize = context.size;
//Gets the offset of the top of the list item from the top of the viewport
final listItemOffset = listItemBox.localToGlobal(
listItemBox.size.topCenter(Offset.zero),
ancestor: scrollableBox);
// Get the size of the background image
final backgroundSize =
(backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
.size;
//Gets the vertical size of the viewport (excludes appbars)
final viewportDimension = scrollable.position.viewportDimension;
// Determine the percent position of this list item within the
// scrollable area.
// - scrollFraction is 1 if listItem's top edge is at the bottom of the
// viewport.
// - scrollFraction is 0 if listItem's top edge is at the top of the
// viewport
// - scrollFraction is -1 if listItem's bottom edge is at the top of the
// viewport
final double scrollFraction, yOffset;
if(listItemOffset.dy > 0){
scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0, 1.0);
yOffset = -scrollFraction * backgroundSize.height;
}else{
scrollFraction = (listItemOffset.dy / listItemSize.height).clamp(-1, 0);
yOffset = -scrollFraction * listItemSize.height;
}
// Paint the background.
context.paintChild(
0,
transform:
Transform.translate(offset: Offset(0, yOffset)).transform,
);
}
#override
bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
return scrollable != oldDelegate.scrollable ||
listItemContext != oldDelegate.listItemContext ||
backgroundImageKey != oldDelegate.backgroundImageKey;
}
}
Works like a beauty for vertically scrolling pages :)

Related

Why does my Flutter CustomPainter class not paint anything on the screen when using the canvas..drawArc() function with a sweepAngle less than 2*pi?

For some reason, my CustomPainter does not draw anything to the screen. I'm trying to build a Pie-Chart but the painter only works when I set the sweepAngle to 2*pi.
The Widget where CustomPaint is called has the following structure:
class PieChart extends StatelessWidget {
const PieChart({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SizedBox(
height: 160,
width: 210,
child: CustomPaint(
painter: PieChartPainter(categories: dataset, width: 10),
),
);
}
}
This is my CustomPainter class:
class PieChartPainter extends CustomPainter {
PieChartPainter({
required this.categories,
required this.width,
});
final List<Category> categories;
final double width;
#override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2, size.height / 2);
double radius = min(size.width / 2, size.height / 2);
double total = 0;
// Calculate total amount from each category
for (var expense in categories) {
total += expense.amount;
}
// The angle/radian at 12 o'clock
double startRadian = -pi / 2;
for (var index = 0; index < categories.length; index++) {
final currentCategory = categories.elementAt(index);
final sweepRadian = currentCategory.amount / total * 2 * pi;
final paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = width
..color = categoryColors.elementAt(index % categories.length);
final rect = Rect.fromCenter(
center: center, width: radius * 2, height: radius * 2);
canvas.drawArc(
rect,
startRadian,
2 * pi, // should really be "sweepRadian"
false,
paint,
);
startRadian += sweepRadian;
}
}
#override
bool shouldRepaint(PieChartPainter oldDelegate) {
return true;
}
}
I can almost certainly say that the problem has nothing to do with the data and colors that I provide, because whenever I log the current elements to the console, it gives the correct output.
Here, you can see my Widget structure:
And here is an image of the component itself: (Notice that only the last category-color is shown for the entire circle, because whenever I use a sweepAngle that is less than 2*pi, the entire widget doesn't show colors.)
This is the component when I set a sweepAngle that is less than 2*pi:
I really cannot figure out what might be causing this issue. Does anyone have an Idea what else is influenced by the sweepAngle parameter? I also have no idea why the colored circles to the left of the individual categories are not visible, because they're rendered in an entirely different Widget-branch...
If you have any idea on how to solve this, I would be more than happy to provide more information but as long as I don't know where to look, I don't want to spam this issue with unnecessary information.
For anyone wondering:
The problem had to do with the "--enable-software-rendering" argument. Once I ran flutter with flutter run --enable-software-rendering, it all worked as expected.

Flutter: How to handle Renderflex overflow in SliverPersistentHeader

I have a SliverPersistentHeader which contains a video. The desired behavior of this view is that as a user scrolls upward, the view should cover or minimize the size of the video. The video header is a widget containing a Chewie video player. The desired behavior works up to a certain point at which I get a pixel overflow as shown in this animation:
When the scroll reaches a certain point, the video can no longer resize and it results in a render overflow. The desired behavior would be for the video to continue to resize until it's gone, or to catch the error and hide or remove the video from the view. The code rendering this scroll view is:
Widget buildScollView(GenericScreenModel model) {
return CustomScrollView(
slivers: [
StandardHeader(),
SliverFillRemaining(
child: Container(
// color: Colors.transparent,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
),
borderRadius: BorderRadius.only(topRight: radius, topLeft: radius)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(model.model?.getContentText ?? 'Empty'),
)),
)
],
);
}
The StandardHeader class is a simple widget containing a Chewie video.
class _StandardHeaderState extends State<StandardHeader> {
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
delegate: Delegate(
Colors.blue,
'Header Title',
),
pinned: true,
);
}
}
Is there a way to catch this error and hide the video player? Can anyone help with this or point me to a resource? Thanks!
The issue seems to be with the Chewie and/or video player widget. If the header's height is less than the required height of the player, the overflow occurs.
You can achieve the desired effect by using a SingleChildRenderObjectWidget. I added an opacity factor that you can easily remove that gives it (in my opinion) an extra touch.
I named this widget: ClipBelowHeight
Output:
Source:
ClipBelowHeight is SingleChildRenderObjectWidget that adds the desired effect by using a clipHeight parameter to clamp the height of the child to one that does not overflow. It centers its child vertically (Chewie player in this case).
To understand more, read the comments inside the performLayout and paint method.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ClipBelowHeight extends SingleChildRenderObjectWidget {
const ClipBelowHeight({
super.key,
super.child,
required this.clipHeight,
required this.opacityFactor,
});
/// The minimum height the [child] must have, as well as the height at which
/// clipping begins.
final double clipHeight;
/// The opacity factor to apply when the height decreases.
final double opacityFactor;
#override
RenderObject createRenderObject(BuildContext context) {
return RenderClipBelowHeight(clipHeight: clipHeight, factor: opacityFactor);
}
#override
void updateRenderObject(
BuildContext context,
RenderClipBelowHeight renderObject,
) {
renderObject
..clipHeight = clipHeight
..factor = opacityFactor;
}
}
class RenderClipBelowHeight extends RenderBox with RenderObjectWithChildMixin {
RenderClipBelowHeight({required double clipHeight, required double factor})
: _clipHeight = clipHeight,
_factor = factor;
double _clipHeight;
double get clipHeight => _clipHeight;
set clipHeight(double value) {
assert(value >= .0);
if (_clipHeight == value) return;
_clipHeight = value;
markNeedsLayout();
}
double _factor;
double get factor => _factor;
set factor(double value) {
assert(value >= .0);
if (_factor == value) return;
_factor = value;
markNeedsLayout();
}
#override
bool get sizedByParent => false;
#override
void performLayout() {
/// The child contraints depend on whether [constraints.maxHeight] is less
/// than [clipHeight]. This RenderObject's responsibility is to ensure that
/// the child's height is never below [clipHeight], because when the
/// child's height is below [clipHeight], then there will be visual
/// overflow.
final childConstraints = constraints.maxHeight < _clipHeight
? BoxConstraints.tight(Size(constraints.maxWidth, _clipHeight))
: constraints;
(child as RenderBox).layout(childConstraints, parentUsesSize: true);
size = Size(constraints.maxWidth, constraints.maxHeight);
}
#override
void paint(PaintingContext context, Offset offset) {
final theChild = child as RenderBox;
/// Clip the painted area to [size], which allows the [child] height to
/// be greater than [size] without overflowing.
context.pushClipRect(
true,
offset,
Offset.zero & size,
(PaintingContext context, Offset offset) {
/// (optional) Set the opacity by applying the specified factor.
context.pushOpacity(
offset,
/// The opacity begins to take effect at approximately half [size].
((255.0 + 128.0) * _factor).toInt(),
(context, offset) {
/// Ensure the child remains centered vertically based on [size].
final centeredOffset =
Offset(.0, (size.height - theChild.size.height) / 2.0);
context.paintChild(theChild, centeredOffset + offset);
},
);
},
);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final theChild = child as RenderBox;
var childParentData = theChild.parentData as BoxParentData;
final isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return theChild.hitTest(result, position: transformed);
},
);
return isHit;
}
#override
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
#override
double computeMinIntrinsicWidth(double height) =>
(child as RenderBox).getMinIntrinsicWidth(height);
#override
double computeMaxIntrinsicWidth(double height) =>
(child as RenderBox).getMaxIntrinsicWidth(height);
#override
double computeMinIntrinsicHeight(double width) =>
(child as RenderBox).getMinIntrinsicHeight(width);
#override
double computeMaxIntrinsicHeight(double width) =>
(child as RenderBox).getMaxIntrinsicHeight(width);
}
The widget that uses the ClipBelowHeight widget is your header delegate. This widget should be self-explanatory and I think that you will be able to understand it.
class Delegate extends SliverPersistentHeaderDelegate {
Delegate(this.color, this.player);
final Color color;
final Chewie player;
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: color,
child: ClipBelowHeight(
clipHeight: 80.0,
opacityFactor: 1.0 - shrinkOffset / maxExtent,
child: player,
),
);
}
#override
double get maxExtent => 150.0;
#override
double get minExtent => .0;
#override
bool shouldRebuild(Delegate oldDelegate) {
return color != oldDelegate.color || player != oldDelegate.player;
}
}

How to draw a rounded border pie chart

I am trying to build a pie chart that will look like this:
I've tried both Flutter_Charts and FL_Chart, but it seems none of them support a rounded corner and spaced items in the pie chart.
Does anyone know what is the best way to achieve this design as a pie chart?
Thank you!
A very similar version to your chart can easily be achieved with the CustomPaint widget.
Here is the resulting chart
To achieve this you will just need a very rudimentary CustomPainter that draws arcs across its canvas.
The rounding effect is achieved through the strokeCap attribute of the Paint that is used to draw the stroke. Sadly StrokeCap only supports
round and square stroke endings.
A rounded rectangle effect like the one in your screenshot cannot be achieved through this.
Colors are achieved by using a separate Paint for each stroke.
// this is used to pass data about chart values to the widget
class PieChartData {
const PieChartData(this.color, this.percent);
final Color color;
final double percent;
}
// our pie chart widget
class PieChart extends StatelessWidget {
PieChart({
required this.data,
required this.radius,
this.strokeWidth = 8,
this.child,
Key? key,
}) : // make sure sum of data is never ovr 100 percent
assert(data.fold<double>(0, (sum, data) => sum + data.percent) <= 100),
super(key: key);
final List<PieChartData> data;
// radius of chart
final double radius;
// width of stroke
final double strokeWidth;
// optional child; can be used for text for example
final Widget? child;
#override
Widget build(context) {
return CustomPaint(
painter: _Painter(strokeWidth, data),
size: Size.square(radius),
child: SizedBox.square(
// calc diameter
dimension: radius * 2,
child: Center(
child: child,
),
),
);
}
}
// responsible for painting our chart
class _PainterData {
const _PainterData(this.paint, this.radians);
final Paint paint;
final double radians;
}
class _Painter extends CustomPainter {
_Painter(double strokeWidth, List<PieChartData> data) {
// convert chart data to painter data
dataList = data
.map((e) => _PainterData(
Paint()
..color = e.color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
// remove padding from stroke
(e.percent - _padding) * _percentInRadians,
))
.toList();
}
static const _percentInRadians = 0.062831853071796;
// this is the gap between strokes in percent
static const _padding = 4;
static const _paddingInRadians = _percentInRadians * _padding;
// 0 radians is to the right, but since we want to start from the top
// we'll use -90 degrees in radians
static const _startAngle = -1.570796 + _paddingInRadians / 2;
late final List<_PainterData> dataList;
#override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
// keep track of start angle for next stroke
double startAngle = _startAngle;
for (final data in dataList) {
final path = Path()..addArc(rect, startAngle, data.radians);
startAngle += data.radians + _paddingInRadians;
canvas.drawPath(path, data.paint);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
You can check out the dartpad to experiment with a working example.
I am positive that the same chart you provided in that picture can be
achieved with a CustomPainter but that will be a lot more complex.

How to detect drag velocity in flutter?

How to detect drag velocity in flutter ?
I want to draw on screen in flutter using custom paint, when the velocity is less the stroke width should be less, but when the drag velocity is high the stroke width should be greater.
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(
() {
RenderBox object = context.findRenderObject();
Offset _localPosition =
object.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(_localPosition);
},
);
},
onPanEnd: (DragEndDetails details) => {
_deletedPoints.clear(),
_points.add(null),
// _listPoints.add(_points),
// _listPoints = List.from(_listPoints)..add(_points),
},
child: CustomPaint(
painter: Draw(points: _points),
size: Size.infinite,
),
),
The custom draw widget that extends customPainter
class Draw extends CustomPainter {
List<Offset> points;
// List<List<Offset>> points;
Draw({this.points});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = brushColor
..strokeCap = StrokeCap.round
..strokeWidth = brushWidth;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
#override
bool shouldRepaint(Draw oldDelegate) => oldDelegate.points != points;
}
velocity is how much the position changed in a given time.
for this purpose you have the details.delta.distance in the onPanUpdate callback, which returns a double indicating how much the pointer has moved since last update, the bigger it is, the larger the velocity.
in your case, you can change the stroke width based on the distance traveled.
I haven't used any of these myself but you could look into
DragUpdateDetails. details in onPanUpdate has an Offset object called delta. Which are coordinates which update every time onPanUpdate is called.
https://api.flutter.dev/flutter/gestures/DragUpdateDetails-class.html
There's also a class called VelocityTracker
https://api.flutter.dev/flutter/gestures/VelocityTracker-class.html
Hope this helps you a bit
[Update] a practical example
late final Ticker _velocityTicker;
final List<int> _velocityRecs = List.filled(6, 0, growable: true);
double _rec = 0;
get _deltaSum {
var sum = 0;
for (var i = 1; i < _velocityRecs.length; i++) {
sum += (_velocityRecs[i] - _velocityRecs[i - 1]).abs();
}
return sum;
}
/// in initState()
_velocityTicker = Ticker((duration) {
_velocityRecs.removeAt(0);
// add whatever values you want to track
_velocityRecs.add(_currentIndex);
// You get a 'velocity' here, do something you want
if (_deltaSum > 2) {
// _offsetYController.forward();
} else if (_deltaSum < 1) {
// _offsetYController.reverse();
}
});
/// then .start()/.stop() the ticker in your event callback
/// Since each ticker has a similar time interval,
/// for Simplicity, I did not use duration here,
/// if you need a more accurate calculation value,
/// you may need to use it
In short, You can't.
Although details.delta and VelocityTracker are mentioned above, neither of them technically has access to the user's PointerEvent velocity,
For example, if the user drags the slider and stops, but the finger does not leave the screen, the velocity should be about 0, but this state can't be captured in flutter.
velocityTracker doesn't help
If you want to implement it yourself, one idea is to create a Timer/Ticker, record the last value and put it into a list, then set it to zero, and each time you want get, the sum of the whole list will be averaged to get the velocity, you need to be careful to determine how much the length of your List is appropriate

Flutter - Custom slider value indicator. How to get the slider's center position

I am trying to implement a custom progress indicator for flutter slider. As for now, I have the given code that draws the indicator.
class InsideSliderValueIndicator extends SliderComponentShape {
#override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.zero;
}
#override
void paint(
PaintingContext context,
Offset center, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final sliderWidth = parentBox.size.width;
final sliderBoxOffset = parentBox.localToGlobal(Offset.zero);
labelPainter.paint(
context.canvas,
Offset(sliderBoxOffset.dx + sliderWidth / 2 - labelPainter.width / 2,
center.dy - labelPainter.height / 2));
}
}
I have also customized the slider to remove the thumb. Here's what it looks like:
As you can see, the text is not centered in the slider.
The problem with this code is that sliderBoxOffset doesn't return the box of the "slider", but of the parent card, and center is actually the position of your thumb on the slider relative to the parent.
So my question is: How can I get the center of the slider from the given parameters ?
Thank you for your help
I found a weird solution that works for my use-case
final sliderWidth = parentBox.size.width;
final halfSliderWidth = sliderWidth / 2;
final translateX = value < 0.5 ? halfSliderWidth - (value * sliderWidth) : halfSliderWidth - (value * sliderWidth);
labelPainter.paint(context.canvas, center.translate(translateX - labelPainter.width / 2, -labelPainter.height / 2));
I'm however pretty sure this won't work if the slider is not centered in its parent. I'm still wondering if there is a less dirty way to do this.