I can get the heading of my position i.e 90 (east),82,77, 180(south) etc, I have an asset image so before I show it in the screen i want to rotate it based on the heading.
For example
rat = position.heading;
//here rat is 270 degrees
RotateImageFunction(
MarkerIcon(
image: AssetImage(
//rotate this image 270 deg
"images/images/arrow.png",
),
), )
The RotateImageFunction class
class RotateImageFunction{
final String id;
final MarkerIcon? markerIcon;
final List<GeoPoint>? geoPoints;
RotateImageFunction(
this.id,
this.markerIcon,
this.geoPoints,
);
}
The MarkerIcon class
class MarkerIcon extends StatelessWidget {
final Icon? icon;
final AssetImage? image;
MarkerIcon({
this.icon,
this.image,
Key? key,
}) : assert(icon != null || image != null),
super(key: key);
#override
Widget build(BuildContext context) {
Widget? child = SizedBox.shrink();
if (icon != null) {
child = icon;
} else if (image != null) child = Image(image: image!);
return child!;
}
}
You can achieve it by wrapping it with Transform.rotate and easily use angle to rotate it
If you know that it will only be rotated in increments of 90 degrees, you can use RotatedBox:
RotatedBox(
quarterTurns: 1,
child: AssetImage( ... ),
)
If you want to be able to rotate it by any amount, you can use Transform.rotate(). However, you will then have to think about how the shape of your widget changes when not rotated by a multiple of 90 degrees:
Transform.rotate(
angle: 0.5 * pi, // radians
child: AssetImage( ... ),
)
Related
I have a simple usecase which is some how super tricky for a beginner in flutter.
I need these values returned for the scenario explained below
There are 2 containers in a row (green and orange)
OnTapDown on green container it should return ‘Green’ (this is straight forward and done)
Without lifting the finger off the screen, I drag my finger over the Orange container and I need that to return ‘Orange’
How do I solve this?
One solution could be to wrap your layout with GestureDetector and "guess" the position of your elements to then know where the drag ends.
EDIT: Adding a real check on the target position to make it more robust thanks to #GoodSp33d comment:
class DragView extends StatefulWidget {
const DragView({Key? key}) : super(key: key);
#override
_DragViewState createState() => _DragViewState();
}
GlobalKey orangeContainerKey = GlobalKey();
GlobalKey greenContainerKey = GlobalKey();
class _DragViewState extends State<DragView> {
Rect? getGlobalPaintBounds(GlobalKey element) {
final renderObject = element.currentContext!.findRenderObject();
var translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
return renderObject?.paintBounds
.shift(Offset(translation.x, translation.y));
} else {
return null;
}
}
bool isInRect(double x, double y, Rect? rect) {
if (rect != null)
return x >= rect.left &&
x <= rect.right &&
y <= rect.bottom &&
y >= rect.top;
return false;
}
#override
Widget build(BuildContext context) {
double _cursorX = 0;
double _cursorY = 0;
return GestureDetector(
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
},
onHorizontalDragEnd: (details) {
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey)))
print("Orange");
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(greenContainerKey)))
print("Green");
},
child: Row(
children: [
Expanded(
child: Container(key: greenContainerKey, color: Colors.green),
),
Expanded(
child: Container(key: orangeContainerKey, color: Colors.orange),
),
],
),
);
}
}
Second edit moving the detection to the onDragUpdate and checks to make it happens only on rect changes:
GlobalKey? currentObject;
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey))) {
if (currentObject == null || currentObject != orangeContainerKey) {
print("Orange");
currentObject = orangeContainerKey;
}
}
if (isInRect(_cursorX, _cursorY,
getGlobalPaintBounds(greenContainerKey))) if (currentObject ==
null ||
currentObject != greenContainerKey) {
print("Green");
currentObject = greenContainerKey;
}
},
I want to rotate an image based on the heading/direction of my device, This class produces the MarkerIcon widget
import 'package:location/location.dart';
class MarkerOption {
final MarkerIcon? defaultMarker;
final MarkerIcon? advancedPickerMarker;
MarkerOption({
this.defaultMarker,
this.advancedPickerMarker,
});
MarkerOption copyWith({
MarkerIcon? defaultMarker,
MarkerIcon? advancedPickerMarker,
}) {
return MarkerOption(
defaultMarker: defaultMarker ?? this.defaultMarker,
advancedPickerMarker:
advancedPickerMarker ?? this.advancedPickerMarker);
}
}
class MarkerIcon extends StatelessWidget {
final Icon? icon;
final AssetImage? image;
MarkerIcon({
this.icon,
this.image,
Key? key,
}) : assert(icon != null || image != null),
super(key: key);
#override
Widget build(BuildContext context) {
Widget? child = SizedBox.shrink();
if (icon != null) {
child = icon;
} else if (image != null)
child = Image(
image: image!,
);
return child!;
}
}
I am using the Location Plugin that allows you to add the rotation attributes to the widget like this
MarkerIcon( rotation: currentLocation.heading )This way the image will point to which ever direction my device is pointing.But I don't know how to use it in this class. How can I add these attributes to the class such that I can call it like this?
markerIcon: MarkerIcon(
image: AssetImage(
"images/images/arrow.png",
rotation: position.heading,
),
)
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;
}
}
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.
I am having trouble replicating a normal settings menu in Flutter. I am using an InkWell to try to create the splash effect that normally occurs when you tap on a settings option. The problem is that the splash effect appears way too fast compared to how it normally is. Basically, I just want to slow down the InkWell.
If you would like a slower ripple effect, then you have to change splashFactory property in your MaterialApp theme from InkSplash.splashFactory (default) to InkRipple.splashFactory. InkRipple's splash looks more like native.
It's possible to create what you wanted but it requires a custom splashFactory under InkWell class.
As you see in the variables below, these are meant to be private values and they are not open to modification within classes.
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1);
const Duration _kSplashFadeDuration = const Duration(milliseconds: 200);
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashConfirmedVelocity = 1.0;
To answer your question, yes you can do it. I just copied and pasted everything from the source code and change the animation values. After the code below just use it in splashFactory.
///Part to use within application
new InkWell(
onTap: () {},
splashFactory: CustomSplashFactory(),
child: Container(
padding: EdgeInsets.all(12.0),
child: Text('Flat Button'),
),
//Part to copy from the source code.
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 10);
const Duration _kSplashFadeDuration = const Duration(seconds: 2);
const double _kSplashInitialSize = 0.0; // logical pixels
const double _kSplashConfirmedVelocity = 0.1;
class CustomSplashFactory extends InteractiveInkFeatureFactory {
const CustomSplashFactory();
#override
InteractiveInkFeature create({
#required MaterialInkController controller,
#required RenderBox referenceBox,
#required Offset position,
#required Color color,
bool containedInkWell = false,
RectCallback rectCallback,
BorderRadius borderRadius,
double radius,
VoidCallback onRemoved,
}) {
return new CustomSplash(
controller: controller,
referenceBox: referenceBox,
position: position,
color: color,
containedInkWell: containedInkWell,
rectCallback: rectCallback,
borderRadius: borderRadius,
radius: radius,
onRemoved: onRemoved,
);
}
}
class CustomSplash extends InteractiveInkFeature {
/// Used to specify this type of ink splash for an [InkWell], [InkResponse]
/// or material [Theme].
static const InteractiveInkFeatureFactory splashFactory = const CustomSplashFactory();
/// Begin a splash, centered at position relative to [referenceBox].
///
/// The [controller] argument is typically obtained via
/// `Material.of(context)`.
///
/// If `containedInkWell` is true, then the splash will be sized to fit
/// the well rectangle, then clipped to it when drawn. The well
/// rectangle is the box returned by `rectCallback`, if provided, or
/// otherwise is the bounds of the [referenceBox].
///
/// If `containedInkWell` is false, then `rectCallback` should be null.
/// The ink splash is clipped only to the edges of the [Material].
/// This is the default.
///
/// When the splash is removed, `onRemoved` will be called.
CustomSplash({
#required MaterialInkController controller,
#required RenderBox referenceBox,
Offset position,
Color color,
bool containedInkWell = false,
RectCallback rectCallback,
BorderRadius borderRadius,
double radius,
VoidCallback onRemoved,
}) : _position = position,
_borderRadius = borderRadius ?? BorderRadius.zero,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
_clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
_repositionToReferenceBox = !containedInkWell,
super(controller: controller, referenceBox: referenceBox, color: color, onRemoved: onRemoved) {
assert(_borderRadius != null);
_radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..forward();
_radius = new Tween<double>(
begin: _kSplashInitialSize,
end: _targetRadius
).animate(_radiusController);
_alphaController = new AnimationController(duration: _kSplashFadeDuration, vsync: controller.vsync)
..addListener(controller.markNeedsPaint)
..addStatusListener(_handleAlphaStatusChanged);
_alpha = new IntTween(
begin: color.alpha,
end: 0
).animate(_alphaController);
controller.addInkFeature(this);
}
final Offset _position;
final BorderRadius _borderRadius;
final double _targetRadius;
final RectCallback _clipCallback;
final bool _repositionToReferenceBox;
Animation<double> _radius;
AnimationController _radiusController;
Animation<int> _alpha;
AnimationController _alphaController;
#override
void confirm() {
final int duration = (_targetRadius / _kSplashConfirmedVelocity).floor();
_radiusController
..duration = new Duration(milliseconds: duration)
..forward();
_alphaController.forward();
}
#override
void cancel() {
_alphaController?.forward();
}
void _handleAlphaStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed)
dispose();
}
#override
void dispose() {
_radiusController.dispose();
_alphaController.dispose();
_alphaController = null;
super.dispose();
}
RRect _clipRRectFromRect(Rect rect) {
return new RRect.fromRectAndCorners(
rect,
topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight,
bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight,
);
}
void _clipCanvasWithRect(Canvas canvas, Rect rect, {Offset offset}) {
Rect clipRect = rect;
if (offset != null) {
clipRect = clipRect.shift(offset);
}
if (_borderRadius != BorderRadius.zero) {
canvas.clipRRect(_clipRRectFromRect(clipRect));
} else {
canvas.clipRect(clipRect);
}
}
#override
void paintFeature(Canvas canvas, Matrix4 transform) {
final Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
Offset center = _position;
if (_repositionToReferenceBox)
center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value);
final Offset originOffset = MatrixUtils.getAsTranslation(transform);
if (originOffset == null) {
canvas.save();
canvas.transform(transform.storage);
if (_clipCallback != null) {
_clipCanvasWithRect(canvas, _clipCallback());
}
canvas.drawCircle(center, _radius.value, paint);
canvas.restore();
} else {
if (_clipCallback != null) {
canvas.save();
_clipCanvasWithRect(canvas, _clipCallback(), offset: originOffset);
}
canvas.drawCircle(center + originOffset, _radius.value, paint);
if (_clipCallback != null)
canvas.restore();
}
}
}
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
if (containedInkWell) {
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
return _getSplashRadiusForPositionInSize(size, position);
}
return Material.defaultSplashRadius;
}
double _getSplashRadiusForPositionInSize(Size bounds, Offset position) {
final double d1 = (position - bounds.topLeft(Offset.zero)).distance;
final double d2 = (position - bounds.topRight(Offset.zero)).distance;
final double d3 = (position - bounds.bottomLeft(Offset.zero)).distance;
final double d4 = (position - bounds.bottomRight(Offset.zero)).distance;
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
}
RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) {
if (rectCallback != null) {
assert(containedInkWell);
return rectCallback;
}
if (containedInkWell)
return () => Offset.zero & referenceBox.size;
return null;
}