Schwusch / widget_arrows

Draw arrows between widgets in Flutter
https://pub.dev/packages/widget_arrows
MIT License
145 stars 14 forks source link

Draggable implementation in future??? #1

Closed DeepStyles closed 4 years ago

Schwusch commented 4 years ago

The problem with Draggable feedback is that it is put in another subtree, namely as an OverlayEntry in an Overlay. You can try wrapping your MaterialApp with the ArrowContainer but I don't know what the side effects are.

Schwusch commented 4 years ago

You need a Directionality at the root. Here is a working example:

import 'package:flutter/material.dart';
import 'package:widget_arrows/widget_arrows.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Directionality(
        textDirection: TextDirection.ltr,
        child: ArrowContainer(
          child: MaterialApp(
            home: HomePage(),
          ),
        ),
      );
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Stack(
          children: [
            Positioned(
              left: 50,
              top: 50,
              child: Draggable(
                child: Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                  child: Text('Drag me'),
                ),
                feedback: ArrowElement(
                  id: 'feedback',
                  targetId: 'target',
                  sourceAnchor: Alignment.centerRight,
                  child:
                      Container(height: 100, width: 100, color: Colors.orange),
                ),
              ),
            ),
            Positioned(
              right: 50,
              bottom: 50,
              child: DragTarget<String>(
                builder: (context, candidateData, rejectedData) => ArrowElement(
                  id: 'target',
                  child: Container(
                    height: 100,
                    width: 100,
                    color: Colors.green,
                    child: Center(
                      child: Text(
                        'Drag target',
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      );
}
DeepStyles commented 4 years ago

lol just saw flutter 1.2 release contains Interactive viewer widget(I meant DragTarget onAcceptDetails method in Draggable Obj), can we use it here???

Schwusch commented 4 years ago

I don't really understand what you want to achieve. But if you come up with something, I am curious to hear about it.

DeepStyles commented 4 years ago

I want both to be draggable(source and target), 1.2 release medium article shows that feature.(I will try it out let u know if it works). Thanks for above example and ofc the awesome package lol...

DeepStyles commented 4 years ago

is thr anyway Arrowelement can point multiple elements???

Schwusch commented 4 years ago

You can wrap an ArrowElement in an ArrowElement. That's at least what is supported at the moment.

DeepStyles commented 4 years ago

hi, i add targetIdList to the package to point arrow multiple elements. Is there any option to show arrow beneath the container when we center alignments of source and target elements(I added pic to show). Capture

Schwusch commented 4 years ago

Is there any option to show arrow beneath the container

There is not, you can however pad the beginning of the arrow.

DeepStyles commented 4 years ago

awesome it helps thanks...

DeepStyles commented 4 years ago

seemlike we cant add list of widgets to arrowcontainer(so that I can add and remove from list).. getting error arrowcontainer missing element!!! if I use statelesswidgets for list of arrowelements couldn't able to drag. Is thr any clue otherwise I will switch to react(react-archer package helps my situation)...

Schwusch commented 4 years ago

If you provide an example I can maybe help you. If you're building for web you might very well be better off using React.

DeepStyles commented 4 years ago

I modified the widget_arrows file in package: removed targetID added targetIdList

library widget_arrows;

import 'dart:math'; import 'dart:ui';

import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:widget_arrows/arrows.dart';

class ArrowContainer extends StatefulWidget { final Widget child;

const ArrowContainer({Key key, this.child}) : super(key: key);

@override _ArrowContainerState createState() => _ArrowContainerState(); }

abstract class StatePatched extends State { void disposePatched() { super.dispose(); } }

class _ArrowContainerState extends StatePatched with ChangeNotifier { final _elements = <String, _ArrowElementState>{};

@override void dispose() { super.dispose(); disposePatched(); }

@override Widget build(BuildContext context) => Stack( children: [ widget.child, IgnorePointer( child: CustomPaint( foregroundPainter: _ArrowPainter(_elements, Directionality.of(context), this), child: Container(), ), ), ], );

void addArrow(ArrowElementState arrow) { WidgetsBinding.instance.addPostFrameCallback(() { _elements[arrow.widget.id] = arrow; notifyListeners(); }); }

void removeArrow(String id) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _elements.remove(id); notifyListeners(); } }); } }

class _ArrowPainter extends CustomPainter { final Map<String, _ArrowElementState> _elements; final TextDirection _direction;

_ArrowPainter(this._elements, this._direction, Listenable repaint) : super(repaint: repaint);

@override void paint(Canvas canvas, Size size) => _elements.values.forEach((elem) { final widget = elem.widget;

    if (!widget.show) return; // don't show/paint
    if (widget.id == null) {
      print('arrow id is null, will not paint');
      return;
    }
    if (widget.targetId == null && widget.targetIdList == null) {
      return; // Unable to draw
    }

    if (_elements[widget.targetId] == null && widget.targetIdList == null) {
      print(
          'cannot find target arrow element with id "${widget.targetId}"');

      return;
    }

final start = elem.context.findRenderObject() as RenderBox;

    if (widget.targetIdList.isNotEmpty) {
      for (int i = 0; i < widget.targetIdList.length; i++) {
        // print('elements: $_elements');

        final end = _elements[widget.targetIdList[i]]
            ?.context
            ?.findRenderObject() as RenderBox;
        // print('index: $i ');
        // print('end: $end ');

        if (start == null ||
            end == null ||
            !start.attached ||
            !end.attached) {
          print(
              'one of "${widget.id}" or "${widget.targetIdList[i]}" arrow elements render boxes is either not found or attached ');
          return; // Unable to draw
        }

        final startGlobalOffset = start.localToGlobal(Offset.zero);
        final endGlobalOffset = end.localToGlobal(Offset.zero);

        final startPosition = widget.sourceAnchor
            .resolve(_direction)
            .withinRect(Rect.fromLTWH(startGlobalOffset.dx,
                startGlobalOffset.dy, start.size.width, start.size.height));
        final endPosition = widget.targetAnchor
            .resolve(_direction)
            .withinRect(Rect.fromLTWH(endGlobalOffset.dx,
                endGlobalOffset.dy, end.size.width, end.size.height));

        final paint = Paint()
          ..color = widget.color
          ..style = PaintingStyle.stroke
          ..strokeCap = StrokeCap.round
          ..strokeJoin = StrokeJoin.round
          ..strokeWidth = widget.width;

        final arrow = getArrow(
          startPosition.dx,
          startPosition.dy,
          endPosition.dx,
          endPosition.dy,
          bow: widget.bow,
          stretch: widget.stretch,
          stretchMin: widget.stretchMin,
          stretchMax: widget.stretchMax,
          padStart: widget.padStart,
          padEnd: widget.padEnd,
          straights: widget.straights,
          flip: widget.flip,
        );
        final path = Path()
          ..moveTo(arrow.sx, arrow.sy)
          ..quadraticBezierTo(arrow.cx, arrow.cy, arrow.ex, arrow.ey);

        final lastPathMetric = path.computeMetrics().last;
        final firstPathMetric = path.computeMetrics().first;

        var tan = lastPathMetric.getTangentForOffset(lastPathMetric.length);
        var adjustmentAngle = 0.0;

        final tipLength = widget.tipLength;
        final tipAngleStart = widget.tipAngleOutwards;

        final angleStart = pi - tipAngleStart;
        final originalPosition = tan.position;

        if (lastPathMetric.length > 10) {
          final tanBefore =
              lastPathMetric.getTangentForOffset(lastPathMetric.length - 5);
          adjustmentAngle =
              _getAngleBetweenVectors(tan.vector, tanBefore.vector);
        }

        Offset tipVector;

        tipVector =
            _rotateVector(tan.vector, angleStart - adjustmentAngle) *
                tipLength;
        path.moveTo(tan.position.dx, tan.position.dy);
        path.relativeLineTo(tipVector.dx, tipVector.dy);

        tipVector =
            _rotateVector(tan.vector, -angleStart - adjustmentAngle) *
                tipLength;
        path.moveTo(tan.position.dx, tan.position.dy);
        path.relativeLineTo(tipVector.dx, tipVector.dy);

        if (widget.doubleSided) {
          tan = firstPathMetric.getTangentForOffset(0);
          if (firstPathMetric.length > 10) {
            final tanBefore = firstPathMetric.getTangentForOffset(5);
            adjustmentAngle =
                _getAngleBetweenVectors(tan.vector, tanBefore.vector);
          }

          tipVector =
              _rotateVector(-tan.vector, angleStart - adjustmentAngle) *
                  tipLength;
          path.moveTo(tan.position.dx, tan.position.dy);
          path.relativeLineTo(tipVector.dx, tipVector.dy);

          tipVector =
              _rotateVector(-tan.vector, -angleStart - adjustmentAngle) *
                  tipLength;
          path.moveTo(tan.position.dx, tan.position.dy);
          path.relativeLineTo(tipVector.dx, tipVector.dy);
        }

        path.moveTo(originalPosition.dx, originalPosition.dy);

        canvas.drawPath(path, paint);
      }
    }
    // }
  });

static Offset _rotateVector(Offset vector, double angle) => Offset( cos(angle) vector.dx - sin(angle) vector.dy, sin(angle) vector.dx + cos(angle) vector.dy, );

static double _getVectorsDotProduct(Offset vector1, Offset vector2) => vector1.dx vector2.dx + vector1.dy vector2.dy;

// Clamp to avoid rounding issues when the 2 vectors are equal. static double _getAngleBetweenVectors(Offset vector1, Offset vector2) => acos((_getVectorsDotProduct(vector1, vector2) / (vector1.distance * vector2.distance)) .clamp(-1.0, 1.0));

@override bool shouldRepaint(_ArrowPainter oldDelegate) => !mapEquals(oldDelegate._elements, _elements) || _direction != oldDelegate._direction; }

class ArrowElement extends StatefulWidget { /// Whether to show the arrow final bool show;

/// ID for being targeted by other [ArrowElement]s final String id;

/// The ID of the [ArrowElement] that will be drawn to final String targetId;

final List targetIdList;

/// Where on the source Widget the arrow should start final AlignmentGeometry sourceAnchor;

/// Where on the target Widget the arrow should end final AlignmentGeometry targetAnchor;

/// A [Widget] to be drawn to or from final Widget child;

/// Whether the arrow should be pointed both ways final bool doubleSided;

/// Arrow color final Color color;

/// Arrow width final double width;

/// Length of arrow tip final double tipLength;

/// Outwards angle of arrow tip, in radians final double tipAngleOutwards;

/// A value representing the natural bow of the arrow. /// At 0, all lines will be straight. final double bow;

/// The length of the arrow where the line should be most stretched. Shorter /// distances than 0 will have no additional effect on the bow of the arrow. final double stretchMin;

/// The length of the arrow at which the stretch should have no effect. final double stretchMax;

/// The effect that the arrow's length will have, relative to its minStretch /// and maxStretch, on the bow of the arrow. At 0, the stretch will have no effect. final double stretch;

/// How far the arrow's starting point should be from the provided start point. final double padStart;

/// How far the arrow's ending point should be from the provided end point. final double padEnd;

/// Whether to reflect the arrow's bow angle. final bool flip;

/// Whether to use straight lines at 45 degree angles. final bool straights;

const ArrowElement({ Key key, @required this.id, @required this.child, this.targetId, this.targetIdList, this.show = true, this.sourceAnchor = Alignment.centerLeft, this.targetAnchor = Alignment.centerLeft, this.doubleSided = false, this.color = Colors.blue, this.width = 3, this.tipLength = 15, this.tipAngleOutwards = pi * 0.2, this.bow = 0.2, this.stretchMin = 0, this.stretchMax = 420, this.stretch = 0.5, this.padStart = 0, this.padEnd = 0, this.flip = false, this.straights = true, }) : // targetIdList = [], super(key: key);

@override _ArrowElementState createState() => _ArrowElementState(); }

class _ArrowElementState extends State { _ArrowContainerState _container;

@override void initState() { _container = context.findAncestorStateOfType<_ArrowContainerState>() ..addArrow(this); super.initState(); }

@override void dispose() { _container.removeArrow(widget.id); super.dispose(); }

@override Widget build(BuildContext context) => widget.child; }

DeepStyles commented 4 years ago

sry for mess, made my repo pub here's link its easy now lol

https://github.com/DeepStyles/mindmapwithflutter

DeepStyles commented 4 years ago

its actually working for mobile but not web, seem like flutter web not formidable enough.

Schwusch commented 4 years ago

Sorry for not checking it out yet, I haven't been able to take the time and run your project. Thanks for finding the mobile/web discrepancy, it could perhaps lead to an issue on the Flutter repo.

DeepStyles commented 4 years ago

seem like an flutter issue...