Repository: flutter
Issue: Allow start dragging a Draggable from within its subtree.
Labels: c: new feature,framework,c: proposal,P3,team-framework,triaged-framework

Hi Team, happy new year!

## Use case

I would like to be able to start dragging a `Draggable` widget from another widget within it's subtree. Example:
```dart
Draggable(
  child: ListTile(
    leading: Icon(Icons.drag_indicator), // Widget that starts the Draggable's drag
  ),
);
```

## Proposal

We could have an static method on the `Draggable` class that would start the drag.
```dart
// Inside Draggable class definition
static void maybeStartDragging<T extends Object>(BuildContext context, PointerDownEvent event) {
  final _DraggableState<T>? draggableState = context.findAncestorStateOfType<_DraggableState<T>>();

  // We need this check otherwise we would get two identical pointer events 
  // and the following assertion would trigger:
  // package:flutter/src/gestures/multidrag.dart: Failed assertion: line 238 pos 12: !_pointers!.containsKey(event.pointer): is not true.
  assert(
    () {
      if (draggableState == null || draggableState.widget.delegateDragStart) {
        return true;
      }
      throw FlutterError('Helpful message here.');
    }(),
  );
  
  if (draggableState != null && draggableState.canDrag) {
    draggableState._routePointer(event);
  }
}
```
A flag would be needed in both `Draggable` and `LongPressDraggable` widgets to disable the pointer event handling of `_DraggableState.build()`.
```dart
class Draggable<T extends Object> extends StatefulWidget {
  Draggable({
    /* ... */
    this.delegateDragStart = false,
  });

  /* ... */

  final bool delegateDragStart; // a better name would be great
}

class LongPressDraggable<T extends Object> extends Draggable<T> {
  LongPressDraggable({
    bool delegateDragStart = false,
  )} : super(delegateDragStart: delegateDragStart);
}

class _DraggableState<T extends Object> extends State<Draggable<T>> {
  // Moved from within build()
  bool get canDrag {
    return widget.maxSimultaneousDrags == null || _activeCount < widget.maxSimultaneousDrags!;
  }

  @override
  Widget build(BuildContext context) {
    assert(Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay) != null);

    final bool showChild = _activeCount == 0 || widget.childWhenDragging == null;

    final Widget? effectiveChild = showChild ? widget.child : widget.childWhenDragging;

    // Returning early if the dragging is going to be handled down the tree.
    if (widget.delegateDragStart) {
      return effectiveChild ?? const SizedBox();
    }

    return Listener(
      behavior: widget.hitTestBehavior,
      onPointerDown: canDrag ? _routePointer : null,
      child: effectiveChild,
    );
  }
}
```
Then my draggable setup would look something like this:

```dart
class MyDraggableTile extends StatelessWidget {
  const MyDraggableTile({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Draggable<Object>(
      delegateDragStart: true,
      child: ListTile(
        leading: DraggableHandle<Object>(), // in a separate widget to get a new context
      ),
    );
  }
}

class DraggableHandle<T extends Object> extends StatelessWidget {
  const DraggableHandle({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Listener(
      child: const Icon(Icons.drag_indicator),
      onPointerDown: (PointerDownEvent event) {
        Draggable.maybeStartDragging<T>(context, event);
      },
    );
  }
}
```

If this change is implemented. the `DraggableHandle` widget above could be part of the framework.

---
If there's another way to do what I'm aiming for here, please let me know and I'll close this issue as resolved immediately.

Thank you for your time, and keep the great work team! :blue_heart: