How to simulate ripple effect and execute onPressed of button in Flutter?

Viewed 277

I'm trying to mimic the behavior of tapping with ripple on a MaterialButton in Flutter. In essence I would like to be able to call some arbitrary function (e.g. tapTargetWithEffect(GlobalKey target)) and see ripple effect on the button with given GlobalKey as well the onPressed callback to be executed.

I see in the pointer tests that it's possible to dispatch pointer events through WidgetsBinding to trigger ripple effect. How to execute the onPressed callbacks?

1 Answers

Here's the naive implementation how this effect can be achieved.

  1. First, find the RenderBox assigned to the given GlobalKey
  2. Then check if it's a button in a semantic sense (i.e. it's possible to press/tap)
  3. Execute hit test on the RenderBox to get list of widgets (HitTestEntry objects) under given pointer position
  4. Find targets that have onTap callbacks assigned (these will implement RenderSemanticsGestureHandler), take first and assign to tappable
  5. Find targets that have onPointerDown callbacks assigned (these will implement RenderPointerListener), take first and assign to clickable
  6. Call the onTap callback to execute button's onPressed
  7. Call the onPointerDown with proper PointerDownEvent to show ripple
    1. PointerDownEvent accepts position argument, make it center of the original RenderBox
  8. Cancel the PointerDownEvent after desired Duration by dispatching PointerMoveEvent somewhere within the app boundaries via WidgetsBinding.instance.dispatchEvent
    1. You'll have to execute hit test before that

Notice that hitTest() function accepts BoxHitTestResult which will contain result of the hit test :)

enter image description here

Sample implementation:

  void tapTargetWithEffect(GlobalKey target) {
    final RenderBox renderBox = target.currentContext.findRenderObject();
    final child = renderBox as RenderSemanticsAnnotations;
    if (child.button) {
      final size = renderBox.size;
      final center = renderBox.localToGlobal(size.center(Offset.zero));
      final hitResult = BoxHitTestResult();
      child.hitTest(hitResult, position: size.center(Offset.zero));
      final pointerDown = PointerDownEvent(
        position: center,
      );
      final tappableL = hitResult.path
          .where((element) => element.target is RenderSemanticsGestureHandler)
          .map((element) => element.target as RenderSemanticsGestureHandler)
          .toList();
      final clickableL = hitResult.path
          .where((element) => element.target is RenderPointerListener)
          .map((e) => e.target as RenderPointerListener)
          .toList();

      if (tappableL.isEmpty) {
        print('No RenderSemanticsGestureHandler available');
        return;
      }

      if (clickableL.isEmpty) {
        print('No RenderPointerListener available');
        return;
      }

      final tappable = tappableL.first;
      final clickable = clickableL.first;
      clickable.onPointerDown?.call(pointerDown);
      tappable.onTap?.call();

      // cancel the pointer down event
      Future.delayed(Duration(milliseconds: 500)).then((value) {
        print('Cancelling the pointer down event');
        final result = BoxHitTestResult();
        WidgetsBinding.instance.hitTest(result, Offset.zero);
        WidgetsBinding.instance.dispatchEvent(PointerMoveEvent(), result);
      });
    }
  }
Related