I am trying to use bottomNavigationBar with notched effect in flutter, and of course it is fine. But when I try to add padding to BottomAppBar on the left and right to make the navigator floating-like, the notch position shifts to the right!!
This means the floating action button is in its correct spot but the notch is not exactly under it but shifted to the right. It shifts to the right exactly as the amount of padding I give to the BottomAppBar! Why!
The shape feature I used for BottomAppBar is this:
AutomaticNotchedShape(RoundedRectangleBorder(borderRadius:
BorderRadius.all(Radius.circular(35))), StadiumBorder()),
Just to be clear, I want something like this but with the notched effect on the middle (FloatingActionButtonLocation.centerDocked).
Like this but with notch in the center
Edit
Sample code to demonstrate this issue:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(
20.0,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(30.0),
child: BottomAppBar(
notchMargin: 5.0,
shape: const CircularNotchedRectangle(),
child: Container(height: 50.0),
color: Colors.blue,
clipBehavior: Clip.hardEdge,
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
Render for sample code:
I solved this problem by creating own CustomCircularNotchedRectangle. I'm added to original custom Offset - notchOffset...
Using:
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0).copyWith(bottom: 13),
child: BottomAppBar(
shape: CustomCircularNotchedRectangle(
notchOffset: Offset(-10, 0),
),
notchMargin: 6.0,
color: widget.backgroundColor,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
),
);
CustomCircularNotchedRectangle:
class CustomCircularNotchedRectangle extends NotchedShape {
CustomCircularNotchedRectangle({
this.notchOffset = const Offset(0, 0),
});
final Offset notchOffset;
#override
Path getOuterPath(Rect host, Rect? guest) {
if (guest == null || !host.overlaps(guest)) return Path()..addRect(host);
// The guest's shape is a circle bounded by the guest rectangle.
// So the guest's radius is half the guest width.
final double notchRadius = guest.width / 2.0;
// We build a path for the notch from 3 segments:
// Segment A - a Bezier curve from the host's top edge to segment B.
// Segment B - an arc with radius notchRadius.
// Segment C - a Bezier curve from segment B back to the host's top edge.
//
// A detailed explanation and the derivation of the formulas below is
// available at: goo.gl/Ufzrqn
const double s1 = 30.0;
const double s2 = 1.0;
final double r = notchRadius;
final double a = -1.0 * r - s2;
final double b = host.top - guest.center.dy;
final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r));
final double p2xA = ((a * r * r) - n2) / (a * a + b * b);
final double p2xB = ((a * r * r) + n2) / (a * a + b * b);
final double p2yA = math.sqrt(r * r - p2xA * p2xA);
final double p2yB = math.sqrt(r * r - p2xB * p2xB);
final List<Offset?> p = List<Offset?>.filled(6, null);
// p0, p1, and p2 are the control points for segment A.
p[0] = Offset(a - s1, b);
p[1] = Offset(a, b);
final double cmp = b < 0 ? -1.0 : 1.0;
p[2] = cmp * p2yA > cmp * p2yB ? Offset(p2xA, p2yA) : Offset(p2xB, p2yB);
// p3, p4, and p5 are the control points for segment B, which is a mirror
// of segment A around the y axis.
p[3] = Offset(-1.0 * p[2]!.dx, p[2]!.dy);
p[4] = Offset(-1.0 * p[1]!.dx, p[1]!.dy);
p[5] = Offset(-1.0 * p[0]!.dx, p[0]!.dy);
// translate all points back to the absolute coordinate system.
for (int i = 0; i < p.length; i += 1) {
p[i] = p[i]! + guest.center + notchOffset;
}
return Path()
..moveTo(host.left, host.top)
..lineTo(p[0]!.dx, p[0]!.dy)
..quadraticBezierTo(p[1]!.dx, p[1]!.dy, p[2]!.dx, p[2]!.dy)
..arcToPoint(
p[3]!,
radius: Radius.circular(notchRadius),
clockwise: false,
)
..quadraticBezierTo(p[4]!.dx, p[4]!.dy, p[5]!.dx, p[5]!.dy)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom)
..close();
}
}
Before:
After:
Related
Text overlapping the another enter image description heretext in Layout Customized Chart.js
enter image description here
Please , somebody can helpe me?
const { x, y } = datapoint.tooltipPosition();
const halfWidth = width / 2;
const halfHeight = width / 2;
const xLine = x >= halfWidth ? x + 100 : x - 25;
const yLine = y >= halfHeight ? y + 100 : y - 25;
const extraLine = x >= halfWidth ? 40 : -40;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(xLine, yLine);
ctx.lineTo(xLine + extraLine, yLine);
ctx.strokeStyle = dataset.backgroundColor[index];
ctx.stroke();
ctx.font = 'bold 12px Arial';
const textXPosition = x >= halfWidth ? 'left' : 'right';
const plusFivePx = x >= halfWidth ? 5 : -5;
ctx.textAlign = textXPosition;
ctx.fillStyle = 'black';
ctx.fillText(
chart.data.labels[index],
xLine + extraLine + plusFivePx,
yLine
);
I am trying to implement custom curved shape like below left image.
Below is the code what I have achieved so far by using quadraticBezierTo:
class CustomNotchedShape extends NotchedShape {
final BuildContext context;
const CustomNotchedShape(this.context);
#override
Path getOuterPath(Rect host, Rect? guest) {
const radius = 110.0;
const lx = 40.0;
const ly = 20;
const bx = 10.0;
const by = 50.0;
var x = (MediaQuery.of(context).size.width - radius) / 2 - lx;
return Path()
..moveTo(host.left, host.top)
..lineTo(x, host.top)
..quadraticBezierTo(x + bx, host.top, x += lx, host.top - ly)
..quadraticBezierTo(
x + radius / 2, host.top - by, x += radius, host.top - ly)
..quadraticBezierTo((x += lx) - bx, host.top, x, host.top)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom);
}
}
I faced the problem in my Flutter app that I can't draw this kind of a shape for my Slider
What I have now is:
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
final rect = Rect.fromCircle(center: center, radius: thumbRadius);
final rrect = RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(rect.left - 5, rect.top),
Offset(rect.right + 5, rect.bottom),
),
Radius.circular(thumbRadius + 2),
);
canvas.drawRRect(rrect, paint);
Also, it would be great to change height of all bar, because next code changes only the size after player
SliderTheme(
data: SliderThemeData(
trackHeight: 2,
thumbShape: CustomSliderPlayer(),
),
child: Slider(...)
From the comments it looks like you are not familiar with quadratic bezier curves, they are very simple, I would recommend you to start on a Javascript canvas, they are easier to test that way and logic is the same, we move to the starting point then we draw the curve, see sample snippet below
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function dobleQuad(x, y, w, h) {
ctx.beginPath();
ctx.moveTo(x - w, y);
ctx.quadraticCurveTo(x, y - h, x + w, y);
ctx.moveTo(x - w, y);
ctx.quadraticCurveTo(x, y + h, x + w, y);
ctx.fill();
}
function drawSlider(x, y) {
ctx.moveTo(0, y - 2);
ctx.fillRect(0, y - 2, canvas.width, 4);
dobleQuad(x, y, 20, 22)
}
drawSlider(50, 50)
canvas.addEventListener('mousemove', function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var rect = canvas.getBoundingClientRect();
drawSlider( evt.clientX - rect.left, 50 )
})
<canvas id="canvas"></canvas>
Just keep in mind that in JS it's quadraticCurveTo but in flutter quadraticBezierTo
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/quadraticCurveTo
void ctx.quadraticCurveTo(cpx, cpy, x, y);
cpx, cpy
The coordinates of the control point.
x, y
The coordinates of the end point.
https://api.flutter.dev/flutter/dart-ui/Path/quadraticBezierTo.html
void quadraticBezierTo(
double x1, double y1,
double x2, double y2
)
Adds a quadratic bezier segment that curves from the current point
to the given point (x2,y2), using the control point (x1,y1).
How to make chat bubble shape use path ?
expect :
class ChatBubbleShapeBorder extends ShapeBorder {
final double arrowWidth;
final double arrowHeight;
final double arrowArc;
final double radius;
ChatBubbleShapeBorder({
this.radius = 12.0,
this.arrowWidth = 16.0,
this.arrowHeight = 8.0,
this.arrowArc = 0.5,
}) : assert(arrowArc <= 1.0 && arrowArc >= 0.0);
#override
EdgeInsetsGeometry get dimensions => EdgeInsets.only(bottom: arrowHeight);
#override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) =>
null ?? Path();
#override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
rect = Rect.fromPoints(
rect.topLeft, rect.bottomRight - Offset(0, arrowHeight));
double x = arrowWidth, y = arrowHeight, r = 1 - arrowArc;
return Path()
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(radius)))
..moveTo(rect.topRight.dx - 30, rect.topRight.dy)
..relativeLineTo(-x / 2 * r, -y * r)
..relativeQuadraticBezierTo(
-x / 2 * (1 - r), -y * (1 - r), -x * (1 - r), 0)
..relativeLineTo(-x / 2 * r, y * r);
}
#override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
#override
ShapeBorder scale(double t) => this;
}
and after tries here is my shot
actual:
i try move arrow to left and right for chat bubble.
could you help me to move arrow?
update :
i try to change rect point and move position of arrow. look good, But, i get problem with arrow
rect =
Rect.fromPoints(rect.topLeft + Offset(arrowWidth, 0), rect.bottomRight - Offset(arrowWidth, 0));
double x = arrowWidth, y = arrowHeight, r = 1 - arrowArc;
return Path()
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(radius)))
..moveTo(rect.topRight.dx + arrowWidth, rect.topRight.dy + 30)
..relativeLineTo(-x / 2 * r, -y * r)
..relativeQuadraticBezierTo(
-x / 2 * (1 - r), -y * (1 - r), -x * (1 - r), 0)
..relativeLineTo(-x / 2 * r, y * r);
shot :
I'm currently using the matrix_gesture_detector package to scale, transform and rotate a Transform widget.
Everything works fine but to improve UX I would like to snap the widget at 90, 180, 270 or 360 degrees once the user rotates the widget close enough to said angles.
Edit: To clarify I would like the user to be able to freely rotate the widget, but snap into the nearest 90 degree rotation within whichever quadrant it is, once it gets close enough.
Hence, the solution should detect that "closeness" and then act accordingly. Please visit this link to see a GIF which shows the desired effect
How can I achieve this?
Below is the code snippet
Widget transformContainer() {
Matrix4 matrix;
GlobalKey matrixDetectorKey = GlobalKey();
return MatrixGestureDetector(
key: matrixDetectorKey,
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, rm);
});
},
child: Transform(
transform: matrix,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100.0,
height: 200.0,
color: Colors.teal,
),
),
);}
For this to work, you have to check if the rotation of the matrix is closing in on a snapping point and rotate the Z-Axis to that point. I was able to write this code which works for this scenario. You may have to tune the threshold to adjust the "snappiness" to your taste.
import vector_math and use the following code:
import 'package:vector_math/vector_math_64.dart' as vec;
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
Matrix4 ogRm = rm.clone();
double radian = MatrixGestureDetector.decomposeToValues(m).rotation;
double degrees = vec.degrees(radian);
double delta_0 = vec.absoluteError(degrees, 0);
double delta_90 = vec.absoluteError(degrees, 90);
double delta_180 = vec.absoluteError(degrees, 180);
double delta_270 = vec.absoluteError(degrees, -90);
double threshold = 4;
if (delta_0 <= threshold) {
rm.rotateZ(vec.radians(0) - radian);
} else if (delta_90 <= threshold) {
rm.rotateZ(vec.radians(90) - radian);
} else if (delta_180 <= threshold) {
rm.rotateZ(vec.radians(180) - radian);
} else if (delta_270 <= threshold) {
rm.rotateZ(vec.radians(270) - radian);
}
// update gesture matrix
if (ogRm != rm) m = m * rm;
setState(() {
//transform your widget using this matrix
matrix = m;
});
}
Note that 360 is the same as 0, so there's no need to check for it.
MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
//270 is the angle
m.rotate(m.getTranslation(), 270);
matrix4 = m;
});
},
child: Transform(
transform: matrix4,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100,
height: 200,
color: Colors.teal,
),
),
),
You will have the rotate function inside the library. this code will work for snapping to 0 degrees. Basically what I am doing is if the difference in rotation and x-axis is less than 0.2 radians, I snap to 0 degrees while keeping the rotation in a separate variable. When the user moves beyond this, I add this value again and keep rotating normally.
Matrix4 _rotate(double angle, Offset focalPoint) {
double toBeRotated = 0;
var array = matrix.applyToVector3Array([0, 0, 0, 1, 0, 0]);
Offset delta = Offset(array[3] - array[0], array[4] - array[1]);
double rotation = delta.direction;
deltaAngle = deltaAngle + angle;
if ((rotation + deltaAngle).abs() > 0.2) {
toBeRotated = deltaAngle;
deltaAngle = 0;
} else if (rotation != 0 && (rotation + deltaAngle).abs() <= 0.2) {
toBeRotated = -rotation;
deltaAngle = deltaAngle + rotation;
} else {
toBeRotated = 0;
}
var c = cos(toBeRotated);
var s = sin(toBeRotated);
var dx = (1 - c) * focalPoint.dx + s * focalPoint.dy;
var dy = (1 - c) * focalPoint.dy - s * focalPoint.dx;
// ..[0] = c # x scale
// ..[1] = s # y skew
// ..[4] = -s # x skew
// ..[5] = c # y scale
// ..[10] = 1 # diagonal "one"
// ..[12] = dx # x translation
// ..[13] = dy # y translation
// ..[15] = 1 # diagonal "one"
return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}