| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:math' as math; |
| |
| import 'package:flutter/material.dart'; |
| |
| import 'package:palette_generator/palette_generator.dart'; |
| |
| void main() => runApp(const MyApp()); |
| |
| const Color _kBackgroundColor = Color(0xffa0a0a0); |
| const Color _kSelectionRectangleBackground = Color(0x15000000); |
| const Color _kSelectionRectangleBorder = Color(0x80000000); |
| const Color _kPlaceholderColor = Color(0x80404040); |
| |
| /// The main Application class. |
| class MyApp extends StatelessWidget { |
| /// Creates the main Application class. |
| const MyApp({super.key}); |
| |
| // This widget is the root of your application. |
| @override |
| Widget build(BuildContext context) { |
| return MaterialApp( |
| title: 'Image Colors', |
| theme: ThemeData( |
| primarySwatch: Colors.green, |
| ), |
| home: const ImageColors( |
| title: 'Image Colors', |
| image: AssetImage('assets/landscape.png'), |
| imageSize: Size(256.0, 170.0), |
| ), |
| ); |
| } |
| } |
| |
| /// The home page for this example app. |
| @immutable |
| class ImageColors extends StatefulWidget { |
| /// Creates the home page. |
| const ImageColors({ |
| super.key, |
| this.title, |
| required this.image, |
| this.imageSize, |
| }); |
| |
| /// The title that is shown at the top of the page. |
| final String? title; |
| |
| /// This is the image provider that is used to load the colors from. |
| final ImageProvider image; |
| |
| /// The dimensions of the image. |
| final Size? imageSize; |
| |
| @override |
| State<ImageColors> createState() { |
| return _ImageColorsState(); |
| } |
| } |
| |
| class _ImageColorsState extends State<ImageColors> { |
| Rect? region; |
| Rect? dragRegion; |
| Offset? startDrag; |
| Offset? currentDrag; |
| PaletteGenerator? paletteGenerator; |
| |
| final GlobalKey imageKey = GlobalKey(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| if (widget.imageSize != null) { |
| region = Offset.zero & widget.imageSize!; |
| } |
| _updatePaletteGenerator(region); |
| } |
| |
| Future<void> _updatePaletteGenerator(Rect? newRegion) async { |
| paletteGenerator = await PaletteGenerator.fromImageProvider( |
| widget.image, |
| size: widget.imageSize, |
| region: newRegion, |
| maximumColorCount: 20, |
| ); |
| setState(() {}); |
| } |
| |
| // Called when the user starts to drag |
| void _onPanDown(DragDownDetails details) { |
| final RenderBox box = |
| imageKey.currentContext!.findRenderObject()! as RenderBox; |
| final Offset localPosition = box.globalToLocal(details.globalPosition); |
| setState(() { |
| startDrag = localPosition; |
| currentDrag = localPosition; |
| dragRegion = Rect.fromPoints(localPosition, localPosition); |
| }); |
| } |
| |
| // Called as the user drags: just updates the region, not the colors. |
| void _onPanUpdate(DragUpdateDetails details) { |
| setState(() { |
| currentDrag = currentDrag! + details.delta; |
| dragRegion = Rect.fromPoints(startDrag!, currentDrag!); |
| }); |
| } |
| |
| // Called if the drag is canceled (e.g. by rotating the device or switching |
| // apps) |
| void _onPanCancel() { |
| setState(() { |
| dragRegion = null; |
| startDrag = null; |
| }); |
| } |
| |
| // Called when the drag ends. Sets the region, and updates the colors. |
| Future<void> _onPanEnd(DragEndDetails details) async { |
| final Size? imageSize = imageKey.currentContext?.size; |
| Rect? newRegion; |
| |
| if (imageSize != null) { |
| newRegion = (Offset.zero & imageSize).intersect(dragRegion!); |
| if (newRegion.size.width < 4 && newRegion.size.width < 4) { |
| newRegion = Offset.zero & imageSize; |
| } |
| } |
| |
| await _updatePaletteGenerator(newRegion); |
| setState(() { |
| region = newRegion; |
| dragRegion = null; |
| startDrag = null; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Scaffold( |
| backgroundColor: _kBackgroundColor, |
| appBar: AppBar( |
| title: Text(widget.title ?? ''), |
| ), |
| body: Column( |
| children: <Widget>[ |
| Padding( |
| padding: const EdgeInsets.all(20.0), |
| // GestureDetector is used to handle the selection rectangle. |
| child: GestureDetector( |
| onPanDown: _onPanDown, |
| onPanUpdate: _onPanUpdate, |
| onPanCancel: _onPanCancel, |
| onPanEnd: _onPanEnd, |
| child: Stack(children: <Widget>[ |
| Image( |
| key: imageKey, |
| image: widget.image, |
| width: widget.imageSize?.width, |
| height: widget.imageSize?.height, |
| ), |
| // This is the selection rectangle. |
| Positioned.fromRect( |
| rect: dragRegion ?? region ?? Rect.zero, |
| child: Container( |
| decoration: BoxDecoration( |
| color: _kSelectionRectangleBackground, |
| border: Border.all( |
| color: _kSelectionRectangleBorder, |
| )), |
| )), |
| ]), |
| ), |
| ), |
| // Use a FutureBuilder so that the palettes will be displayed when |
| // the palette generator is done generating its data. |
| PaletteSwatches(generator: paletteGenerator), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| /// A widget that draws the swatches for the [PaletteGenerator] it is given, |
| /// and shows the selected target colors. |
| class PaletteSwatches extends StatelessWidget { |
| /// Create a Palette swatch. |
| /// |
| /// The [generator] is optional. If it is null, then the display will |
| /// just be an empty container. |
| const PaletteSwatches({super.key, this.generator}); |
| |
| /// The [PaletteGenerator] that contains all of the swatches that we're going |
| /// to display. |
| final PaletteGenerator? generator; |
| |
| @override |
| Widget build(BuildContext context) { |
| final List<Widget> swatches = <Widget>[]; |
| final PaletteGenerator? paletteGen = generator; |
| if (paletteGen == null || paletteGen.colors.isEmpty) { |
| return Container(); |
| } |
| for (final Color color in paletteGen.colors) { |
| swatches.add(PaletteSwatch(color: color)); |
| } |
| return Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| Wrap( |
| children: swatches, |
| ), |
| Container(height: 30.0), |
| PaletteSwatch( |
| label: 'Dominant', color: paletteGen.dominantColor?.color), |
| PaletteSwatch( |
| label: 'Light Vibrant', color: paletteGen.lightVibrantColor?.color), |
| PaletteSwatch(label: 'Vibrant', color: paletteGen.vibrantColor?.color), |
| PaletteSwatch( |
| label: 'Dark Vibrant', color: paletteGen.darkVibrantColor?.color), |
| PaletteSwatch( |
| label: 'Light Muted', color: paletteGen.lightMutedColor?.color), |
| PaletteSwatch(label: 'Muted', color: paletteGen.mutedColor?.color), |
| PaletteSwatch( |
| label: 'Dark Muted', color: paletteGen.darkMutedColor?.color), |
| ], |
| ); |
| } |
| } |
| |
| /// A small square of color with an optional label. |
| @immutable |
| class PaletteSwatch extends StatelessWidget { |
| /// Creates a PaletteSwatch. |
| /// |
| /// If the [paletteColor] has property `isTargetColorFound` as `false`, |
| /// then the swatch will show a placeholder instead, to indicate |
| /// that there is no color. |
| const PaletteSwatch({ |
| super.key, |
| this.color, |
| this.label, |
| }); |
| |
| /// The color of the swatch. |
| final Color? color; |
| |
| /// The optional label to display next to the swatch. |
| final String? label; |
| |
| @override |
| Widget build(BuildContext context) { |
| // Compute the "distance" of the color swatch and the background color |
| // so that we can put a border around those color swatches that are too |
| // close to the background's saturation and lightness. We ignore hue for |
| // the comparison. |
| final HSLColor hslColor = HSLColor.fromColor(color ?? Colors.transparent); |
| final HSLColor backgroundAsHsl = HSLColor.fromColor(_kBackgroundColor); |
| final double colorDistance = math.sqrt( |
| math.pow(hslColor.saturation - backgroundAsHsl.saturation, 2.0) + |
| math.pow(hslColor.lightness - backgroundAsHsl.lightness, 2.0)); |
| |
| Widget swatch = Padding( |
| padding: const EdgeInsets.all(2.0), |
| child: color == null |
| ? const Placeholder( |
| fallbackWidth: 34.0, |
| fallbackHeight: 20.0, |
| color: Color(0xff404040), |
| ) |
| : Tooltip( |
| message: color!.toRGB(), |
| child: Container( |
| decoration: BoxDecoration( |
| color: color, |
| border: Border.all( |
| color: _kPlaceholderColor, |
| style: colorDistance < 0.2 |
| ? BorderStyle.solid |
| : BorderStyle.none, |
| )), |
| width: 34.0, |
| height: 20.0, |
| ), |
| ), |
| ); |
| |
| if (label != null) { |
| swatch = ConstrainedBox( |
| constraints: const BoxConstraints(maxWidth: 130.0, minWidth: 130.0), |
| child: Row( |
| children: <Widget>[ |
| swatch, |
| Container(width: 5.0), |
| Text(label!), |
| ], |
| ), |
| ); |
| } |
| return swatch; |
| } |
| } |
| |
| /// Converts a [Color] into a #RRGGBB string. |
| extension on Color { |
| String toRGB() { |
| // In the example all alphas are 255, so no need to show it. |
| return '#${red.toHex()}${green.toHex()}${blue.toHex()}'; |
| } |
| } |
| |
| /// Converts an [int] to a uppercase hexadecimal string of at least [minDigits] length. |
| extension on int { |
| String toHex([int minDigits = 2]) { |
| return toRadixString(16).toUpperCase().padLeft(minDigits, '0'); |
| } |
| } |