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
+
|
@@ -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,