diff --git a/CHANGELOG.md b/CHANGELOG.md index 82c6171e..870d8741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Version 2.0.0 +- Introducing the "Sticker" editor for seamless loading of stickers and widgets directly into the editor. + ## Version 1.0.3 - Update README.md with improved preview image diff --git a/README.md b/README.md index 9b78cb93..bb2e7a07 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ The ProImageEditor is a Flutter widget designed for image editing within your ap - **[❓ Usage](#usage)** - [Open the editor in a new page](#open-the-editor-in-a-new-page) - [Show the editor inside of a widget](#show-the-editor-inside-of-a-widget) + - [Own stickers or widgets](#own-stickers-or-widgets) - [Highly configurable](#highly-configurable) - **[📚 Documentation](#documentation)** - **[🤝 Contributing](#contributing)** @@ -84,7 +85,7 @@ The ProImageEditor is a Flutter widget designed for image editing within your ap Emoji-Editor - + Sticker/ Widget Editor @@ -93,6 +94,7 @@ The ProImageEditor is a Flutter widget designed for image editing within your ap Emoji-Editor + Sticker-Widget-Editor @@ -123,13 +125,13 @@ The ProImageEditor is a Flutter widget designed for image editing within your ap - ✅ Selectable design mode between Material and Cupertino - ✅ Interactive layers - ✅ Hit detection for painted layers +- ✅ Loading of stickers or widgets in the editor #### Future Features -- ✨ Text-layer with an improved hit-box and ensure it's vertically centered on all devices - ✨ Improved layer movement and scaling functionality for desktop devices +- ✨ Text-layer with an improved hit-box and ensure it's vertically centered on all devices - ✨ Enhanced crop editor with improved performance (No dependencies on `image_editor` and `extended_image`) -- ✨ Stickers support ## Getting started @@ -257,6 +259,88 @@ Widget build(BuildContext context) { } ``` +#### Own stickers or widgets + +To display stickers or widgets in the ProImageEditor, you have the flexibility to customize and load your own content. The `buildStickers` method allows you to define your own logic for loading stickers, whether from a backend, assets, or local storage, and then push them into the editor. The example below demonstrates how to load images that can serve as stickers and then add them to the editor: + +```dart +ProImageEditor.network( + 'https://picsum.photos/id/156/2000', + onImageEditingComplete: (bytes) async { + Navigator.pop(context); + }, + configs: ProImageEditorConfigs( + stickerEditorConfigs: StickerEditorConfigs( + enabled: true, + buildStickers: (setLayer) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + color: const Color.fromARGB(255, 224, 239, 251), + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemCount: 21, + shrinkWrap: true, + itemBuilder: (context, index) { + Widget widget = ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image.network( + 'https://picsum.photos/id/${(index + 3) * 3}/2000', + width: 120, + height: 120, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + return AnimatedSwitcher( + layoutBuilder: (currentChild, previousChildren) { + return SizedBox( + width: 120, + height: 120, + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ), + ); + }, + duration: const Duration(milliseconds: 200), + child: loadingProgress == null + ? child + : Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + ), + ); + return GestureDetector( + onTap: () => setLayer(widget), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: widget, + ), + ); + }, + ), + ), + ); + }, + ), + ), +), +``` + #### Highly configurable Customize the image editor to suit your preferences. Of course, each class like `I18nTextEditor` includes more configuration options. @@ -291,6 +375,7 @@ return Scaffold( cropRotateEditor: I18nCropRotateEditor(), filterEditor: I18nFilterEditor(filters: I18nFilters()), emojiEditor: I18nEmojiEditor(), + stickerEditor: I18nStickerEditor(), // More translations... ), helperLines: const HelperLines( @@ -312,6 +397,7 @@ return Scaffold( cropRotateEditor: CropRotateEditorTheme(), filterEditor: FilterEditorTheme(), emojiEditor: EmojiEditorTheme(), + stickerEditor: StickerEditorTheme(), background: Color.fromARGB(255, 22, 22, 22), loadingDialogTextColor: Color(0xFFE1E1E1), uiOverlayStyle: SystemUiOverlayStyle( @@ -328,6 +414,7 @@ return Scaffold( cropRotateEditor: IconsCropRotateEditor(), filterEditor: IconsFilterEditor(), emojiEditor: IconsEmojiEditor(), + stickerEditor: IconsStickerEditor(), closeEditor: Icons.clear, doneIcon: Icons.done, applyChanges: Icons.done, @@ -341,6 +428,45 @@ return Scaffold( cropRotateEditorConfigs: const CropRotateEditorConfigs(), filterEditorConfigs: FilterEditorConfigs(), emojiEditorConfigs: const EmojiEditorConfigs(), + stickerEditorConfigs: StickerEditorConfigs( + enabled: true, + buildStickers: (setLayer) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + color: const Color.fromARGB(255, 224, 239, 251), + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemCount: 21, + shrinkWrap: true, + itemBuilder: (context, index) { + Widget widget = ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image.network( + 'https://picsum.photos/id/${(index + 3) * 3}/2000', + width: 120, + height: 120, + fit: BoxFit.cover, + ), + ); + return GestureDetector( + onTap: () => setLayer(widget), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: widget, + ), + ); + }, + ), + ), + ); + }, + ), designMode: ImageEditorDesignModeE.material, heroTag: 'hero', theme: ThemeData( @@ -399,37 +525,39 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. |---------------------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------| | `i18n` | Internationalization settings for the Image Editor. | `I18n()` | | `helperLines` | Configuration options for helper lines in the Image Editor. | `HelperLines()` | -| `customWidgets` | Custom widgets to be used in the Image Editor. | `ImageEditorCustomWidgets()` | +| `customWidgets` | Custom widgets to be used in the Image Editor. | `ImageEditorCustomWidgets()` | | `imageEditorTheme` | Theme settings for the Image Editor. | `ImageEditorTheme()` | -| `icons` | Icons to be used in the Image Editor. | `ImageEditorIcons()` | -| `paintEditorConfigs` | Configuration options for the Paint Editor. | `PaintEditorConfigs()` | -| `textEditorConfigs` | Configuration options for the Text Editor. | `TextEditorConfigs()` | -| `cropRotateEditorConfigs` | Configuration options for the Crop and Rotate Editor. | `CropRotateEditorConfigs()` | -| `filterEditorConfigs` | Configuration options for the Filter Editor. | `FilterEditorConfigs()` | -| `emojiEditorConfigs` | Configuration options for the Emoji Editor. | `EmojiEditorConfigs()` | -| `designMode` | The design mode for the Image Editor. | `ImageEditorDesignModeE.material` | -| `theme` | The theme to be used for the Image Editor. | `null` | -| `heroTag` | A unique hero tag for the Image Editor widget. | `'Pro-Image-Editor-Hero'` | +| `icons` | Icons to be used in the Image Editor. | `ImageEditorIcons()` | +| `paintEditorConfigs` | Configuration options for the Paint Editor. | `PaintEditorConfigs()` | +| `textEditorConfigs` | Configuration options for the Text Editor. | `TextEditorConfigs()` | +| `cropRotateEditorConfigs` | Configuration options for the Crop and Rotate Editor. | `CropRotateEditorConfigs()` | +| `filterEditorConfigs` | Configuration options for the Filter Editor. | `FilterEditorConfigs()` | +| `emojiEditorConfigs` | Configuration options for the Emoji Editor. | `EmojiEditorConfigs()` | +| `stickerEditorConfigs` | Configuration options for the Sticker Editor. | `StickerEditorConfigs()` | +| `designMode` | The design mode for the Image Editor. | `ImageEditorDesignModeE.material` | +| `theme` | The theme to be used for the Image Editor. | `null` | +| `heroTag` | A unique hero tag for the Image Editor widget. | `'Pro-Image-Editor-Hero'` | | `activePreferredOrientations` | The editor currently supports only 'portraitUp' orientation. After closing the editor, it will revert to your default settings. | `[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]` |
i18n - -| Property | Description | Default Value | -|----------------------|-------------------------------------------------------------|---------------------------| -| `paintEditor` | Translations and messages specific to the painting editor. | `I18nPaintingEditor()` | -| `textEditor` | Translations and messages specific to the text editor. | `I18nTextEditor()` | + +| Property | Description | Default Value | +|----------------------|---------------------------------------------------------------|---------------------------| +| `paintEditor` | Translations and messages specific to the painting editor. | `I18nPaintingEditor()` | +| `textEditor` | Translations and messages specific to the text editor. | `I18nTextEditor()` | | `cropRotateEditor` | Translations and messages specific to the crop and rotate editor. | `I18nCropRotateEditor()` | -| `filterEditor` | Translations and messages specific to the filter editor. | `I18nFilterEditor()` | -| `emojiEditor` | Translations and messages specific to the emoji editor. | `I18nEmojiEditor()` | -| `various` | Translations and messages for various parts of the editor. | `I18nVarious()` | +| `filterEditor` | Translations and messages specific to the filter editor. | `I18nFilterEditor()` | +| `emojiEditor` | Translations and messages specific to the emoji editor. | `I18nEmojiEditor()` | +| `stickerEditor` | Translations and messages specific to the sticker editor. | `I18nStickerEditor()` | +| `various` | Translations and messages for various parts of the editor. | `I18nVarious()` | | `cancel` | The text for the "Cancel" button. | `'Cancel'` | | `undo` | The text for the "Undo" action. | `'Undo'` | | `redo` | The text for the "Redo" action. | `'Redo'` | | `done` | The text for the "Done" action. | `'Done'` | | `remove` | The text for the "Remove" action. | `'Remove'` | -| `doneLoadingMsg` | Message displayed while changes are being applied. | `'Changes are being applied'` | +| `doneLoadingMsg` | Message displayed while changes are being applied. | `'Changes are being applied'` | #### `i18n paintEditor` @@ -497,6 +625,12 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. | `bottomNavigationBarText` | Text for the bottom navigation bar item that opens the Emoji Editor. | 'Emoji' | +#### `i18n stickerEditor` +| Property | Description | Default Value | +|---------------------------|------------------------------------------------------------------------|---------------| +| `bottomNavigationBarText` | Text for the bottom navigation bar item that opens the Sticker Editor. | 'Stickers' | + + #### `i18n various` | Property | Description | Default Value | @@ -545,6 +679,7 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. | `cropRotateEditor` | Theme for the crop & rotate editor. | `CropRotateEditorTheme()` | | `filterEditor` | Theme for the filter editor. | `FilterEditorTheme()` | | `emojiEditor` | Theme for the emoji editor. | `EmojiEditorTheme()` | +| `stickerEditor` | Theme for the sticker editor. | `StickerEditorTheme()` | | `helperLine` | Theme for helper lines in the image editor. | `HelperLineTheme()` | | `background` | Background color for the image editor. | `imageEditorBackgroundColor` | | `loadingDialogTextColor` | Text color for loading dialogs. | `imageEditorTextColor` | @@ -591,16 +726,21 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. #### Theme emojiEditor -| Property | Description | Default Value | -| ------------------------- | ----------------------------------------------------- | ------------------------------- | -| `background` | Background color of the emoji editor widget. | `imageEditorBackgroundColor` | +| Property | Description | Default Value | +| ------------------------- | ----------------------------------------------------- | -------------------------------- | +| `background` | Background color of the emoji editor widget. | `imageEditorBackgroundColor` | | `indicatorColor` | Color of the category indicator. | `imageEditorPrimaryColor` | -| `iconColorSelected` | Color of the category icon when selected. | `imageEditorPrimaryColor` | +| `iconColorSelected` | Color of the category icon when selected. | `imageEditorPrimaryColor` | | `iconColor` | Color of the category icons. | `Color(0xFF9E9E9E)` | | `skinToneDialogBgColor` | Background color of the skin tone dialog. | `Color(0xFF252728)` | | `skinToneIndicatorColor` | Color of the small triangle next to skin tone emojis. | `Color(0xFF9E9E9E)` | +#### Theme stickerEditor +| Property | Description | Default Value | +| ------------------------- | ----------------------------------------------------- | ------------------------------- | + + #### Theme helperLine | Property | Description | Default Value | | -------------------- | ------------------------------------------------------- | ---------------- | @@ -614,18 +754,19 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. | Property | Description | Default Value | | --------------------- | ---------------------------------------------------- | -------------------------- | -| `closeEditor` | The icon for closing the editor without saving. | `Icons.clear` | +| `closeEditor` | The icon for closing the editor without saving. | `Icons.clear` | | `doneIcon` | The icon for applying changes and closing the editor.| `Icons.done` | | `backButton` | The icon for the back button. | `Icons.arrow_back` | | `applyChanges` | The icon for applying changes in the editor. | `Icons.done` | | `undoAction` | The icon for undoing the last action. | `Icons.undo` | | `redoAction` | The icon for redoing the last undone action. | `Icons.redo` | | `removeElementZone` | The icon for removing an element/layer like an emoji.| `Icons.delete_outline_rounded` | -| `paintingEditor` | Customizable icons for the Painting Editor component.| `IconsPaintingEditor` | -| `textEditor` | Customizable icons for the Text Editor component. | `IconsTextEditor` | +| `paintingEditor` | Customizable icons for the Painting Editor component.| `IconsPaintingEditor` | +| `textEditor` | Customizable icons for the Text Editor component. | `IconsTextEditor` | | `cropRotateEditor` | Customizable icons for the Crop and Rotate Editor component.| `IconsCropRotateEditor` | -| `filterEditor` | Customizable icons for the Filter Editor component. | `IconsFilterEditor` | -| `emojiEditor` | Customizable icons for the Emoji Editor component. | `IconsEmojiEditor` | +| `filterEditor` | Customizable icons for the Filter Editor component. | `IconsFilterEditor` | +| `emojiEditor` | Customizable icons for the Emoji Editor component. | `IconsEmojiEditor` | +| `stickerEditor` | Customizable icons for the Sticker Editor component. | `IconsStickerEditor` | #### icons paintingEditor | Property | Description | Default Value | @@ -652,22 +793,29 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. #### icons cropRotateEditor -| Property | Description | Default Value | -| --------------- | ---------------------------- | ----------------------------------------- | -| `bottomNavBar` | Icon for bottom navigation bar| `Icons.crop_rotate_rounded` | +| Property | Description | Default Value | +| --------------- | ----------------------------- | ---------------------------------------- | +| `bottomNavBar` | Icon for bottom navigation bar| `Icons.crop_rotate_rounded` | | `rotate` | Icon for the rotate action | `Icons.rotate_90_degrees_ccw_outlined` | -| `aspectRatio` | Icon for the aspect ratio action | `Icons.crop` | +| `aspectRatio` | Icon for the aspect ratio action | `Icons.crop` | #### icons filterEditor -| Property | Description | Default Value | -| --------------- | ------------------------------ | ------------- | -| `bottomNavBar` | Icon for bottom navigation bar | `Icons.filter` | +| Property | Description | Default Value | +| --------------- | ------------------------------ | -------------- | +| `bottomNavBar` | Icon for bottom navigation bar | `Icons.filter` | #### icons emojiEditor | Property | Description | Default Value | | --------------- | ------------------------------------ | ----------------------------------- | -| `bottomNavBar` | Icon for bottom navigation bar | `Icons.sentiment_satisfied_alt_rounded` | +| `bottomNavBar` | Icon for bottom navigation bar | `Icons.sentiment_satisfied_alt_rounded` | + + +#### icons stickerEditor +| Property | Description | Default Value | +| --------------- | ------------------------------------ | ----------------------------------- | +| `bottomNavBar` | Icon for bottom navigation bar | `Icons.layers_outlined` | +
@@ -748,6 +896,17 @@ Creates a `ProImageEditor` widget for editing an image from a network URL. | `customSkinColorOverlayHorizontalOffset`| Customize skin color overlay horizontal offset, especially useful when EmojiPicker is not aligned to the left border of the screen. | `null` |
+
+ stickerEditorConfigs + +| Feature | Description | Default Value | +|-------------------|----------------------------------------------------------|---------------| +| `enabled` | Enables or disables the sticker editor. | `false` | +| `initWidth` | Sets the initial width of stickers in logical pixels. | `100` | +| `buildStickers` | A callback to build custom stickers in the editor. | | +
+ +
diff --git a/assets/sticker-editor.gif b/assets/sticker-editor.gif new file mode 100644 index 00000000..8b5ae558 Binary files /dev/null and b/assets/sticker-editor.gif differ diff --git a/example/lib/main.dart b/example/lib/main.dart index 4dc59473..18896b1c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -75,8 +75,7 @@ class _MyHomePageState extends State { if (!kIsWeb) ...[ OutlinedButton.icon( onPressed: () async { - FilePickerResult? result = - await FilePicker.platform.pickFiles( + FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, ); @@ -134,8 +133,7 @@ class _MyHomePageState extends State { various: I18nVarious( loadingDialogMsg: 'Please wait...', closeEditorWarningTitle: 'Close Image Editor?', - closeEditorWarningMessage: - 'Are you sure you want to close the Image Editor? Your changes will not be saved.', + closeEditorWarningMessage: 'Are you sure you want to close the Image Editor? Your changes will not be saved.', closeEditorWarningConfirmBtn: 'OK', closeEditorWarningCancelBtn: 'Cancel', ), @@ -177,8 +175,7 @@ class _MyHomePageState extends State { smallScreenMoreTooltip: 'More', ), filterEditor: I18nFilterEditor( - applyFilterDialogMsg: - 'Filter is being applied.', + applyFilterDialogMsg: 'Filter is being applied.', bottomNavigationBarText: 'Filter', back: 'Back', done: 'Done', @@ -231,6 +228,9 @@ class _MyHomePageState extends State { emojiEditor: I18nEmojiEditor( bottomNavigationBarText: 'Emoji', ), + stickerEditor: I18nStickerEditor( + bottomNavigationBarText: 'I18nStickerEditor', + ), cancel: 'Cancel', undo: 'Undo', redo: 'Redo', @@ -288,13 +288,13 @@ class _MyHomePageState extends State { skinToneDialogBgColor: Color(0xFF252728), skinToneIndicatorColor: Color(0xFF9E9E9E), ), + stickerEditor: StickerEditorTheme(), background: Color.fromARGB(255, 22, 22, 22), loadingDialogTextColor: Color(0xFFE1E1E1), uiOverlayStyle: SystemUiOverlayStyle( statusBarColor: Color(0x42000000), statusBarIconBrightness: Brightness.light, - systemNavigationBarIconBrightness: - Brightness.light, + systemNavigationBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.dark, systemNavigationBarColor: Color(0xFF000000), ), @@ -315,10 +315,8 @@ class _MyHomePageState extends State { textEditor: IconsTextEditor( bottomNavBar: Icons.text_fields, alignLeft: Icons.align_horizontal_left_rounded, - alignCenter: - Icons.align_horizontal_center_rounded, - alignRight: - Icons.align_horizontal_right_rounded, + alignCenter: Icons.align_horizontal_center_rounded, + alignRight: Icons.align_horizontal_right_rounded, backgroundMode: Icons.layers_rounded, ), cropRotateEditor: IconsCropRotateEditor( @@ -330,8 +328,10 @@ class _MyHomePageState extends State { bottomNavBar: Icons.filter, ), emojiEditor: IconsEmojiEditor( - bottomNavBar: - Icons.sentiment_satisfied_alt_rounded, + bottomNavBar: Icons.sentiment_satisfied_alt_rounded, + ), + stickerEditor: IconsStickerEditor( + bottomNavBar: Icons.layers_outlined, ), closeEditor: Icons.clear, doneIcon: Icons.done, @@ -364,11 +364,9 @@ class _MyHomePageState extends State { canToggleBackgroundMode: true, initFontSize: 24.0, initialTextAlign: TextAlign.center, - initialBackgroundColorMode: - LayerBackgroundColorModeE.backgroundAndColor, + initialBackgroundColorMode: LayerBackgroundColorModeE.backgroundAndColor, ), - cropRotateEditorConfigs: - const CropRotateEditorConfigs( + cropRotateEditorConfigs: const CropRotateEditorConfigs( enabled: true, canRotate: true, canChangeAspectRatio: true, @@ -384,8 +382,7 @@ class _MyHomePageState extends State { recentTabBehavior: RecentTabBehavior.RECENT, enableSkinTones: true, recentsLimit: 28, - textStyle: TextStyle( - fontFamilyFallback: ['Apple Color Emoji']), + textStyle: TextStyle(fontFamilyFallback: ['Apple Color Emoji']), checkPlatformCompatibility: true, emojiSet: null /* [ @@ -439,6 +436,92 @@ class _MyHomePageState extends State { icon: const Icon(Icons.public_outlined), label: const Text('Editor from network'), ), + const SizedBox(height: 30), + OutlinedButton.icon( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ProImageEditor.network( + 'https://picsum.photos/2000', + onImageEditingComplete: (bytes) async { + Navigator.pop(context); + }, + configs: ProImageEditorConfigs( + stickerEditorConfigs: StickerEditorConfigs( + enabled: true, + buildStickers: (setLayer) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + color: const Color.fromARGB(255, 224, 239, 251), + child: GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemCount: 21, + shrinkWrap: true, + itemBuilder: (context, index) { + Widget widget = ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image.network( + 'https://picsum.photos/id/${(index + 3) * 3}/2000', + width: 120, + height: 120, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + return AnimatedSwitcher( + layoutBuilder: (currentChild, previousChildren) { + return SizedBox( + width: 120, + height: 120, + child: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ), + ); + }, + duration: const Duration(milliseconds: 200), + child: loadingProgress == null + ? child + : Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + ), + ); + return GestureDetector( + onTap: () => setLayer(widget), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: widget, + ), + ); + }, + ), + ), + ); + }, + ), + ), + ), + ), + ); + }, + icon: const Icon(Icons.layers_outlined), + label: const Text('Editor with Stickers'), + ), ], ), ), diff --git a/lib/utils/pro_image_editor_configs.dart b/lib/models/editor_configs/pro_image_editor_configs.dart similarity index 85% rename from lib/utils/pro_image_editor_configs.dart rename to lib/models/editor_configs/pro_image_editor_configs.dart index 5501e296..61b858d9 100644 --- a/lib/utils/pro_image_editor_configs.dart +++ b/lib/models/editor_configs/pro_image_editor_configs.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import '../models/custom_widgets.dart'; -import '../models/editor_configs/crop_rotate_editor_configs.dart'; -import '../models/editor_configs/emoji_editor_configs.dart'; -import '../models/editor_configs/filter_editor_configs.dart'; -import '../models/editor_configs/paint_editor_configs.dart'; -import '../models/editor_configs/text_editor_configs.dart'; -import '../models/helper_lines.dart'; -import '../models/i18n/i18n.dart'; -import '../models/icons/icons.dart'; -import '../models/theme/theme.dart'; -import 'design_mode.dart'; +import 'package:pro_image_editor/models/editor_configs/sticker_editor_configs.dart'; + +import '../custom_widgets.dart'; +import 'crop_rotate_editor_configs.dart'; +import 'emoji_editor_configs.dart'; +import 'filter_editor_configs.dart'; +import 'paint_editor_configs.dart'; +import 'text_editor_configs.dart'; +import '../helper_lines.dart'; +import '../i18n/i18n.dart'; +import '../icons/icons.dart'; +import '../theme/theme.dart'; +import '../../utils/design_mode.dart'; /// A class representing configuration options for the Image Editor. class ProImageEditorConfigs { @@ -54,6 +55,9 @@ class ProImageEditorConfigs { /// Configuration options for the Emoji Editor. final EmojiEditorConfigs emojiEditorConfigs; + /// Configuration options for the Sticker Editor. + final StickerEditorConfigs? stickerEditorConfigs; + /// The design mode for the Image Editor. final ImageEditorDesignModeE designMode; @@ -71,6 +75,7 @@ class ProImageEditorConfigs { /// - The `cropRotateEditorConfigs` configures the Crop and Rotate Editor. By default, it uses an empty `CropRotateEditorConfigs` instance. /// - The `filterEditorConfigs` configures the Filter Editor. By default, it uses an empty `FilterEditorConfigs` instance. /// - The `emojiEditorConfigs` configures the Emoji Editor. By default, it uses an empty `EmojiEditorConfigs` instance. + /// - The `stickerEditorConfigs` configures the Sticker Editor. By default, it uses an empty `stickerEditorConfigs` instance. /// - The `designMode` specifies the design mode for the Image Editor. By default, it is `ImageEditorDesignMode.material`. const ProImageEditorConfigs({ this.theme, @@ -91,6 +96,7 @@ class ProImageEditorConfigs { this.cropRotateEditorConfigs = const CropRotateEditorConfigs(), this.filterEditorConfigs = const FilterEditorConfigs(), this.emojiEditorConfigs = const EmojiEditorConfigs(), + this.stickerEditorConfigs, this.designMode = ImageEditorDesignModeE.material, }); } diff --git a/lib/models/editor_configs/sticker_editor_configs.dart b/lib/models/editor_configs/sticker_editor_configs.dart new file mode 100644 index 00000000..60849b6b --- /dev/null +++ b/lib/models/editor_configs/sticker_editor_configs.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; + +/// Configuration options for a sticker editor. +/// +/// `StickerEditorConfigs` allows you to define various settings for a sticker +/// editor. You can configure features like enabling/disabling the editor, +/// initial sticker width, and a custom method to build stickers. +/// +/// Example usage: +/// ```dart +/// StickerEditorConfigs( +/// enabled: false, +/// initWidth: 150, +/// buildStickers: (setLayer) { +/// return Container(); // Replace with your builder to load and display stickers. +/// }, +/// ); +/// ``` +class StickerEditorConfigs { + /// Indicates whether the sticker editor is enabled. + /// + /// When set to `true`, the sticker editor is active and users can interact with it. + /// If `false`, the editor is disabled and does not respond to user inputs. + final bool enabled; + + /// The initial width of the stickers in the editor. + /// + /// Specifies the starting width of the stickers when they are first placed + /// in the editor. This value is in logical pixels. + final double initWidth; + + /// A callback that builds the stickers. + /// + /// This typedef is a function that takes a function as a parameter and + /// returns a Widget. The function parameter `setLayer` is used to set a + /// layer in the editor. This callback allows for customizing the appearance + /// and behavior of stickers in the editor. + final BuildStickers buildStickers; + + /// Creates an instance of StickerEditorConfigs with optional settings. + /// + /// By default, the editor is disabled (if not specified), and other properties + /// are set to reasonable defaults. + const StickerEditorConfigs({ + required this.buildStickers, + this.initWidth = 100, + this.enabled = false, + }); +} + +typedef BuildStickers = Widget Function(Function(Widget) setLayer); diff --git a/lib/models/i18n/i18n.dart b/lib/models/i18n/i18n.dart index 75834c14..cf5dda4d 100644 --- a/lib/models/i18n/i18n.dart +++ b/lib/models/i18n/i18n.dart @@ -2,6 +2,7 @@ import 'i18n_crop_rotate_editor.dart'; import 'i18n_emoji_editor.dart'; import 'i18n_filter_editor.dart'; import 'i18n_painting_editor.dart'; +import 'i18n_sticker_editor.dart'; import 'i18n_text_editor.dart'; import 'i18n_various.dart'; @@ -10,6 +11,7 @@ export 'i18n_text_editor.dart'; export 'i18n_painting_editor.dart'; export 'i18n_filter_editor.dart'; export 'i18n_emoji_editor.dart'; +export 'i18n_sticker_editor.dart'; export 'i18n_crop_rotate_editor.dart'; /// The `I18n` class provides internationalization settings for the image editor @@ -106,6 +108,9 @@ class I18n { /// Translations and messages specific to the emoji editor. final I18nEmojiEditor emojiEditor; + /// Translations and messages specific to the sticker editor. + final I18nStickerEditor stickerEditor; + /// Translations and messages specific to the crop and rotate editor. final I18nCropRotateEditor cropRotateEditor; @@ -167,6 +172,7 @@ class I18n { this.cropRotateEditor = const I18nCropRotateEditor(), this.filterEditor = const I18nFilterEditor(), this.emojiEditor = const I18nEmojiEditor(), + this.stickerEditor = const I18nStickerEditor(), this.various = const I18nVarious(), this.cancel = 'Cancel', this.undo = 'Undo', diff --git a/lib/models/i18n/i18n_sticker_editor.dart b/lib/models/i18n/i18n_sticker_editor.dart new file mode 100644 index 00000000..2bdd2a03 --- /dev/null +++ b/lib/models/i18n/i18n_sticker_editor.dart @@ -0,0 +1,21 @@ +/// Internationalization (i18n) settings for the I18nStickerEditor Editor component. +class I18nStickerEditor { + /// Text for the bottom navigation bar item that opens the I18nStickerEditor Editor. + final String bottomNavigationBarText; + + /// Creates an instance of [I18nStickerEditor] with customizable internationalization settings. + /// + /// You can provide translations and messages specifically for the I18nStickerEditor Editor + /// component of your application. + /// + /// Example: + /// + /// ```dart + /// I18nStickerEditor( + /// bottomNavigationBarText: 'I18nStickerEditor', + /// ) + /// ``` + const I18nStickerEditor({ + this.bottomNavigationBarText = 'Stickers', + }); +} diff --git a/lib/models/icons/icons.dart b/lib/models/icons/icons.dart index 164822de..6adafbb3 100644 --- a/lib/models/icons/icons.dart +++ b/lib/models/icons/icons.dart @@ -4,10 +4,12 @@ import 'icons_crop_rotate_editor.dart'; import 'icons_emoji_editor.dart'; import 'icons_filter_editor.dart'; import 'icons_painting_editor.dart'; +import 'icons_sticker_editor.dart'; import 'icons_text_editor.dart'; export 'icons_crop_rotate_editor.dart'; export 'icons_emoji_editor.dart'; +export 'icons_sticker_editor.dart'; export 'icons_filter_editor.dart'; export 'icons_painting_editor.dart'; export 'icons_text_editor.dart'; @@ -50,6 +52,9 @@ class ImageEditorIcons { /// Icons for the Emoji Editor component. final IconsEmojiEditor emojiEditor; + /// Icons for the Sticker Editor component. + final IconsStickerEditor stickerEditor; + /// Creates an instance of [ImageEditorIcons] with customizable icon settings. /// /// You can provide custom icons for various actions in the Image Editor component. @@ -66,6 +71,7 @@ class ImageEditorIcons { /// - [cropRotateEditor]: Customizable icons for the Crop and Rotate Editor component. /// - [filterEditor]: Customizable icons for the Filter Editor component. /// - [emojiEditor]: Customizable icons for the Emoji Editor component. + /// - [stickerEditor]: Customizable icons for the Sticker Editor component. /// /// If no custom icons are provided, default icons are used for each action. /// @@ -114,6 +120,7 @@ class ImageEditorIcons { this.cropRotateEditor = const IconsCropRotateEditor(), this.filterEditor = const IconsFilterEditor(), this.emojiEditor = const IconsEmojiEditor(), + this.stickerEditor = const IconsStickerEditor(), this.closeEditor = Icons.clear, this.doneIcon = Icons.done, this.applyChanges = Icons.done, diff --git a/lib/models/icons/icons_sticker_editor.dart b/lib/models/icons/icons_sticker_editor.dart new file mode 100644 index 00000000..cae7ae54 --- /dev/null +++ b/lib/models/icons/icons_sticker_editor.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +/// Customizable icons for the Sticker Editor component. +class IconsStickerEditor { + /// The icon to be displayed in the bottom navigation bar. + final IconData bottomNavBar; + + /// Creates an instance of [IconsStickerEditor] with customizable icon settings. + /// + /// You can provide a custom [bottomNavBar] icon to be displayed in the + /// bottom navigation bar of the Sticker Editor component. If no custom icon + /// is provided, the default icon is used. + /// + /// Example: + /// + /// ```dart + /// IconsStickerEditor( + /// bottomNavBar: Icons.layers_outlined, + /// ) + /// ``` + const IconsStickerEditor({ + this.bottomNavBar = Icons.layers_outlined, + }); +} diff --git a/lib/models/layer.dart b/lib/models/layer.dart index 1bd1e053..7bdebb7e 100644 --- a/lib/models/layer.dart +++ b/lib/models/layer.dart @@ -216,3 +216,30 @@ class PaintingLayerData extends Layer { /// Returns the size of the layer after applying the scaling factor. Size get size => Size(rawSize.width * scale, rawSize.height * scale); } + +class StickerLayerData extends Layer { + /// The sticker to display on the layer. + Widget sticker; + + /// Creates an instance of StickerLayerData. + /// + /// The [sticker] parameter is required, and other properties are optional. + StickerLayerData({ + required this.sticker, + Offset? offset, + double? opacity, + double? rotation, + double? scale, + String? id, + bool? flipX, + bool? flipY, + }) : super( + offset: offset, + opacity: opacity, + rotation: rotation, + scale: scale, + id: id, + flipX: flipX, + flipY: flipY, + ); +} diff --git a/lib/models/theme/theme.dart b/lib/models/theme/theme.dart index 7de1ff47..c127264b 100644 --- a/lib/models/theme/theme.dart +++ b/lib/models/theme/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/services.dart'; +import 'package:pro_image_editor/models/theme/theme_sticker_editor.dart'; import 'theme_crop_rotate_editor.dart'; import 'theme_emoji_editor.dart'; @@ -14,6 +15,7 @@ export 'theme_filter_editor.dart'; export 'theme_text_editor.dart'; export 'theme_crop_rotate_editor.dart'; export 'theme_helper_lines.dart'; +export 'theme_sticker_editor.dart'; /// The `ImageEditorTheme` class defines the overall theme for the image editor /// in your Flutter application. It includes themes for various editor components @@ -30,6 +32,7 @@ export 'theme_helper_lines.dart'; /// cropRotateEditor: CropRotateEditorTheme(), /// filterEditor: FilterEditorTheme(), /// emojiEditor: EmojiEditorTheme(), +/// stickerEditor: StickerEditorTheme(), /// ); /// ``` /// @@ -47,6 +50,8 @@ export 'theme_helper_lines.dart'; /// /// - `emojiEditor`: Theme for the emoji editor. /// +/// - `stickerEditor`: Theme for the sticker editor. +/// /// - `background`: Background color for the image editor. /// /// - `loadingDialogTextColor`: Text color for loading dialogs. @@ -86,6 +91,9 @@ class ImageEditorTheme { /// Theme for the emoji editor. final EmojiEditorTheme emojiEditor; + /// Theme for the sticker editor. + final StickerEditorTheme stickerEditor; + /// Background color for the image editor. final Color background; @@ -107,6 +115,7 @@ class ImageEditorTheme { this.cropRotateEditor = const CropRotateEditorTheme(), this.filterEditor = const FilterEditorTheme(), this.emojiEditor = const EmojiEditorTheme(), + this.stickerEditor = const StickerEditorTheme(), this.background = imageEditorBackgroundColor, this.loadingDialogTextColor = imageEditorTextColor, this.uiOverlayStyle = const SystemUiOverlayStyle( diff --git a/lib/models/theme/theme_sticker_editor.dart b/lib/models/theme/theme_sticker_editor.dart new file mode 100644 index 00000000..49ddc923 --- /dev/null +++ b/lib/models/theme/theme_sticker_editor.dart @@ -0,0 +1,23 @@ +/// The `StickerEditorTheme` class defines the theme for the sticker editor in the image editor. +/// It includes properties such as colors for the background, category indicator, category icons, and more. +/// +/// Usage: +/// +/// ```dart +/// StickerEditorTheme stickerEditorTheme = StickerEditorTheme( +/// ); +/// ``` +/// +/// Properties: +/// +/// Example Usage: +/// +/// ```dart +/// StickerEditorTheme stickerEditorTheme = StickerEditorTheme( +/// ); +/// +/// ``` +class StickerEditorTheme { + /// Creates an instance of the `StickerEditorTheme` class with the specified theme properties. + const StickerEditorTheme(); +} diff --git a/lib/modules/emoji_editor.dart b/lib/modules/emoji_editor.dart index 83a1b015..58284359 100644 --- a/lib/modules/emoji_editor.dart +++ b/lib/modules/emoji_editor.dart @@ -77,8 +77,7 @@ class EmojiEditorState extends State { } /// Builds a SizedBox containing the EmojiPicker with dynamic sizing. - Widget _buildEmojiPickerSizedBox( - BoxConstraints constraints, BuildContext context) { + Widget _buildEmojiPickerSizedBox(BoxConstraints constraints, BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), @@ -87,14 +86,12 @@ class EmojiEditorState extends State { child: SizedBox( height: max( 50, - min(320, constraints.maxHeight) - - MediaQuery.of(context).padding.bottom, + min(320, constraints.maxHeight) - MediaQuery.of(context).padding.bottom, ), child: EmojiPicker( key: const ValueKey('Emoji-Picker'), textEditingController: _controller, - onEmojiSelected: (category, emoji) => - {Navigator.pop(context, EmojiLayerData(emoji: emoji.emoji))}, + onEmojiSelected: (category, emoji) => {Navigator.pop(context, EmojiLayerData(emoji: emoji.emoji))}, config: _buildEmojiPickerConfig(constraints), ), ), @@ -106,10 +103,8 @@ class EmojiEditorState extends State { return Config( columns: _calculateColumns(constraints), emojiSizeMax: 32, - skinToneDialogBgColor: - widget.imageEditorTheme.emojiEditor.skinToneDialogBgColor, - skinToneIndicatorColor: - widget.imageEditorTheme.emojiEditor.skinToneIndicatorColor, + skinToneDialogBgColor: widget.imageEditorTheme.emojiEditor.skinToneDialogBgColor, + skinToneIndicatorColor: widget.imageEditorTheme.emojiEditor.skinToneIndicatorColor, bgColor: widget.imageEditorTheme.emojiEditor.background, indicatorColor: widget.imageEditorTheme.emojiEditor.indicatorColor, iconColorSelected: widget.imageEditorTheme.emojiEditor.iconColorSelected, @@ -127,12 +122,9 @@ class EmojiEditorState extends State { noRecents: const SizedBox.shrink(), tabIndicatorAnimDuration: kTabScrollDuration, categoryIcons: widget.configs.categoryIcons, - buttonMode: widget.designMode == ImageEditorDesignModeE.cupertino - ? ButtonMode.CUPERTINO - : ButtonMode.MATERIAL, + buttonMode: widget.designMode == ImageEditorDesignModeE.cupertino ? ButtonMode.CUPERTINO : ButtonMode.MATERIAL, checkPlatformCompatibility: widget.configs.checkPlatformCompatibility, - customSkinColorOverlayHorizontalOffset: - widget.configs.customSkinColorOverlayHorizontalOffset, + customSkinColorOverlayHorizontalOffset: widget.configs.customSkinColorOverlayHorizontalOffset, loadingIndicator: const Center( child: CircularProgressIndicator(), ), @@ -140,6 +132,5 @@ class EmojiEditorState extends State { } /// Calculates the number of columns for the EmojiPicker. - int _calculateColumns(BoxConstraints constraints) => - max(1, 10 / 400 * constraints.maxWidth - 1).floor(); + int _calculateColumns(BoxConstraints constraints) => max(1, 10 / 400 * constraints.maxWidth - 1).floor(); } diff --git a/lib/modules/paint_editor/paint_editor.dart b/lib/modules/paint_editor/paint_editor.dart index a28c53ac..8cabf2e5 100644 --- a/lib/modules/paint_editor/paint_editor.dart +++ b/lib/modules/paint_editor/paint_editor.dart @@ -72,6 +72,9 @@ class PaintingEditor extends StatefulWidget { /// The font size for text layers within the editor. final double layerFontSize; + /// The initial width of the stickers in the editor. + final double stickerInitWidth; + /// Custom emoji text style to apply to emoji characters in the grid. final TextStyle emojiTextStyle; @@ -95,12 +98,10 @@ class PaintingEditor extends StatefulWidget { this.emojiTextStyle = const TextStyle(), this.paddingHelper, this.layerFontSize = 24, + this.stickerInitWidth = 100, this.designMode = ImageEditorDesignModeE.material, }) : assert( - byteArray != null || - file != null || - networkUrl != null || - assetPath != null, + byteArray != null || file != null || networkUrl != null || assetPath != null, 'At least one of bytes, file, networkUrl, or assetPath must not be null.', ); @@ -119,6 +120,7 @@ class PaintingEditor extends StatefulWidget { List? layers, EdgeInsets? paddingHelper, double layerFontSize = 24.0, + double stickerInitWidth = 100.0, TextStyle emojiTextStyle = const TextStyle(), }) { return PaintingEditor._( @@ -127,6 +129,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -154,6 +157,7 @@ class PaintingEditor extends StatefulWidget { List? layers, EdgeInsets? paddingHelper, double layerFontSize = 24.0, + double stickerInitWidth = 100.0, TextStyle emojiTextStyle = const TextStyle(), }) { return PaintingEditor._( @@ -162,6 +166,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -187,6 +192,7 @@ class PaintingEditor extends StatefulWidget { List? layers, EdgeInsets? paddingHelper, double layerFontSize = 24.0, + double stickerInitWidth = 100.0, TextStyle emojiTextStyle = const TextStyle(), }) { return PaintingEditor._( @@ -195,6 +201,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -220,6 +227,7 @@ class PaintingEditor extends StatefulWidget { List? layers, EdgeInsets? paddingHelper, double layerFontSize = 24.0, + double stickerInitWidth = 100.0, TextStyle emojiTextStyle = const TextStyle(), }) { return PaintingEditor._( @@ -228,6 +236,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -256,6 +265,7 @@ class PaintingEditor extends StatefulWidget { List? layers, EdgeInsets? paddingHelper, double layerFontSize = 24.0, + double stickerInitWidth = 100.0, TextStyle emojiTextStyle = const TextStyle(), }) { if (byteArray != null) { @@ -265,6 +275,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -280,6 +291,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -295,6 +307,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -310,6 +323,7 @@ class PaintingEditor extends StatefulWidget { theme: theme, i18n: i18n, customWidgets: customWidgets, + stickerInitWidth: stickerInitWidth, icons: icons, designMode: designMode, imageEditorTheme: imageEditorTheme, @@ -319,8 +333,7 @@ class PaintingEditor extends StatefulWidget { configs: configs, ); } else { - throw ArgumentError( - "Either 'byteArray', 'file', 'networkUrl' or 'assetPath' must be provided."); + throw ArgumentError("Either 'byteArray', 'file', 'networkUrl' or 'assetPath' must be provided."); } } @@ -451,9 +464,7 @@ class PaintingEditorState extends State { return AnnotatedRegion( value: widget.imageEditorTheme.uiOverlayStyle, child: Theme( - data: widget.theme.copyWith( - tooltipTheme: - widget.theme.tooltipTheme.copyWith(preferBelow: true)), + data: widget.theme.copyWith(tooltipTheme: widget.theme.tooltipTheme.copyWith(preferBelow: true)), child: LayoutBuilder(builder: (context, constraints) { return Scaffold( resizeToAvoidBottomInset: false, @@ -475,10 +486,8 @@ class PaintingEditorState extends State { return widget.customWidgets.appBarPaintingEditor ?? AppBar( automaticallyImplyLeading: false, - backgroundColor: - widget.imageEditorTheme.paintingEditor.appBarBackgroundColor, - foregroundColor: - widget.imageEditorTheme.paintingEditor.appBarForegroundColor, + backgroundColor: widget.imageEditorTheme.paintingEditor.appBarBackgroundColor, + foregroundColor: widget.imageEditorTheme.paintingEditor.appBarForegroundColor, actions: [ IconButton( tooltip: widget.i18n.paintEditor.back, @@ -505,9 +514,7 @@ class PaintingEditorState extends State { tooltip: widget.i18n.paintEditor.toggleFill, padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( - !_fill - ? widget.icons.paintingEditor.noFill - : widget.icons.paintingEditor.fill, + !_fill ? widget.icons.paintingEditor.noFill : widget.icons.paintingEditor.fill, color: Colors.white, ), onPressed: () { @@ -521,9 +528,7 @@ class PaintingEditorState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( widget.icons.undoAction, - color: _imageKey.currentState!.canUndo - ? Colors.white - : Colors.white.withAlpha(80), + color: _imageKey.currentState!.canUndo ? Colors.white : Colors.white.withAlpha(80), ), onPressed: undoAction, ), @@ -532,9 +537,7 @@ class PaintingEditorState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( widget.icons.redoAction, - color: _imageKey.currentState!.canRedo - ? Colors.white - : Colors.white.withAlpha(80), + color: _imageKey.currentState!.canRedo ? Colors.white : Colors.white.withAlpha(80), ), onPressed: redoAction, ), @@ -558,15 +561,12 @@ class PaintingEditorState extends State { PopupMenuOption( label: widget.i18n.paintEditor.toggleFill, icon: Icon( - !_fill - ? widget.icons.paintingEditor.noFill - : widget.icons.paintingEditor.fill, + !_fill ? widget.icons.paintingEditor.noFill : widget.icons.paintingEditor.fill, ), onTap: () { _fill = !_fill; setFill(_fill); - if (widget.designMode == - ImageEditorDesignModeE.cupertino) { + if (widget.designMode == ImageEditorDesignModeE.cupertino) { Navigator.pop(context); } }, @@ -674,10 +674,8 @@ class PaintingEditorState extends State { builder: (_) { var item = paintModes[index]; var color = _imageKey.currentState?.mode == item.mode - ? widget.imageEditorTheme.paintingEditor - .bottomBarActiveItemColor - : widget.imageEditorTheme.paintingEditor - .bottomBarInactiveItemColor; + ? widget.imageEditorTheme.paintingEditor.bottomBarActiveItemColor + : widget.imageEditorTheme.paintingEditor.bottomBarInactiveItemColor; return FlatIconTextButton( label: Text( @@ -734,11 +732,7 @@ class PaintingEditorState extends State { child: BarColorPicker( length: min( 350, - MediaQuery.of(context).size.height - - MediaQuery.of(context).viewInsets.bottom - - kToolbarHeight - - MediaQuery.of(context).padding.top - - 30, + MediaQuery.of(context).size.height - MediaQuery.of(context).viewInsets.bottom - kToolbarHeight - MediaQuery.of(context).padding.top - 30, ), horizontal: false, thumbColor: Colors.white, @@ -766,6 +760,7 @@ class PaintingEditorState extends State { layerData: layerItem, textFontSize: widget.layerFontSize, emojiTextStyle: widget.emojiTextStyle, + stickerInitWidth: widget.stickerInitWidth, onTap: (layerData) async {}, onTapUp: () {}, onTapDown: () {}, diff --git a/lib/modules/sticker_editor.dart b/lib/modules/sticker_editor.dart new file mode 100644 index 00000000..43062d18 --- /dev/null +++ b/lib/modules/sticker_editor.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../models/editor_configs/sticker_editor_configs.dart'; +import '../models/theme/theme.dart'; +import '../models/i18n/i18n.dart'; +import '../models/layer.dart'; +import '../utils/design_mode.dart'; + +/// The `StickerEditor` class is responsible for creating a widget that allows users to select emojis. +/// +/// This widget provides an EmojiPicker that allows users to choose emojis, which are then returned +/// as `EmojiLayerData` containing the selected emoji text. +class StickerEditor extends StatefulWidget { + /// The internationalization (i18n) configuration for the editor. + final I18n i18n; + + /// The design mode of the editor. + final ImageEditorDesignModeE designMode; + + /// The theme configuration specific to the image editor. + final ImageEditorTheme imageEditorTheme; + + /// The configuration for the EmojiPicker. + /// + /// This parameter allows you to customize the behavior and appearance of the EmojiPicker. + final StickerEditorConfigs configs; + + /// Creates an `StickerEditor` widget. + /// + /// The [i18n] parameter is used for internationalization. + /// + /// The [designMode] parameter specifies the design mode of the editor. + /// + /// The [imageEditorTheme] parameter is the theme configuration specific to the image editor. + const StickerEditor({ + super.key, + required this.configs, + this.i18n = const I18n(), + this.imageEditorTheme = const ImageEditorTheme(), + this.designMode = ImageEditorDesignModeE.material, + }); + + @override + createState() => StickerEditorState(); +} + +/// The state class for the `StickerEditor` widget. +class StickerEditorState extends State { + /// Closes the editor without applying changes. + void close() { + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return widget.configs.buildStickers(setLayer); + } + + void setLayer(Widget sticker) { + Navigator.of(context).pop( + StickerLayerData(sticker: sticker), + ); + } +} diff --git a/lib/pro_image_editor.dart b/lib/pro_image_editor.dart index 81c37458..4bc4a1e7 100644 --- a/lib/pro_image_editor.dart +++ b/lib/pro_image_editor.dart @@ -9,20 +9,18 @@ export 'package:pro_image_editor/models/icons/icons.dart'; export 'package:pro_image_editor/models/theme/theme.dart'; export 'package:pro_image_editor/models/helper_lines.dart'; export 'package:pro_image_editor/models/custom_widgets.dart'; -export 'package:pro_image_editor/utils/pro_image_editor_configs.dart'; +export 'package:pro_image_editor/models/editor_configs/pro_image_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/paint_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/text_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/crop_rotate_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/filter_editor_configs.dart'; export 'package:pro_image_editor/models/editor_configs/emoji_editor_configs.dart'; +export 'package:pro_image_editor/models/editor_configs/sticker_editor_configs.dart'; export 'package:pro_image_editor/utils/design_mode.dart'; export 'package:pro_image_editor/modules/paint_editor/utils/paint_editor_enum.dart'; -export 'package:pro_image_editor/widgets/layer_widget.dart' - show LayerBackgroundColorModeE; +export 'package:pro_image_editor/widgets/layer_widget.dart' show LayerBackgroundColorModeE; export 'package:extended_image/extended_image.dart' show CropAspectRatios; -export 'package:emoji_picker_flutter/emoji_picker_flutter.dart' - show Emoji, RecentTabBehavior, CategoryIcons, Category, CategoryEmoji; -export 'package:colorfilter_generator/presets.dart' - show presetFiltersList, PresetFilters; +export 'package:emoji_picker_flutter/emoji_picker_flutter.dart' show Emoji, RecentTabBehavior, CategoryIcons, Category, CategoryEmoji; +export 'package:colorfilter_generator/presets.dart' show presetFiltersList, PresetFilters; diff --git a/lib/pro_image_editor_main.dart b/lib/pro_image_editor_main.dart index 187ddef0..0fb4970f 100644 --- a/lib/pro_image_editor_main.dart +++ b/lib/pro_image_editor_main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:pro_image_editor/modules/sticker_editor.dart'; import 'package:screenshot/screenshot.dart'; import 'package:vibration/vibration.dart'; @@ -20,7 +21,7 @@ import 'modules/filter_editor.dart'; import 'modules/paint_editor/paint_editor.dart'; import 'modules/text_editor.dart'; import 'utils/debounce.dart'; -import 'utils/pro_image_editor_configs.dart'; +import 'models/editor_configs/pro_image_editor_configs.dart'; import 'widgets/adaptive_dialog.dart'; import 'widgets/auto_image.dart'; import 'widgets/flat_icon_text_button.dart'; @@ -98,10 +99,7 @@ class ProImageEditor extends StatefulWidget { this.file, this.configs = const ProImageEditorConfigs(), }) : assert( - byteArray != null || - file != null || - networkUrl != null || - assetPath != null, + byteArray != null || file != null || networkUrl != null || assetPath != null, 'At least one of bytes, file, networkUrl, or assetPath must not be null.', ); @@ -347,19 +345,13 @@ class ProImageEditorState extends State { double _imageHeight = 0; /// Getter for the screen inner height, excluding top and bottom padding. - double get _screenInnerHeight => - _screen.height - - _screenPadding.top - - _screenPadding.bottom - - kToolbarHeight * 2; + double get _screenInnerHeight => _screen.height - _screenPadding.top - _screenPadding.bottom - kToolbarHeight * 2; /// Getter for the X-coordinate of the middle of the screen. - double get _screenMiddleX => - _screen.width / 2 - (_screenPadding.left + _screenPadding.right) / 2; + double get _screenMiddleX => _screen.width / 2 - (_screenPadding.left + _screenPadding.right) / 2; /// Getter for the Y-coordinate of the middle of the screen. - double get _screenMiddleY => - _screen.height / 2 - (_screenPadding.top + _screenPadding.bottom) / 2; + double get _screenMiddleY => _screen.height / 2 - (_screenPadding.top + _screenPadding.bottom) / 2; /// Last recorded X-axis position for layers. LayerLastPosition _lastPositionX = LayerLastPosition.center; @@ -399,8 +391,7 @@ class ProImageEditorState extends State { _changes.add(ImageEditorChanges(bytesRefIndex: 0, layers: [])); Vibration.hasVibrator().then((value) => _deviceCanVibrate = value ?? false); - Vibration.hasCustomVibrationsSupport() - .then((value) => _deviceCanCustomVibrate = value ?? false); + Vibration.hasCustomVibrationsSupport().then((value) => _deviceCanCustomVibrate = value ?? false); ServicesBinding.instance.keyboard.addHandler(_onKey); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); @@ -416,11 +407,8 @@ class ProImageEditorState extends State { _bottomBarScrollCtrl.dispose(); _scaleDebounce.dispose(); _screenSizeDebouncer.dispose(); - SystemChrome.setPreferredOrientations( - widget.configs.activePreferredOrientations); - SystemChrome.setSystemUIOverlayStyle(_theme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark); + SystemChrome.setPreferredOrientations(widget.configs.activePreferredOrientations); + SystemChrome.setSystemUIOverlayStyle(_theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark); SystemChrome.restoreSystemUIOverlays(); ServicesBinding.instance.keyboard.removeHandler(_onKey); if (kIsWeb && _browserContextMenuBeforeEnabled) { @@ -526,15 +514,14 @@ class ProImageEditorState extends State { /// Add a new layer to the image editor. /// /// This method adds a new layer to the image editor and updates the editing state. - void _addLayer(Layer layer, {int removeLayerIndex = -1, EditorImage? image}) { + void addLayer(Layer layer, {int removeLayerIndex = -1, EditorImage? image}) { _cleanForwardChanges(); if (image != null) _changeList.add(image); _changes.add( ImageEditorChanges( bytesRefIndex: _changeList.length - 1, - layers: List.from(_changes.last.layers.map((e) => _copyLayer(e))) - ..add(layer), + layers: List.from(_changes.last.layers.map((e) => _copyLayer(e)))..add(layer), ), ); _editPosition++; @@ -554,8 +541,7 @@ class ProImageEditorState extends State { layers: List.from(_changes.last.layers.map((e) => _copyLayer(e))), ), ); - var oldIndex = - _layers.indexWhere((element) => element.id == _tempLayer!.id); + var oldIndex = _layers.indexWhere((element) => element.id == _tempLayer!.id); if (oldIndex >= 0) { _changes[_editPosition].layers[oldIndex] = _copyLayer(_tempLayer!); } @@ -576,11 +562,9 @@ class ProImageEditorState extends State { layers: layers, ), ); - var oldIndex = _layers - .indexWhere((element) => element.id == (layer?.id ?? _tempLayer!.id)); + var oldIndex = _layers.indexWhere((element) => element.id == (layer?.id ?? _tempLayer!.id)); if (oldIndex >= 0) { - _changes[_editPosition].layers[oldIndex] = - _copyLayer(layer ?? _tempLayer!); + _changes[_editPosition].layers[oldIndex] = _copyLayer(layer ?? _tempLayer!); } _editPosition++; } @@ -640,6 +624,8 @@ class ProImageEditorState extends State { return _createCopyEmojiLayer(layer); } else if (layer is PaintingLayerData) { return _createCopyPaintingLayer(layer); + } else if (layer is StickerLayerData) { + return _createCopyStickerLayer(layer); } else { return layer; } @@ -678,6 +664,20 @@ class ProImageEditorState extends State { ); } + /// Create a copy of an EmojiLayerData instance. + StickerLayerData _createCopyStickerLayer(StickerLayerData layer) { + return StickerLayerData( + id: layer.id, + sticker: layer.sticker, + offset: Offset(layer.offset.dx, layer.offset.dy), + opacity: layer.opacity, + rotation: layer.rotation, + scale: layer.scale, + flipX: layer.flipX, + flipY: layer.flipY, + ); + } + /// Create a copy of a PaintingLayerData instance. PaintingLayerData _createCopyPaintingLayer(PaintingLayerData layer) { return PaintingLayerData( @@ -770,81 +770,59 @@ class ProImageEditorState extends State { _enabledHitDetection = false; if (detail.pointerCount == 1) { if (_activeScale) return; - _freeStyleHighPerformanceMoving = - widget.configs.paintEditorConfigs.freeStyleHighPerformanceMoving ?? - isWebMobile; + _freeStyleHighPerformanceMoving = widget.configs.paintEditorConfigs.freeStyleHighPerformanceMoving ?? isWebMobile; _activeLayer.offset = Offset( _activeLayer.offset.dx + detail.focalPointDelta.dx, _activeLayer.offset.dy + detail.focalPointDelta.dy, ); - hoverRemoveBtn = detail.focalPoint.dx <= kToolbarHeight && - detail.focalPoint.dy <= - kToolbarHeight + MediaQuery.of(context).viewPadding.top; + hoverRemoveBtn = detail.focalPoint.dx <= kToolbarHeight && detail.focalPoint.dy <= kToolbarHeight + MediaQuery.of(context).viewPadding.top; bool vibarate = false; double posX = _activeLayer.offset.dx + screenPaddingHelper.left; double posY = _activeLayer.offset.dy + screenPaddingHelper.top; - bool hitAreaX = detail.focalPoint.dx >= _snapStartPosX - _hitSpan && - detail.focalPoint.dx <= _snapStartPosX + _hitSpan; - bool hitAreaY = detail.focalPoint.dy >= _snapStartPosY - _hitSpan && - detail.focalPoint.dy <= _snapStartPosY + _hitSpan; + bool hitAreaX = detail.focalPoint.dx >= _snapStartPosX - _hitSpan && detail.focalPoint.dx <= _snapStartPosX + _hitSpan; + bool hitAreaY = detail.focalPoint.dy >= _snapStartPosY - _hitSpan && detail.focalPoint.dy <= _snapStartPosY + _hitSpan; - bool helperGoNearLineLeft = - posX >= _screenMiddleX && _lastPositionX == LayerLastPosition.left; - bool helperGoNearLineRight = - posX <= _screenMiddleX && _lastPositionX == LayerLastPosition.right; - bool helperGoNearLineTop = - posY >= _screenMiddleY && _lastPositionY == LayerLastPosition.top; - bool helperGoNearLineBottom = - posY <= _screenMiddleY && _lastPositionY == LayerLastPosition.bottom; + bool helperGoNearLineLeft = posX >= _screenMiddleX && _lastPositionX == LayerLastPosition.left; + bool helperGoNearLineRight = posX <= _screenMiddleX && _lastPositionX == LayerLastPosition.right; + bool helperGoNearLineTop = posY >= _screenMiddleY && _lastPositionY == LayerLastPosition.top; + bool helperGoNearLineBottom = posY <= _screenMiddleY && _lastPositionY == LayerLastPosition.bottom; /// Calc vertical helper line - if ((!_showVerticalHelperLine && - (helperGoNearLineLeft || helperGoNearLineRight)) || - (_showVerticalHelperLine && hitAreaX)) { + if ((!_showVerticalHelperLine && (helperGoNearLineLeft || helperGoNearLineRight)) || (_showVerticalHelperLine && hitAreaX)) { if (!_showVerticalHelperLine) { vibarate = true; _snapStartPosX = detail.focalPoint.dx; } _showVerticalHelperLine = true; - _activeLayer.offset = Offset( - _screenMiddleX - screenPaddingHelper.left, _activeLayer.offset.dy); + _activeLayer.offset = Offset(_screenMiddleX - screenPaddingHelper.left, _activeLayer.offset.dy); _lastPositionX = LayerLastPosition.center; } else { _showVerticalHelperLine = false; - _lastPositionX = posX <= _screenMiddleX - ? LayerLastPosition.left - : LayerLastPosition.right; + _lastPositionX = posX <= _screenMiddleX ? LayerLastPosition.left : LayerLastPosition.right; } /// Calc horizontal helper line - if ((!_showHorizontalHelperLine && - (helperGoNearLineTop || helperGoNearLineBottom)) || - (_showHorizontalHelperLine && hitAreaY)) { + if ((!_showHorizontalHelperLine && (helperGoNearLineTop || helperGoNearLineBottom)) || (_showHorizontalHelperLine && hitAreaY)) { if (!_showHorizontalHelperLine) { vibarate = true; _snapStartPosY = detail.focalPoint.dy; } _showHorizontalHelperLine = true; - _activeLayer.offset = Offset( - _activeLayer.offset.dx, _screenMiddleY - screenPaddingHelper.top); + _activeLayer.offset = Offset(_activeLayer.offset.dx, _screenMiddleY - screenPaddingHelper.top); _lastPositionY = LayerLastPosition.center; } else { _showHorizontalHelperLine = false; - _lastPositionY = posY <= _screenMiddleY - ? LayerLastPosition.top - : LayerLastPosition.bottom; + _lastPositionY = posY <= _screenMiddleY ? LayerLastPosition.top : LayerLastPosition.bottom; } if (vibarate) { _lineHitVibrate(); } } else if (detail.pointerCount == 2) { - _freeStyleHighPerformanceScaling = - widget.configs.paintEditorConfigs.freeStyleHighPerformanceScaling ?? - !isDesktop; + _freeStyleHighPerformanceScaling = widget.configs.paintEditorConfigs.freeStyleHighPerformanceScaling ?? !isDesktop; _activeScale = true; _activeLayer.scale = _baseScaleFactor * detail.scale; @@ -860,15 +838,10 @@ class ProImageEditorState extends State { if ((!_showRotationHelperLine && ((degHit > 0 && degHit <= hitSpanX && _snapLastRotation < deg) || - (degHit < 45 && - degHit >= 45 - hitSpanX && - _snapLastRotation > deg))) || + (degHit < 45 && degHit >= 45 - hitSpanX && _snapLastRotation > deg))) || (_showRotationHelperLine && hitArea)) { if (_rotationStartedHelper) { - _activeLayer.rotation = - (deg - (degHit > 45 - hitSpanX ? degHit - 45 : degHit)) / - 180 * - pi; + _activeLayer.rotation = (deg - (degHit > 45 - hitSpanX ? degHit - 45 : degHit)) / 180 * pi; _rotationHelperLineDeg = _activeLayer.rotation; double posY = _activeLayer.offset.dy + screenPaddingHelper.top; @@ -1088,18 +1061,10 @@ class ProImageEditorState extends State { customWidgets: widget.configs.customWidgets, layerFontSize: widget.configs.textEditorConfigs.initFontSize, configs: widget.configs.paintEditorConfigs, + stickerInitWidth: widget.configs.stickerEditorConfigs?.initWidth ?? 100, paddingHelper: EdgeInsets.only( - top: (_screen.height - - _screenPadding.top - - _screenPadding.bottom - - _imageHeight) / - 2 - - kToolbarHeight, - left: (_screen.width - - _screenPadding.left - - _screenPadding.right - - _imageWidth) / - 2, + top: (_screen.height - _screenPadding.top - _screenPadding.bottom - _imageHeight) / 2 - kToolbarHeight, + left: (_screen.width - _screenPadding.left - _screenPadding.right - _imageWidth) / 2, ), designMode: widget.configs.designMode, emojiTextStyle: widget.configs.emojiEditorConfigs.textStyle, @@ -1108,7 +1073,7 @@ class ProImageEditorState extends State { ).then((List? paintingLayers) { if (paintingLayers != null && paintingLayers.isNotEmpty) { for (var layer in paintingLayers) { - _addLayer(layer); + addLayer(layer); } setState(() {}); @@ -1142,7 +1107,7 @@ class ProImageEditorState extends State { _imageHeight / 2, ); - _addLayer(layer); + addLayer(layer); setState(() {}); } @@ -1221,10 +1186,8 @@ class ProImageEditorState extends State { double fitFactor = 1; - bool oldFitWidth = _imageWidth >= _screen.width - 0.1 && - _imageWidth <= _screen.width + 0.1; - bool newFitWidth = - newImgW >= _screen.width - 0.1 && newImgW <= _screen.width + 0.1; + bool oldFitWidth = _imageWidth >= _screen.width - 0.1 && _imageWidth <= _screen.width + 0.1; + bool newFitWidth = newImgW >= _screen.width - 0.1 && newImgW <= _screen.width + 0.1; var scaleX = newFitWidth ? oldFullW / w : oldFullH / h; if (oldFitWidth != newFitWidth) { @@ -1354,7 +1317,32 @@ class ProImageEditorState extends State { _imageHeight / 2, ); - _addLayer(layer); + addLayer(layer); + + setState(() {}); + } + + /// Opens the sticker editor as a modal bottom sheet. + void openStickerEditor() async { + ServicesBinding.instance.keyboard.removeHandler(_onKey); + StickerLayerData? layer = await showModalBottomSheet( + context: context, + backgroundColor: Colors.black, + builder: (BuildContext context) => StickerEditor( + i18n: widget.configs.i18n, + imageEditorTheme: widget.configs.imageEditorTheme, + designMode: widget.configs.designMode, + configs: widget.configs.stickerEditorConfigs!, + ), + ); + ServicesBinding.instance.keyboard.addHandler(_onKey); + if (layer == null || !mounted) return; + layer.offset = Offset( + _imageWidth / 2, + _imageHeight / 2, + ); + + addLayer(layer); setState(() {}); } @@ -1444,8 +1432,7 @@ class ProImageEditorState extends State { AdaptiveDialogAction( designMode: widget.configs.designMode, onPressed: () => Navigator.pop(context, 'Cancel'), - child: - Text(widget.configs.i18n.various.closeEditorWarningCancelBtn), + child: Text(widget.configs.i18n.various.closeEditorWarningCancelBtn), ), AdaptiveDialogAction( designMode: widget.configs.designMode, @@ -1454,8 +1441,7 @@ class ProImageEditorState extends State { Navigator.pop(context, 'OK'); Navigator.pop(context); }, - child: - Text(widget.configs.i18n.various.closeEditorWarningConfirmBtn), + child: Text(widget.configs.i18n.various.closeEditorWarningConfirmBtn), ), ], ), @@ -1491,10 +1477,8 @@ class ProImageEditorState extends State { /// Handles mouse scroll events. void _mouseScroll(PointerSignalEvent event) { - bool shiftDown = RawKeyboard.instance.keysPressed - .contains(LogicalKeyboardKey.shiftLeft) || - RawKeyboard.instance.keysPressed - .contains(LogicalKeyboardKey.shiftRight); + bool shiftDown = RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft) || + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftRight); if (event is PointerScrollEvent && _selectedLayer >= 0) { if (shiftDown) { @@ -1522,16 +1506,8 @@ class ProImageEditorState extends State { /// Get the screen padding values. EdgeInsets get screenPaddingHelper => EdgeInsets.only( - top: (_screen.height - - _screenPadding.top - - _screenPadding.bottom - - _imageHeight) / - 2, - left: (_screen.width - - _screenPadding.left - - _screenPadding.right - - _imageWidth) / - 2, + top: (_screen.height - _screenPadding.top - _screenPadding.bottom - _imageHeight) / 2, + left: (_screen.width - _screenPadding.left - _screenPadding.right - _imageWidth) / 2, ); @override @@ -1546,7 +1522,8 @@ class ProImageEditorState extends State { ); if (_imageNeedDecode) _decodeImage(); return PopScope( - /* canPop: _editPosition <= 0, + /* TODO: write logic for mobile devices + canPop: _editPosition <= 0, onPopInvoked: (didPop) { if (_editPosition > 0) { showAdaptiveDialog( @@ -1572,8 +1549,7 @@ class ProImageEditorState extends State { }, */ child: LayoutBuilder(builder: (context, constraints) { // Check if screensize changed to recalculate image size - if (_lastScreenSize.width != constraints.maxWidth || - _lastScreenSize.height != constraints.maxHeight) { + if (_lastScreenSize.width != constraints.maxWidth || _lastScreenSize.height != constraints.maxHeight) { _screenSizeDebouncer(() { _decodeImage(); }); @@ -1592,9 +1568,7 @@ class ProImageEditorState extends State { resizeToAvoidBottomInset: false, appBar: _buildAppBar(), body: _buildBody(), - bottomNavigationBar: - widget.configs.customWidgets.bottomNavigationBar ?? - _buildBottomNavBar(), + bottomNavigationBar: widget.configs.customWidgets.bottomNavigationBar ?? _buildBottomNavBar(), ), ), ), @@ -1625,9 +1599,7 @@ class ProImageEditorState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( widget.configs.icons.undoAction, - color: _editPosition > 0 - ? Colors.white - : Colors.white.withAlpha(80), + color: _editPosition > 0 ? Colors.white : Colors.white.withAlpha(80), ), onPressed: undoAction, ), @@ -1637,9 +1609,7 @@ class ProImageEditorState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), icon: Icon( widget.configs.icons.redoAction, - color: _editPosition < _changes.length - 1 - ? Colors.white - : Colors.white.withAlpha(80), + color: _editPosition < _changes.length - 1 ? Colors.white : Colors.white.withAlpha(80), ), onPressed: redoAction, ), @@ -1686,16 +1656,10 @@ class ProImageEditorState extends State { builder: (context, snapshot) { return MouseRegion( hitTestBehavior: HitTestBehavior.translucent, - cursor: snapshot.data != true - ? SystemMouseCursors.basic - : widget - .configs.imageEditorTheme.layerHoverCursor, + cursor: snapshot.data != true ? SystemMouseCursors.basic : widget.configs.imageEditorTheme.layerHoverCursor, onHover: isDesktop ? (event) { - var hasHit = _layers.indexWhere((element) => - element is PaintingLayerData && - element.item.hit) >= - 0; + var hasHit = _layers.indexWhere((element) => element is PaintingLayerData && element.item.hit) >= 0; if (hasHit != snapshot.data) { _mouseMoveStream.add(hasHit); } @@ -1758,23 +1722,17 @@ class ProImageEditorState extends State { maxWidth: 500, ), child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ if (widget.configs.paintEditorConfigs.enabled) FlatIconTextButton( - key: const ValueKey( - 'open-painting-editor-btn'), - label: Text( - widget.configs.i18n.paintEditor - .bottomNavigationBarText, - style: bottomTextStyle), + key: const ValueKey('open-painting-editor-btn'), + label: Text(widget.configs.i18n.paintEditor.bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.paintingEditor - .bottomNavBar, + widget.configs.icons.paintingEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), @@ -1783,30 +1741,20 @@ class ProImageEditorState extends State { if (widget.configs.textEditorConfigs.enabled) FlatIconTextButton( key: const ValueKey('open-text-editor-btn'), - label: Text( - widget.configs.i18n.textEditor - .bottomNavigationBarText, - style: bottomTextStyle), + label: Text(widget.configs.i18n.textEditor.bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.textEditor - .bottomNavBar, + widget.configs.icons.textEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openTextEditor, ), - if (widget - .configs.cropRotateEditorConfigs.enabled) + if (widget.configs.cropRotateEditorConfigs.enabled) FlatIconTextButton( - key: const ValueKey( - 'open-crop-rotate-editor-btn'), - label: Text( - widget.configs.i18n.cropRotateEditor - .bottomNavigationBarText, - style: bottomTextStyle), + key: const ValueKey('open-crop-rotate-editor-btn'), + label: Text(widget.configs.i18n.cropRotateEditor.bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.cropRotateEditor - .bottomNavBar, + widget.configs.icons.cropRotateEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), @@ -1814,15 +1762,10 @@ class ProImageEditorState extends State { ), if (widget.configs.filterEditorConfigs.enabled) FlatIconTextButton( - key: const ValueKey( - 'open-filter-editor-btn'), - label: Text( - widget.configs.i18n.filterEditor - .bottomNavigationBarText, - style: bottomTextStyle), + key: const ValueKey('open-filter-editor-btn'), + label: Text(widget.configs.i18n.filterEditor.bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.filterEditor - .bottomNavBar, + widget.configs.icons.filterEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), @@ -1830,20 +1773,26 @@ class ProImageEditorState extends State { ), if (widget.configs.emojiEditorConfigs.enabled) FlatIconTextButton( - key: - const ValueKey('open-emoji-editor-btn'), - label: Text( - widget.configs.i18n.emojiEditor - .bottomNavigationBarText, - style: bottomTextStyle), + key: const ValueKey('open-emoji-editor-btn'), + label: Text(widget.configs.i18n.emojiEditor.bottomNavigationBarText, style: bottomTextStyle), icon: Icon( - widget.configs.icons.emojiEditor - .bottomNavBar, + widget.configs.icons.emojiEditor.bottomNavBar, size: bottomIconSize, color: Colors.white, ), onPressed: openEmojiEditor, ), + if (widget.configs.stickerEditorConfigs?.enabled == true) + FlatIconTextButton( + key: const ValueKey('open-sticker-editor-btn'), + label: Text(widget.configs.i18n.stickerEditor.bottomNavigationBarText, style: bottomTextStyle), + icon: Icon( + widget.configs.icons.stickerEditor.bottomNavBar, + size: bottomIconSize, + color: Colors.white, + ), + onPressed: openStickerEditor, + ), ], ), ), @@ -1873,6 +1822,7 @@ class ProImageEditorState extends State { freeStyleHighPerformanceScaling: _freeStyleHighPerformanceScaling, freeStyleHighPerformanceMoving: _freeStyleHighPerformanceMoving, designMode: widget.configs.designMode, + stickerInitWidth: widget.configs.stickerEditorConfigs?.initWidth ?? 100, onTap: (layer) async { if (layer is TextLayerData) { _onTextLayerTap(layer); @@ -1889,9 +1839,7 @@ class ProImageEditorState extends State { }, onRemoveTap: () { setState(() { - _removeLayer( - _layers.indexWhere((element) => element.id == layerItem.id), - layer: layerItem); + _removeLayer(_layers.indexWhere((element) => element.id == layerItem.id), layer: layerItem); }); }, i18n: widget.configs.i18n, @@ -1961,8 +1909,7 @@ class ProImageEditorState extends State { width: kToolbarHeight, decoration: BoxDecoration( color: hoverRemoveBtn ? Colors.red : Colors.grey.shade800, - borderRadius: - const BorderRadius.only(bottomRight: Radius.circular(20)), + borderRadius: const BorderRadius.only(bottomRight: Radius.circular(20)), ), child: Center( child: Icon( diff --git a/lib/widgets/layer_widget.dart b/lib/widgets/layer_widget.dart index efa38020..7c2d57e1 100644 --- a/lib/widgets/layer_widget.dart +++ b/lib/widgets/layer_widget.dart @@ -46,6 +46,9 @@ class LayerWidget extends StatefulWidget { /// Font size for text layers. final double textFontSize; + /// The initial width of the stickers in the editor. + final double stickerInitWidth; + /// The design mode of the editor. final ImageEditorDesignModeE designMode; @@ -77,6 +80,7 @@ class LayerWidget extends StatefulWidget { required this.onRemoveTap, required this.i18n, required this.textFontSize, + required this.stickerInitWidth, required this.emojiTextStyle, required this.enabledHitDetection, required this.freeStyleHighPerformanceScaling, @@ -97,14 +101,22 @@ class _LayerWidgetState extends State { @override void initState() { - if (widget.layerData is TextLayerData) { - _layerType = _LayerType.text; - } else if (widget.layerData is EmojiLayerData) { - _layerType = _LayerType.emoji; - } else if (widget.layerData is PaintingLayerData) { - _layerType = _LayerType.canvas; - } else { - _layerType = _LayerType.unkown; + switch (widget.layerData.runtimeType) { + case TextLayerData: + _layerType = _LayerType.text; + break; + case EmojiLayerData: + _layerType = _LayerType.emoji; + break; + case StickerLayerData: + _layerType = _LayerType.sticker; + break; + case PaintingLayerData: + _layerType = _LayerType.canvas; + break; + default: + _layerType = _LayerType.unknown; + break; } super.initState(); @@ -164,8 +176,7 @@ class _LayerWidgetState extends State { /// Checks if the hit is outside the canvas for certain types of layers. bool _checkHitIsOutsideInCanvas() { - return _layerType == _LayerType.canvas && - !(_layer as PaintingLayerData).item.hit; + return _layerType == _LayerType.canvas && !(_layer as PaintingLayerData).item.hit; } /// Calculates the transformation matrix for the layer's position and rotation. @@ -250,6 +261,8 @@ class _LayerWidgetState extends State { return _buildEmoji(); case _LayerType.text: return _buildText(); + case _LayerType.sticker: + return _buildSticker(); case _LayerType.canvas: return _buildCanvas(); default: @@ -315,6 +328,18 @@ class _LayerWidgetState extends State { ); } + /// Build the sticker widget + Widget _buildSticker() { + var layer = _layer as StickerLayerData; + return SizedBox( + width: widget.stickerInitWidth * layer.scale, + child: FittedBox( + fit: BoxFit.contain, + child: layer.sticker, + ), + ); + } + /// Build the canvas widget Widget _buildCanvas() { var layer = _layer as PaintingLayerData; @@ -329,8 +354,7 @@ class _LayerWidgetState extends State { item: layer.item, scale: widget.layerData.scale, enabledHitDetection: widget.enabledHitDetection, - freeStyleHighPerformanceScaling: - widget.freeStyleHighPerformanceScaling, + freeStyleHighPerformanceScaling: widget.freeStyleHighPerformanceScaling, freeStyleHighPerformanceMoving: widget.freeStyleHighPerformanceMoving, ), ), @@ -339,7 +363,7 @@ class _LayerWidgetState extends State { } // ignore: camel_case_types -enum _LayerType { emoji, text, canvas, unkown } +enum _LayerType { emoji, text, sticker, canvas, unknown } /// Enumeration for controlling the background color mode of the text layer. enum LayerBackgroundColorModeE { diff --git a/pubspec.yaml b/pubspec.yaml index e00af8cd..d1137c8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pro_image_editor description: "A Flutter image editor: Seamlessly enhance your images with user-friendly editing features." -version: 1.0.3 +version: 2.0.0 homepage: https://github.com/hm21/pro_image_editor/ repository: https://github.com/hm21/pro_image_editor/ issue_tracker: https://github.com/hm21/pro_image_editor/issues/ diff --git a/test/modules/sticker_editor_test.dart b/test/modules/sticker_editor_test.dart new file mode 100644 index 00000000..5d806673 --- /dev/null +++ b/test/modules/sticker_editor_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pro_image_editor/modules/sticker_editor.dart'; +import 'package:pro_image_editor/pro_image_editor.dart'; + +void main() { + group('StickerEditor Tests', () { + testWidgets('StickerEditor widget should be created', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: StickerEditor( + configs: StickerEditorConfigs( + enabled: true, + buildStickers: (setLayer) { + return Container(); + }, + ), + ), + )); + + expect(find.byType(StickerEditor), findsOneWidget); + }); + }); +} diff --git a/test/widgets/layer_widget_test.dart b/test/widgets/layer_widget_test.dart index ab23bfbc..e941fb46 100644 --- a/test/widgets/layer_widget_test.dart +++ b/test/widgets/layer_widget_test.dart @@ -38,6 +38,7 @@ void main() { layerHoverCursor: SystemMouseCursors.click, i18n: const I18n(), textFontSize: 16.0, + stickerInitWidth: 100, emojiTextStyle: const TextStyle(fontSize: 16.0), enabledHitDetection: true, freeStyleHighPerformanceScaling: true,