Reputation: 11999
To make a widget draggable, it has to be passed to a Draggable like so:
Draggable<String>(
data: "SomeDateToBeDragged",
child: Container(
width: 300,
height: 200,
alignment: Alignment.center,
color: Colors.purple,
child: Image.network(
'https://someServer.com/dog.jpg',
fit: BoxFit.cover,
),
),
...
As far as I know, a Widget is something rectangular. At least, I found nothing else than rectangular widgets.
I'd like to build a sketching app. Thus, I'd like to make a 'two dimensional' connector [a Line between two points] draggable.
How to I make a line draggable?
In other words: I'd like to make a click initiate a drag only if the drag e.g. is on a painted area and not on transparent background of the area. If I would draw a circle, it would drag if the circle would be clicked. If clicked some pixel outside the circle, it should not start a drag.
Upvotes: 2
Views: 1120
Reputation: 8393
Flutter is incredible in making things that appear complex really easy.
Here is a solution for a draggable segment of line with two handles for its extremities.
As you see, you can grab either the line itself or its extremities.
MyApp
is the MaterialApp
with the Scaffold
CustomPainterDraggable
is the main Widget with the State Management
LinePainter
a basic Painter to paint the line and the two handlesUtils
, a Utility class to calculate the distance between the cursor and the line or handles. The distance defines which part of the drawing should be dragged around.Part
, a Union class defining the different elements of the drawing (line and handles) to better structure the code.import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
@freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
class CustomPainterDraggable extends HookWidget {
@override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
@freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
class CustomPainterDraggable extends HookWidget {
@override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
Upvotes: 4
Reputation: 315
As a Container creates a Renderbox
, the resulting draggable area will always be a rectangle.
You can create a line using a CustomPainter. However, the line created by this will by itself not be draggable. If you wrap it with a Container, the line will be draggable. But the area that is draggable is then again determined by the size of the container.
I'd suggest you use a Canvas and keep track of the state of your lines by yourself, like in this thread: How to draw custom shape in flutter and drag that shape around?
Upvotes: 1