flutter_pip_view 0.0.2 copy "flutter_pip_view: ^0.0.2" to clipboard
flutter_pip_view: ^0.0.2 copied to clipboard

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.

example/lib/main.dart

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

void main() => runApp(const PiPExampleApp());

/// Demonstrates `flutter_pip_view`. The important bit is `PiPView` wrapping
/// the whole app once, in `MaterialApp.builder`, so any screen can call
/// `PiPController.show(...)` and have the floating window appear on top of
/// whatever is currently displayed — including across navigation.
class PiPExampleApp extends StatefulWidget {
  const PiPExampleApp({super.key});

  @override
  State<PiPExampleApp> createState() => _PiPExampleAppState();
}

class _PiPExampleAppState extends State<PiPExampleApp> {
  ThemeMode _themeMode = ThemeMode.light;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light
          ? ThemeMode.dark
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_pip_view example',
      debugShowCheckedModeBanner: false,
      themeMode: _themeMode,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      builder: (context, child) => PiPView(child: child!),
      home: HomeScreen(themeMode: _themeMode, onToggleTheme: _toggleTheme),
    );
  }
}

/// Generic placeholder content shown inside the PiP window. Deliberately
/// not video-player-specific — any widget works here (a chat thread, a map,
/// a mini dashboard, ...).
class DemoContent extends StatelessWidget {
  const DemoContent({super.key, required this.label, required this.color});

  final String label;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      alignment: Alignment.center,
      // `FittedBox` scales the content down as needed — this same widget
      // is shown both at the configured expanded size and, unchanged, in
      // the small minimized bubble, so it must degrade gracefully rather
      // than overflow when squeezed.
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: Padding(
          padding: const EdgeInsets.all(8),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.videocam_rounded, color: Colors.white, size: 32),
              const SizedBox(height: 8),
              Text(
                label,
                textAlign: TextAlign.center,
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 12,
                  decoration: TextDecoration.none,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// The distinct widget shown while fullscreen — handed to the engine via
/// `PiPController.show(fullscreenChild: ...)` rather than just letting the
/// small [DemoContent] preview stretch. This is where a real app would put
/// its actual call screen, complete with its own interactive controls;
/// `end call` below is just `PiPController.hide()`, the same generic call
/// any consumer of this package uses to dismiss the window.
class FullCallScreen extends StatelessWidget {
  const FullCallScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return ColoredBox(
      color: const Color(0xFF12132E),
      child: SafeArea(
        child: Column(
          children: [
            // `Expanded` + `FittedBox` (not a fixed-size `Spacer` sandwich):
            // this guarantees no overflow no matter how little vertical
            // space is actually available, the same defensive pattern
            // `DemoContent` uses for the minimized bubble.
            Expanded(
              child: Center(
                child: FittedBox(
                  fit: BoxFit.scaleDown,
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        const Icon(
                          Icons.account_circle_rounded,
                          color: Colors.white54,
                          size: 72,
                        ),
                        const SizedBox(height: 12),
                        const Text(
                          'You\'re in full screen',
                          style: TextStyle(color: Colors.white, fontSize: 18),
                        ),
                        const Text(
                          'A real call screen, not a stretched preview',
                          textAlign: TextAlign.center,
                          style: TextStyle(color: Colors.white54),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(bottom: 32),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  _CallButton(icon: Icons.mic_rounded, onPressed: () {}),
                  _CallButton(
                    icon: Icons.call_end_rounded,
                    background: Colors.redAccent,
                    size: 56,
                    // App code, not a package API: ending the call is just
                    // hiding the window.
                    onPressed: PiPController.hide,
                  ),
                  _CallButton(icon: Icons.videocam_rounded, onPressed: () {}),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _CallButton extends StatelessWidget {
  const _CallButton({
    required this.icon,
    required this.onPressed,
    this.background = const Color(0x33FFFFFF),
    this.size = 44,
  });

  final IconData icon;
  final VoidCallback onPressed;
  final Color background;
  final double size;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: background,
      shape: const CircleBorder(),
      child: InkWell(
        onTap: onPressed,
        customBorder: const CircleBorder(),
        child: SizedBox(
          width: size,
          height: size,
          child: Icon(icon, color: Colors.white, size: size * 0.5),
        ),
      ),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({
    super.key,
    required this.themeMode,
    required this.onToggleTheme,
  });

  final ThemeMode themeMode;
  final VoidCallback onToggleTheme;

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool _snapToEdge = true;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Scaffold(
      backgroundColor: scheme.surfaceContainerLowest,
      appBar: AppBar(
        title: const Text(
          'flutter_pip_view',
          style: TextStyle(fontWeight: FontWeight.w700),
        ),
        actions: [
          IconButton(
            tooltip: 'Toggle light/dark theme',
            icon: Icon(
              widget.themeMode == ThemeMode.light
                  ? Icons.dark_mode_outlined
                  : Icons.light_mode_outlined,
            ),
            onPressed: widget.onToggleTheme,
          ),
          const SizedBox(width: 4),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
        children: [
          _FeatureCard(
            icon: Icons.picture_in_picture_alt_rounded,
            title: 'Floating preview',
            description:
                'A small window that floats on top of your app. Tap it to '
                'go full screen, just like a video call bubble.',
            child: SizedBox(
              width: double.infinity,
              child: FilledButton.icon(
                icon: const Icon(Icons.smart_display_rounded),
                label: const Text('Show PiP'),
                onPressed: () => PiPController.show(
                  context,
                  child: const DemoContent(
                    label: 'Tap me to go\nfullscreen',
                    color: Colors.indigo,
                  ),
                  fullscreenChild: const FullCallScreen(),
                ),
              ),
            ),
          ),
          const SizedBox(height: 16),
          _FeatureCard(
            icon: Icons.open_with_rounded,
            title: 'Drag & snap',
            description:
                'Move the window anywhere you like. Let go near an edge '
                'and it glides into place on its own.',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                _SettingRow(
                  label: 'Snap to nearest edge on release',
                  value: _snapToEdge,
                  onChanged: (v) => setState(() => _snapToEdge = v),
                ),
                const SizedBox(height: 14),
                SizedBox(
                  width: double.infinity,
                  child: FilledButton.tonalIcon(
                    icon: const Icon(Icons.open_with_rounded),
                    label: const Text('Show draggable PiP'),
                    onPressed: () => PiPController.show(
                      context,
                      child: const DemoContent(
                        label: 'Drag me',
                        color: Colors.teal,
                      ),
                      config: PiPConfig(snapToEdge: _snapToEdge),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// A single demo entry: icon badge + title + description + whatever
/// interactive controls the demo needs, on a soft-shadowed, rounded card —
/// the same "feature card" shape used across the flutter_pip_view example
/// and sibling DashStack package examples.
class _FeatureCard extends StatelessWidget {
  const _FeatureCard({
    required this.icon,
    required this.title,
    required this.description,
    required this.child,
  });

  final IconData icon;
  final String title;
  final String description;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final textTheme = Theme.of(context).textTheme;
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: scheme.surface,
        borderRadius: BorderRadius.circular(24),
        border: Border.all(color: scheme.outlineVariant.withValues(alpha: 0.4)),
        boxShadow: [
          BoxShadow(
            color: scheme.shadow.withValues(alpha: 0.06),
            blurRadius: 24,
            offset: const Offset(0, 10),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(10),
                decoration: BoxDecoration(
                  color: scheme.primaryContainer,
                  borderRadius: BorderRadius.circular(14),
                ),
                child: Icon(icon, color: scheme.onPrimaryContainer),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Text(
                  title,
                  style: textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Text(
            description,
            style: textTheme.bodyMedium?.copyWith(
              color: scheme.onSurfaceVariant,
              height: 1.4,
            ),
          ),
          const SizedBox(height: 16),
          child,
        ],
      ),
    );
  }
}

/// A settings-style toggle row (label + switch) on a subtle tinted
/// background, instead of a bare `SwitchListTile` — reads as part of the
/// card rather than a plain list item.
class _SettingRow extends StatelessWidget {
  const _SettingRow({
    required this.label,
    required this.value,
    required this.onChanged,
  });

  final String label;
  final bool value;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Material(
      color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
      borderRadius: BorderRadius.circular(14),
      child: InkWell(
        borderRadius: BorderRadius.circular(14),
        onTap: () => onChanged(!value),
        child: Padding(
          padding: const EdgeInsets.only(left: 14, right: 6),
          child: Row(
            children: [
              Expanded(
                child: Text(
                  label,
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
              ),
              Switch(value: value, onChanged: onChanged),
            ],
          ),
        ),
      ),
    );
  }
}
1
likes
150
points
105
downloads

Documentation

API reference

Publisher

verified publisherdashstack.tech

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#picture-in-picture #pip #floating-window #overlay #video

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_pip_view