SteAp
SteAp

Reputation: 11999

Make a non-rectangular area draggable

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

Answers (2)

Thierry
Thierry

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.

enter image description here

As you see, you can grab either the line itself or its extremities.


A quick overview of the solution:

  • 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 handles
  • Utils, 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.

1. Material App

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(),
        ),
      ),
    );
  }
}

2. Part Union

@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;
}

3. CustomPainterDraggable

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(),
        ),
      ),
    );
  }
}

4. LinePainter

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;
}

5. Utils

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;
  }
}

Full Source Code (Easy to copy paste)

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

Tobias Braun
Tobias Braun

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

Related Questions