Skip to content

Draggable: onDragUpdate callback #39490

Description

@rostopira

Use case

I'm creating widget, like free-transform in photoshop
I have main draggable (big rectangle) and small draggable (small circles) in each corner
To be able to update all their positions I had to add onDragUpdate callback to Draggable

Proposal

Draggable should have parameter Function(DragUpdateDetails) onDragUpdate

I can provide a PR if any maintainer approve this as valid feature

Activity

  1. rostopira commented on Aug 29, 2019

    @rostopira
    ContributorAuthor

    It's very similar to #16873

  2. added
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    on Aug 29, 2019
  3. emvaized commented on May 26, 2020

    @emvaized
    Contributor

    Any updates on it?

  4. julianoappelklein commented on Jun 2, 2020

    @julianoappelklein

    As a workaround I'm passing a GlobalKey to the feedback widget to get it's position when needed. I can get the change comparing the first position with the current.

  5. emvaized commented on Jun 11, 2020

    @emvaized
    Contributor

    As a workaround I'm passing a GlobalKey to the feedback widget to get it's position when needed. I can get the change comparing the first position with the current.

    Not sure if it will fit in my case -- I want to update childWhenDragging's position according to feedback widget position, e.g. make it follow feedback like a shadow :-)

  6. julianoappelklein commented on Jun 12, 2020

    @julianoappelklein

    @emvaized - See the class I'm using below. It's not pretty, but it works.

    When using it, If you wrap your childWhenDragging with a Transform.translate to adjust its position using the offset from onDragUpdated.

    Please note that this class is not worried about the target. You may need to adapt it to suits your needs.

    import 'package:flutter/material.dart';
    import 'package:flutter/widgets.dart';
    
    class SimpleDraggable extends StatefulWidget {
      SimpleDraggable({
        Key key,
        this.child,
        this.onDragStarted,
        this.onDragUpdated,
        this.onDragEnd,
        this.feedback,
        this.affinity,
        this.axis,
        this.feedbackOffset = Offset.zero,
        this.dragAnchor = DragAnchor.child,
        this.childWhenDragging,
      }) : super(key: key);
    
      final void Function() onDragStarted;
      final void Function(Offset offset) onDragUpdated;
      final Offset feedbackOffset;
      final void Function(Offset offset) onDragEnd;
      final Widget feedback;
      final Axis axis;
      final Axis affinity;
      final Widget child;
      final Widget childWhenDragging;
      final DragAnchor dragAnchor;
    
      @override
      _SimpleDraggableState createState() => _SimpleDraggableState();
    }
    
    class _SimpleDraggableState extends State<SimpleDraggable> {
      Offset _currentPosition;
      GlobalKey _dragFeedback = GlobalKey();
      Future<dynamic> _updateFuture;
      Offset _startPosition;
    
      @override
      Widget build(BuildContext context) {
        return Draggable(
          affinity: widget.affinity,
          axis: widget.axis,
          child: widget.child,
          childWhenDragging: widget.childWhenDragging,
          dragAnchor: widget.dragAnchor,
          feedbackOffset: widget.feedbackOffset,
          feedback: Container(
            key: _dragFeedback,
            child: widget.feedback,
          ),
          maxSimultaneousDrags: 1,
          onDragStarted: handleDragStarted,
          onDragEnd: handleDragEnd,
        );
      }
    
      Offset getUpdatePosition() {
        if (_dragFeedback.currentContext == null) return null;
        final RenderBox feebackRenderBox =
            _dragFeedback.currentContext.findRenderObject();
        return feebackRenderBox.localToGlobal(Offset.zero);
      }
    
      void scheduleNextUpdate() {
        if(!this.mounted) return;
        _updateFuture = Future.delayed(const Duration(milliseconds: 20), () {
          if (_updateFuture == null) {
            return;
          }
          final position = getUpdatePosition();
          if (position != null) {
            if (_startPosition == null) {
              _startPosition = position;
              _currentPosition = position;
              widget.onDragStarted?.call();
            } else {
              _currentPosition = position;
              this.widget.onDragUpdated(_currentPosition - _startPosition);
            }
          }
          scheduleNextUpdate();
        });
      }
    
      void handleDragStarted() {
        scheduleNextUpdate();
      }
    
      void handleDragEnd(DraggableDetails details) {
        if (_startPosition != null) {
          widget.onDragEnd
              ?.call((_currentPosition ?? _startPosition) - _startPosition);
        }
        _updateFuture = null;
        _startPosition = null;
        _currentPosition = null;
      }
    }
    
    
  7. emvaized commented on Jun 13, 2020

    @emvaized
    Contributor

    @julianoappelklein Wow, your solution actually works great!
    I guess it's a perfect workaround until Flutter team will implement this feature. Thank you for sharing!

  8. cms103 commented on Jul 12, 2020

    @cms103

    Here is a different approach which provides a drop in replacement for Draggable and provides an onDragUpdate property:

    import 'package:flutter/gestures.dart';
    import 'package:flutter/widgets.dart';
    
    /// Adds an additional property onDragUpdate that receives calls during a drag event
    /// All other behaviour is inherited from Draggable
    class TrackingDraggable<T> extends Draggable<T> {
      final GestureDragUpdateCallback onDragUpdate;
    
      TrackingDraggable(
          {Key key,
          @required child,
          @required feedback,
          data,
          axis,
          childWhenDragging,
          feedbackOffset = Offset.zero,
          dragAnchor = DragAnchor.child,
          affinity,
          maxSimultaneousDrags,
          onDragStarted,
          onDraggableCanceled,
          onDragEnd,
          onDragCompleted,
          ignoringFeedbackSemantics = true,
          this.onDragUpdate})
          : super(
                key: key,
                child: child,
                feedback: feedback,
                data: data,
                axis: axis,
                childWhenDragging: childWhenDragging,
                feedbackOffset: feedbackOffset,
                dragAnchor: dragAnchor,
                affinity: affinity,
                maxSimultaneousDrags: maxSimultaneousDrags,
                onDragStarted: onDragStarted,
                onDraggableCanceled: onDraggableCanceled,
                onDragEnd: onDragEnd,
                onDragCompleted: onDragCompleted,
                ignoringFeedbackSemantics: ignoringFeedbackSemantics);
    
      /// Hooks into the Draggable start point so we can proxy Drag events and provide updates
      @protected
      MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(
          GestureMultiDragStartCallback onStart) {
        var originalRecognizer = super.createRecognizer(onStart);
    
        // Intercept the onStart and insert our proxy
        var originalOnStartFunction = originalRecognizer.onStart;
    
        originalRecognizer.onStart = (Offset position) {
          // Get the original Drag
          var originalDrag = originalOnStartFunction(position);
          // Proxy it
          return _DragProxy(originalDrag, this);
        };
    
        // We've now inserted our onStart, so return
        return originalRecognizer;
      }
    }
    
    class _DragProxy implements Drag {
      Drag _originalDrag;
      TrackingDraggable _ourTracker;
    
      _DragProxy(this._originalDrag, this._ourTracker);
    
      @override
      void cancel() {
        _originalDrag.cancel();
      }
    
      @override
      void end(DragEndDetails details) {
        _originalDrag.end(details);
      }
    
      @override
      void update(DragUpdateDetails details) {
        if (_ourTracker.onDragUpdate != null) {
          _ourTracker.onDragUpdate(details);
        }
        // Call the original as well.
        _originalDrag.update(details);
      }
    }
    
  9. gabber235 commented on Nov 13, 2020

    @gabber235
    Contributor

    I would like to be assigned to this issue

  10. gabber235 commented on Mar 11, 2021

    @gabber235
    Contributor

    This has now been added to the stable channel, hence this issue may be closed.

  11. added a commit that references this issue on Feb 9, 2023
  12. added a commit that references this issue on Feb 9, 2023
  13. github-actions commented on Mar 4, 2023

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  14. locked as resolved and limited conversation to collaborators on Mar 4, 2023
  15. added a commit that references this issue on Jul 25, 2025
    8c0be3c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    c: new featureNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions