dashstack_poster

flutter_pip_view

Turn any Flutter widget into a floating, draggable Picture-in-Picture window — like the little bubble you see during a video call — while the rest of your app keeps running underneath it.

PiPController.show(context, child: VideoCallView());

That's the whole API to get a floating window on screen. Everything else below is optional polish.

Requires: Dart >= 3.13.0 (see pubspec.yaml)

See it in action

Draggable bubble Dragging it around
Draggable PiP bubble on the example home screen Draggable PiP bubble being dragged around the screen

Draggable PiP demo — drag, resize, and close a floating window

Try it yourself — everything above is running in example/.

Why you'd want this

  • Bring your own content. No video player or calling SDK is baked in — show any widget you like.
  • Feels native. Drag, flick, and let go — the window settles into place with a real spring, not a flat animation.
  • Snaps to the nearest edge automatically when you release it.
  • Tap to go fullscreen, just like tapping a video call preview to bring back the full call.
  • Minimizes to a small bubble and expands back with a tap.
  • Accessible by default — keyboard support, focus ring, and respects the OS "reduce motion" setting.
  • Zero dependencies beyond the Flutter SDK.

Install

dependencies:
  flutter_pip_view: ^0.1.0
flutter pub get

Quick start

Wrap your app once — usually in MaterialApp.builder — so the PiP window can float above every screen:

MaterialApp(
  builder: (context, child) => PiPView(child: child!),
  home: const HomeScreen(),
)

PiPView stays invisible and does nothing until you call the PiP API, so this is safe to add even before you need it.

Then, from anywhere in your app:

// Show a small preview; hand fullscreen a distinct, real full-screen widget.
PiPController.show(
  context,
  child: VideoCallPreview(),
  fullscreenChild: VideoCallScreen(), // shown only while fullscreen
);

PiPController.moveTo(PiPPosition.bottomRight); // move it to a corner
PiPController.minimize();                      // collapse to a bubble
PiPController.expand();                         // restore it
PiPController.enterFullscreen();                // go fullscreen
PiPController.exitFullscreen();                 // back to floating
PiPController.hide();                           // close it

Customizing it

Every option below is optional — const PiPConfig() works fine on its own.

PiPController.show(
  context,
  child: VideoCallView(),
  config: PiPConfig(
    width: 160,
    height: 220,
    minWidth: 120,
    minHeight: 160,
    maxWidth: 400,
    maxHeight: 500,
    borderRadius: 16,
    elevation: 8,
    draggable: true,
    resizable: true,
    snapToEdge: true,
    initialPosition: PiPPosition.bottomRight,
    margin: EdgeInsets.all(12),
    showCloseButton: true,
    showExpandButton: true,
    enableHapticFeedback: true,
    tapToFullscreen: true,       // tap the window to go fullscreen
    doubleTapToToggle: false,    // opt-in, for content with its own buttons
    resetOnClose: true,          // closing resets position/size/state
    showDragHandle: true,        // small grip when expanded
  ),
);

See the dartdoc on PiPConfig for the full list, including custom decoration, padding, closeButtonBuilder, expandButtonBuilder, and controlsBuilder.

Listening for events

PiPController.show(
  context,
  child: VideoCallView(),
  callbacks: PiPCallbacks(
    onShow: () {},
    onHide: () {},
    onExpand: () {},
    onMinimize: () {},
    onFullscreenEnter: () {}, // e.g. pause the small preview, start the full one
    onFullscreenExit: () {},
    onDragStart: () {},
    onDragUpdate: (offset) {},
    onDragEnd: () {},
    onPositionChanged: (offset) {},
    onSizeChanged: (size) {},
  ),
);

Swapping in your own controls

PiPConfig(
  controlsBuilder: (
    context,
    isMinimized,
    isFullscreen,
    onClose,
    onToggleMinimize,
    onToggleFullscreen,
  ) {
    return Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      child: MyControlBar(
        onClose: onClose,
        onCollapse: onToggleMinimize,
        onToggleFullscreen: onToggleFullscreen,
      ),
    );
  },
)

A typical video-call screen keeps the small floating preview passive (no buttons) and puts the real controls (mic, camera, end call) inside a separate fullscreenChild — see FullCallScreen in example/lib/main.dart.

Good to know

  • Closing really cleans up. hide() unmounts your child/ fullscreenChild once the close animation finishes, so any camera/audio stream, timer, or socket it holds gets disposed normally. Calling show() again before that animation ends just cancels the pending disposal instead of tearing everything down.
  • Closing resets the window by default — position, size, minimized and fullscreen state all clear, so the next show() starts fresh. Set PiPConfig.resetOnClose: false if you'd rather it remember where it was left.

How it's put together

Piece What it does
PiPManager The engine — holds all state (child, config, position, size, etc). Use your own instance for more than one independent PiP scope.
PiPController Static facade over the default PiPManager — .show(), .hide(), .expand(), .minimize(), .enterFullscreen(), .exitFullscreen(), .moveTo().
PiPFloatingWindow The only widget that touches gestures/animation — rebuilds stay scoped here.
PiPView A thin Stack of your app plus PiPFloatingWindow, for teams who'd rather have it in the widget tree than call show() cold.

None of these know or care what they're displaying — PiPView(child: AnythingAtAll()) just works.

What about OS-level PiP (Android/iOS system PiP)?

NativePiPController is a separate, optional hook for OS-owned PiP (like Android's enterPictureInPictureMode). No platform channel ships with this package — Android can hand any Activity to system PiP, but iOS only offers it through AVPictureInPictureController, which is tied to a video pipeline and doesn't fit a "shrink whatever's on screen" package. Until a host app registers handlers on NativePiPController.channel, its methods are safe no-ops. For anything short of "hand the whole Activity to Android's OS," use the in-app PiPView engine described above instead.

Example

Check example/lib/main.dart for a full demo: a floating widget with tap-to-fullscreen (opening a real FullCallScreen with mic/camera/end-call controls) plus drag and snap-to-edge. The rest of the API — expand() / minimize(), moveTo(), custom controls/size — is covered by test/pip_view_widget_test.dart.

Bugs & questions

Report bugs and ask questions on GitHub Issues.

Maintained by Dashstack Infotech, Surat.

Libraries

flutter_pip_view
A reusable, production-ready Picture-in-Picture (PiP) floating view engine for Flutter. Turn any widget into a draggable, resizable, edge-snapping floating window with a few lines of code: