selectable_area 1.0.3 copy "selectable_area: ^1.0.3" to clipboard
selectable_area: ^1.0.3 copied to clipboard

A powerful and flexible Flutter widget that allows users to select and capture a rectangular area over any child widget.

example/lib/main.dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:selectable_area/selectable_area.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'SelectableArea Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ExampleScreen(),
    );
  }
}

// The main screen that hosts the examples.
class ExampleScreen extends StatefulWidget {
  const ExampleScreen({super.key});

  @override
  State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
  // --- STATE VARIABLES ---

  // Controls whether the selection functionality is enabled on the SelectableArea widgets.
  bool _isSelectionEnabled = true;

  // Determines the output format (base64 string or raw bytes) for the captured selection.
  SelectionOutputType _outputType = SelectionOutputType.base64;

  /// This callback is triggered by the SelectableArea widget when a selection is complete.
  /// It handles both `base64` (String) and `bytes` (Uint8List) output types.
  void _handleSelection(dynamic selectionResult) {
    Uint8List imageBytes;
    String resultMeta;

    // Determine the type of the result and decode if necessary.
    if (selectionResult is String) {
      imageBytes = base64Decode(selectionResult);
      resultMeta =
          "Received base64 string of length: ${selectionResult.length}";
    } else if (selectionResult is Uint8List) {
      imageBytes = selectionResult;
      resultMeta = "Received Uint8List of length: ${selectionResult.length}";
    } else {
      // Exit if the result is not a recognized type.
      return;
    }

    debugPrint(resultMeta);

    // Show a dialog with the captured image.
    showDialog(
      context: context,
      builder: (context) => Dialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16.0),
        ),
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                'Selection Captured!',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
              const SizedBox(height: 16),
              // Display the captured image from its byte data.
              ClipRRect(
                borderRadius: BorderRadius.circular(12.0),
                child: Image.memory(imageBytes),
              ),
              const SizedBox(height: 16),
              Container(
                padding: const EdgeInsets.symmetric(
                  horizontal: 12,
                  vertical: 8,
                ),
                decoration: BoxDecoration(
                  color: Theme.of(
                    context,
                  ).colorScheme.secondaryContainer.withAlpha(100),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Text(
                  'Output Type: ${_outputType.name}',
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    color: Theme.of(context).colorScheme.onSecondaryContainer,
                  ),
                ),
              ),
              const SizedBox(height: 20),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton.icon(
                  icon: const Icon(Icons.check_circle_outline),
                  label: const Text('OK'),
                  onPressed: () => Navigator.of(context).pop(),
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(vertical: 12),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // DefaultTabController coordinates the TabBar and TabBarView.
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(title: const Text('SelectableArea'), centerTitle: true),
        body: Column(
          children: [
            // Tabs to switch between the different examples.
            const TabBar(
              tabs: [
                Tab(text: 'Image Example', icon: Icon(Icons.image_outlined)),
                Tab(text: 'Widget Example', icon: Icon(Icons.widgets_outlined)),
              ],
            ),
            // The global controls for changing the output type.
            _buildGlobalControls(),
            Expanded(
              child: TabBarView(
                // Disable swiping between tabs to prevent gesture conflicts.
                physics: NeverScrollableScrollPhysics(),
                children: [_buildImageExample(), _buildWidgetExample()],
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// Builds the toggle buttons for choosing the output format (Base64 or Bytes).
  Widget _buildGlobalControls() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
      child: Center(
        child: ToggleButtons(
          isSelected: [
            _outputType == SelectionOutputType.base64,
            _outputType == SelectionOutputType.bytes,
          ],
          onPressed: (index) {
            setState(() {
              _outputType = index == 0
                  ? SelectionOutputType.base64
                  : SelectionOutputType.bytes;
            });
          },
          borderRadius: BorderRadius.circular(8.0),
          selectedColor: Colors.white,
          color: Theme.of(context).colorScheme.primary,
          fillColor: Theme.of(context).colorScheme.primary,
          splashColor: Theme.of(context).colorScheme.primary.withAlpha(31),
          hoverColor: Theme.of(context).colorScheme.primary.withAlpha(10),
          constraints: const BoxConstraints(minHeight: 40.0, minWidth: 170.0),
          children: const [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.data_object_outlined),
                SizedBox(width: 8),
                Text('Base64 Output'),
              ],
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.memory_outlined),
                SizedBox(width: 8),
                Text('Bytes Output'),
              ],
            ),
          ],
        ),
      ),
    );
  }

  /// Builds a simple card with a ListTile and a Switch to enable or disable selection.
  Widget _buildEnableSelectionCard() {
    return Card(
      child: ListTile(
        title: const Text('Enable Selection'),
        trailing: Transform.scale(
          scale: 0.8, // Makes the switch smaller
          child: Switch(
            value: _isSelectionEnabled,
            onChanged: (value) => setState(() => _isSelectionEnabled = value),
          ),
        ),
      ),
    );
  }

  /// Builds the first example, showing how to select an area over a static Image widget.
  Widget _buildImageExample() {
    return Column(
      children: [
        const Text(
          'This example shows how to select an area over an image.',
          textAlign: TextAlign.center,
        ),
        const SizedBox(height: 16),
        _buildEnableSelectionCard(),
        const SizedBox(height: 16),
        Card(
          elevation: 2,
          clipBehavior: Clip.antiAlias,
          child: SelectableArea(
            isEnabled: _isSelectionEnabled,
            outputType: _outputType,
            onSelectionEnd: _handleSelection,
            child: Image.network(
              'https://e7.pngegg.com/pngimages/441/311/png-clipart-identity-document-identification-security-hologram-badge-template-identity-card-company-text.png',
              // Example receipt image
              loadingBuilder: (context, child, progress) {
                return progress == null
                    ? child
                    : const Center(
                        heightFactor: 4,
                        child: CircularProgressIndicator(),
                      );
              },
            ),
          ),
        ),
      ],
    );
  }

  /// Builds the second example, showing how to select an area over a scrollable list of widgets.
  Widget _buildWidgetExample() {
    return SingleChildScrollView(
      /* --- IMPORTANT: SCROLLING FIX ---
       This is the core of the solution to the gesture conflict.

       THE PROBLEM: A `SingleChildScrollView` and the `GestureDetector` inside
       `SelectableArea` both want to respond to a vertical drag gesture.
       This creates an ambiguity where the app might scroll while the user is
       trying to draw a selection box, leading to incorrect coordinates.

       THE SOLUTION: We dynamically change the scroll physics.
       - When the user is selecting (`_isSelecting` is true), we use
         `NeverScrollableScrollPhysics()` to completely DISABLE scrolling.
      - When the user is not selecting, we use normal `ClampingScrollPhysics()`.

       The `_isSelecting` flag is controlled by the `onDragStarted` and
       `onDragEnded` callbacks provided by the `SelectableArea` widget.*/
      physics: _isSelectionEnabled
          ? const NeverScrollableScrollPhysics()
          : const ClampingScrollPhysics(),
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          const Text(
            'This example shows how to select an area over any widget, like this list of contacts.',
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          _buildEnableSelectionCard(),
          const SizedBox(height: 16),
          Card(
            elevation: 2,
            clipBehavior: Clip.antiAlias,
            child: SelectableArea(
              isEnabled: _isSelectionEnabled,
              outputType: _outputType,
              onSelectionEnd: _handleSelection,
              child: ListView(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                children: const [
                  ListTile(
                    leading: Icon(Icons.person),
                    title: Text('Brenda'),
                    subtitle: Text('(555) 123-4567'),
                  ),
                  Divider(height: 1),
                  ListTile(
                    leading: Icon(Icons.person_outline),
                    title: Text('David'),
                    subtitle: Text('(555) 987-6543'),
                  ),
                  Divider(height: 1),
                  ListTile(
                    leading: Icon(Icons.person_pin),
                    title: Text('Charles'),
                    subtitle: Text('(555) 555-1212'),
                  ),
                  Divider(height: 1),
                  ListTile(
                    leading: Icon(Icons.person),
                    title: Text('Emily'),
                    subtitle: Text('(555) 345-6789'),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
6
likes
160
points
5
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful and flexible Flutter widget that allows users to select and capture a rectangular area over any child widget.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on selectable_area