improve draw editor

This commit is contained in:
otsmr 2025-02-03 19:16:32 +01:00
parent fdaa1ff7dd
commit 5f45de620a
9 changed files with 674 additions and 580 deletions

25
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Connect-App",
"request": "launch",
"type": "dart",
},
{
"name": "Connect-App (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "Connect-App (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class ActionButton extends StatelessWidget {
final VoidCallback? onPressed;
final IconData? icon;
final Color? color;
const ActionButton(this.icon, {super.key, this.onPressed, this.color});
@override
Widget build(BuildContext context) {
return IconButton(
icon: FaIcon(
icon,
size: 30,
color: color,
shadows: [
Shadow(
color: const Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0,
)
],
),
onPressed: onPressed,
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hand_signature/signature.dart';
import 'package:twonly/src/components/image_editor/data/image_item.dart'; import 'package:twonly/src/components/image_editor/data/image_item.dart';
/// Layer class with some common properties /// Layer class with some common properties
@ -7,12 +8,14 @@ class Layer {
double rotation, scale, opacity; double rotation, scale, opacity;
bool isEditing; bool isEditing;
bool isDeleted; bool isDeleted;
bool hasCustomActionButtons;
Layer({ Layer({
this.offset = const Offset(0, 0), this.offset = const Offset(0, 0),
this.opacity = 1, this.opacity = 1,
this.isEditing = false, this.isEditing = false,
this.isDeleted = false, this.isDeleted = false,
this.hasCustomActionButtons = false,
this.rotation = 0, this.rotation = 0,
this.scale = 1, this.scale = 1,
}); });
@ -71,3 +74,22 @@ class TextLayerData extends Layer {
super.isEditing = true, super.isEditing = true,
}); });
} }
/// Attributes used by [DrawLayer]
class DrawLayerData extends Layer {
final control = HandSignatureControl(
threshold: 3.0,
smoothRatio: 0.65,
velocityRange: 2.0,
);
// String text;
DrawLayerData({
super.offset,
super.opacity,
super.rotation,
super.scale,
super.hasCustomActionButtons = true,
super.isEditing = true,
});
}

View file

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hand_signature/signature.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/components/image_editor/action_button.dart';
import 'package:twonly/src/components/image_editor/data/layer.dart';
class DrawLayer extends StatefulWidget {
final DrawLayerData layerData;
final VoidCallback? onUpdate;
const DrawLayer({
super.key,
required this.layerData,
this.onUpdate,
});
@override
createState() => _DrawLayerState();
}
class _DrawLayerState extends State<DrawLayer> {
Color pickerColor = Colors.white, currentColor = Colors.white;
var screenshotController = ScreenshotController();
List<CubicPath> undoList = [];
bool skipNextEvent = false;
@override
void initState() {
widget.layerData.control.addListener(() {
if (widget.layerData.control.hasActivePath) return;
if (skipNextEvent) {
skipNextEvent = false;
return;
}
undoList = [];
setState(() {});
});
super.initState();
}
double _sliderValue = 0.0;
final colors = [
Colors.white,
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.indigo,
Colors.blue,
Colors.black,
];
Color _getColorFromSliderValue(double value) {
// Calculate the index based on the slider value
int index = (value * (colors.length - 1)).floor();
int nextIndex = (index + 1).clamp(0, colors.length - 1);
// Calculate the interpolation factor
double factor = value * (colors.length - 1) - index;
// Interpolate between the two colors
return Color.lerp(colors[index], colors[nextIndex], factor)!;
}
void _onSliderChanged(double value) {
setState(() {
_sliderValue = value;
currentColor = _getColorFromSliderValue(value);
});
}
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Screenshot(
controller: screenshotController,
child: HandSignature(
control: widget.layerData.control,
color: currentColor,
width: 1.0,
maxWidth: 7.0,
type: SignatureDrawType.shape,
),
),
),
),
if (widget.layerData.isEditing)
Positioned(
top: 5,
left: 5,
right: 50,
child: Row(
children: [
ActionButton(
FontAwesomeIcons.check,
onPressed: () async {
widget.layerData.isEditing = false;
},
),
Expanded(child: Container()),
ActionButton(
FontAwesomeIcons.arrowRotateLeft,
color: widget.layerData.control.paths.isNotEmpty
? Colors.white
: Colors.white.withAlpha(80),
onPressed: () {
if (widget.layerData.control.paths.isEmpty) return;
skipNextEvent = true;
undoList.add(widget.layerData.control.paths.last);
widget.layerData.control.stepBack();
setState(() {});
},
),
ActionButton(
FontAwesomeIcons.arrowRotateRight,
color: undoList.isNotEmpty
? Colors.white
: Colors.white.withAlpha(80),
onPressed: () {
if (undoList.isEmpty) return;
widget.layerData.control.paths.add(undoList.removeLast());
setState(() {});
},
),
],
),
),
if (widget.layerData.isEditing)
Positioned(
right: 20,
top: 50,
child: Stack(
children: <Widget>[
Container(
height: 240,
width: 40,
color: Colors.transparent,
),
SizedBox(
height: 240,
width: 40,
child: Center(
child: Container(
alignment: Alignment.center,
width: 10,
height: 195,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: colors,
stops: List.generate(colors.length,
(index) => index / (colors.length - 1)),
),
borderRadius: BorderRadius.circular(10),
),
),
),
),
Positioned.fill(
child: RotatedBox(
quarterTurns: 1,
child: Slider(
value: _sliderValue,
thumbColor: currentColor,
activeColor: Colors.transparent,
inactiveColor: Colors.transparent,
onChanged: _onSliderChanged,
min: 0.0,
max: 1.0,
divisions: 100,
),
),
),
],
),
),
if (!widget.layerData.isEditing)
Positioned.fill(
child: Container(
color: Colors.transparent,
))
],
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/image_editor/action_button.dart';
import 'package:twonly/src/components/image_editor/data/layer.dart'; import 'package:twonly/src/components/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera_to_share/share_image_editor_view.dart';
/// Text layer /// Text layer
class TextLayer extends StatefulWidget { class TextLayer extends StatefulWidget {
@ -148,9 +148,8 @@ class _TextViewState extends State<TextLayer> {
onTapUp: (d) { onTapUp: (d) {
textController.text = ""; textController.text = "";
}, },
child: FaIcon( child: ActionButton(
FontAwesomeIcons.trashCan, FontAwesomeIcons.trashCan,
shadows: ShareImageEditorView.iconShadow,
color: deleteLayer ? Colors.red : Colors.white, color: deleteLayer ? Colors.red : Colors.white,
), ),
), ),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/image_editor/data/layer.dart'; import 'package:twonly/src/components/image_editor/data/layer.dart';
import 'package:twonly/src/components/image_editor/layers/background_layer.dart'; import 'package:twonly/src/components/image_editor/layers/background_layer.dart';
import 'package:twonly/src/components/image_editor/layers/draw_layer.dart';
import 'package:twonly/src/components/image_editor/layers/emoji_layer.dart'; import 'package:twonly/src/components/image_editor/layers/emoji_layer.dart';
import 'package:twonly/src/components/image_editor/layers/image_layer.dart'; import 'package:twonly/src/components/image_editor/layers/image_layer.dart';
import 'package:twonly/src/components/image_editor/layers/text_layer.dart'; import 'package:twonly/src/components/image_editor/layers/text_layer.dart';
@ -20,42 +21,53 @@ class LayersViewer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: layers.map((layerItem) { children: [
// Background layer // Background and Image layers at the bottom
...layers
.where((layerItem) =>
layerItem is BackgroundLayerData || layerItem is ImageLayerData)
.map((layerItem) {
if (layerItem is BackgroundLayerData) { if (layerItem is BackgroundLayerData) {
return BackgroundLayer( return BackgroundLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} } else if (layerItem is ImageLayerData) {
// Image layer
if (layerItem is ImageLayerData) {
return ImageLayer( return ImageLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} }
return Container(); // Fallback, should not reach here
}),
// Emoji layer // Draw layer (if needed, can be placed anywhere)
...layers.whereType<DrawLayerData>().map((layerItem) {
return DrawLayer(
layerData: layerItem,
onUpdate: onUpdate,
);
}),
// Emoji and Text layers at the top
...layers
.where((layerItem) =>
layerItem is EmojiLayerData || layerItem is TextLayerData)
.map((layerItem) {
if (layerItem is EmojiLayerData) { if (layerItem is EmojiLayerData) {
return EmojiLayer( return EmojiLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} } else if (layerItem is TextLayerData) {
// Text layer
if (layerItem is TextLayerData) {
return TextLayer( return TextLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} }
return Container(); // Fallback, should not reach here
// Blank layer }),
return Container(); ],
}).toList(),
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -58,6 +59,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaViewSizing( return MediaViewSizing(
Stack(
children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
child: CameraAwesomeBuilder.custom( child: CameraAwesomeBuilder.custom(
@ -86,8 +89,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
opaque: false, opaque: false,
pageBuilder: (context, a1, a2) => pageBuilder: (context, a1, a2) =>
ShareImageEditorView(imageBytes: imageBytes), ShareImageEditorView(imageBytes: imageBytes),
transitionsBuilder: transitionsBuilder: (context, animation,
(context, animation, secondaryAnimation, child) { secondaryAnimation, child) {
return child; return child;
}, },
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
@ -102,7 +105,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}, },
multiple: (multiple) { multiple: (multiple) {
multiple.fileBySensor.forEach((key, value) { multiple.fileBySensor.forEach((key, value) {
debugPrint('multiple image taken: $key ${value?.path}'); debugPrint(
'multiple image taken: $key ${value?.path}');
}); });
}, },
); );
@ -117,7 +121,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}, },
multiple: (multiple) { multiple: (multiple) {
multiple.fileBySensor.forEach((key, value) { multiple.fileBySensor.forEach((key, value) {
debugPrint('multiple video taken: $key ${value?.path}'); debugPrint(
'multiple video taken: $key ${value?.path}');
}); });
}, },
); );
@ -172,7 +177,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
icon: FontAwesomeIcons.repeat, icon: FontAwesomeIcons.repeat,
onTap: () async { onTap: () async {
cameraState.switchCameraSensor( cameraState.switchCameraSensor(
aspectRatio: CameraAspectRatios.ratio_16_9); aspectRatio:
CameraAspectRatios.ratio_16_9);
}, },
), ),
SizedBox(height: 20), SizedBox(height: 20),
@ -180,7 +186,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
icon: FontAwesomeIcons.bolt, icon: FontAwesomeIcons.bolt,
color: isFlashOn color: isFlashOn
? const Color.fromARGB(255, 255, 230, 0) ? const Color.fromARGB(255, 255, 230, 0)
: const Color.fromARGB(158, 255, 255, 255), : const Color.fromARGB(
158, 255, 255, 255),
onTap: () async { onTap: () async {
if (isFlashOn) { if (isFlashOn) {
cameraState.sensorConfig cameraState.sensorConfig
@ -287,6 +294,20 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
// ), // ),
), ),
), ),
if (sharePreviewIsShown)
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 100.0,
sigmaY: 100.0,
),
child: Center(
child: CircularProgressIndicator(),
),
),
)
],
),
); );
} }
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/image_editor/action_button.dart';
import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/media_view_sizing.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart';
@ -11,7 +12,6 @@ import 'package:twonly/src/components/image_editor/data/layer.dart';
import 'package:twonly/src/components/image_editor/layers_viewer.dart'; import 'package:twonly/src/components/image_editor/layers_viewer.dart';
import 'package:twonly/src/components/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/components/image_editor/modules/all_emojis.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:hand_signature/signature.dart';
List<Layer> layers = []; List<Layer> layers = [];
List<Layer> undoLayers = []; List<Layer> undoLayers = [];
@ -20,18 +20,13 @@ List<Layer> removedLayers = [];
class ShareImageEditorView extends StatefulWidget { class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({super.key, required this.imageBytes}); const ShareImageEditorView({super.key, required this.imageBytes});
final Uint8List imageBytes; final Uint8List imageBytes;
static List<Shadow> get iconShadow => [
Shadow(
color: const Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0,
)
];
@override @override
State<ShareImageEditorView> createState() => _ShareImageEditorView(); State<ShareImageEditorView> createState() => _ShareImageEditorView();
} }
class _ShareImageEditorView extends State<ShareImageEditorView> { class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _imageSaved = false; bool _imageSaved = false;
bool _imageSaving = false;
ImageItem currentImage = ImageItem(); ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
@ -49,24 +44,90 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.dispose(); super.dispose();
} }
List<Widget> get filterActions { List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty &&
layers.last.isEditing &&
layers.last.hasCustomActionButtons) {
return [];
}
return <Widget>[
BottomButton(
icon: FontAwesomeIcons.font,
onTap: () async {
undoLayers.clear();
removedLayers.clear();
layers.add(TextLayerData());
setState(() {});
},
),
BottomButton(
icon: FontAwesomeIcons.pencil,
onTap: () async {
// var drawing = await Navigator.push(
// context,
// PageRouteBuilder(
// opaque: false,
// pageBuilder: (context, a, b) => ImageEditorDrawing(
// image: currentImage,
// ),
// transitionDuration: Duration.zero,
// reverseTransitionDuration: Duration.zero,
// ),
// );
// if (drawing != null) {
undoLayers.clear();
removedLayers.clear();
layers.add(DrawLayerData());
// setState(() {});
// }
},
),
BottomButton(
icon: FontAwesomeIcons.faceGrinWide,
onTap: () async {
EmojiLayerData? layer = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (BuildContext context) {
return const Emojis();
},
);
if (layer == null) return;
undoLayers.clear();
removedLayers.clear();
layers.add(layer);
setState(() {});
},
),
];
}
List<Widget> get actionsAtTheTop {
if (layers.isNotEmpty &&
layers.last.isEditing &&
layers.last.hasCustomActionButtons) {
return [];
}
return [ return [
IconButton( ActionButton(
icon: FaIcon(FontAwesomeIcons.xmark, FontAwesomeIcons.xmark,
size: 30, shadows: ShareImageEditorView.iconShadow),
color: Colors.white,
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
Expanded(child: Container()), Expanded(child: Container()),
IconButton( const SizedBox(width: 8),
padding: const EdgeInsets.symmetric(horizontal: 8), ActionButton(
icon: FaIcon(FontAwesomeIcons.rotateLeft, FontAwesomeIcons.rotateLeft,
color: layers.length > 1 || removedLayers.isNotEmpty color: layers.length > 1 || removedLayers.isNotEmpty
? Colors.white ? Colors.white
: Colors.grey, : Colors.grey,
shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (removedLayers.isNotEmpty) { if (removedLayers.isNotEmpty) {
layers.add(removedLayers.removeLast()); layers.add(removedLayers.removeLast());
@ -79,16 +140,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {}); setState(() {});
}, },
), ),
IconButton( const SizedBox(width: 8),
padding: const EdgeInsets.symmetric(horizontal: 8), ActionButton(
icon: FaIcon(FontAwesomeIcons.rotateRight, FontAwesomeIcons.rotateRight,
color: undoLayers.isNotEmpty ? Colors.white : Colors.grey, color: undoLayers.isNotEmpty ? Colors.white : Colors.grey,
shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (undoLayers.isEmpty) return; if (undoLayers.isEmpty) return;
layers.add(undoLayers.removeLast()); layers.add(undoLayers.removeLast());
setState(() {}); setState(() {});
}, },
), ),
@ -168,7 +226,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
right: 0, right: 0,
child: SafeArea( child: SafeArea(
child: Row( child: Row(
children: filterActions, children: actionsAtTheTop,
), ),
), ),
), ),
@ -181,69 +239,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: actionsAtTheRight,
BottomButton(
icon: FontAwesomeIcons.font,
onTap: () async {
undoLayers.clear();
removedLayers.clear();
layers.add(TextLayerData());
setState(() {});
},
),
SizedBox(height: 20),
BottomButton(
icon: FontAwesomeIcons.pencil,
onTap: () async {
var drawing = await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a, b) => ImageEditorDrawing(
image: currentImage,
),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
),
);
if (drawing != null) {
undoLayers.clear();
removedLayers.clear();
layers.add(
ImageLayerData(
image: ImageItem(drawing),
offset: Offset(0, 0),
),
);
setState(() {});
}
},
),
SizedBox(height: 20),
BottomButton(
icon: FontAwesomeIcons.faceGrinWide,
onTap: () async {
EmojiLayerData? layer = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (BuildContext context) {
return const Emojis();
},
);
if (layer == null) return;
undoLayers.clear();
removedLayers.clear();
layers.add(layer);
setState(() {});
},
),
],
), ),
), ),
), ),
@ -259,7 +255,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
icon: _imageSaved icon: _imageSaving
? SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 1))
: _imageSaved
? Icon(Icons.check) ? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk), : FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
@ -271,11 +272,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
), ),
onPressed: () async { onPressed: () async {
setState(() {
_imageSaving = true;
});
Uint8List? imageBytes = await getMergedImage(); Uint8List? imageBytes = await getMergedImage();
if (imageBytes == null || !context.mounted) return; if (imageBytes == null || !context.mounted) return;
final res = await saveImageToGallery(imageBytes); final res = await saveImageToGallery(imageBytes);
if (res == null) { if (res == null) {
setState(() { setState(() {
_imageSaving = false;
_imageSaved = true; _imageSaved = true;
}); });
} }
@ -341,10 +346,10 @@ class BottomButton extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column( child: Column(
children: [ children: [
FaIcon( ActionButton(
icon, icon,
color: color, color: color,
shadows: ShareImageEditorView.iconShadow, onPressed: onTap ?? () {},
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@ -354,240 +359,20 @@ class BottomButton extends StatelessWidget {
} }
} }
/// Show image drawing surface over image // /// Show image drawing surface over image
class ImageEditorDrawing extends StatefulWidget { // class ImageEditorDrawing extends StatefulWidget {
final ImageItem image; // final ImageItem image;
const ImageEditorDrawing({ // const ImageEditorDrawing({
super.key, // super.key,
required this.image, // required this.image,
}); // });
@override // @override
State<ImageEditorDrawing> createState() => _ImageEditorDrawingState(); // State<ImageEditorDrawing> createState() => _ImageEditorDrawingState();
}
class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
Color pickerColor = Colors.white, currentColor = Colors.white;
var screenshotController = ScreenshotController();
final control = HandSignatureControl(
threshold: 3.0,
smoothRatio: 0.65,
velocityRange: 2.0,
);
List<CubicPath> undoList = [];
bool skipNextEvent = false;
// void changeColor(Colors color) {
// currentColor = color.color;
// currentBackgroundColor = color.background;
// setState(() {});
// } // }
@override // class _ImageEditorDrawingState extends State<ImageEditorDrawing> {
void initState() {
control.addListener(() {
if (control.hasActivePath) return;
if (skipNextEvent) { // }
skipNextEvent = false; // }
return;
}
undoList = [];
setState(() {});
});
super.initState();
}
@override
Widget build(BuildContext context) {
final colors = [
Colors.black,
Colors.white,
Colors.blue,
Colors.green,
Colors.pink,
Colors.purple,
Colors.brown,
Colors.indigo,
];
return Scaffold(
backgroundColor: Colors.red.withAlpha(0),
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
Positioned.fill(
top: 0,
child: Container(
height: 600,
width: 300,
decoration: BoxDecoration(
color: const Color.fromARGB(0, 210, 7, 7),
),
// child: Container(),
child: Screenshot(
controller: screenshotController,
// image: widget.options.showBackground
// ? DecorationImage(
// image: Image.memory(widget.image.bytes).image,
// fit: BoxFit.contain,
// )
// : null,
// child: Container(),
child: HandSignature(
control: control,
color: currentColor,
width: 1.0,
maxWidth: 7.0,
type: SignatureDrawType.shape,
),
),
),
),
Positioned(
top: 100,
right: 50,
child: Column(
children: [
IconButton(
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: const Icon(Icons.clear),
onPressed: () {
Navigator.pop(context);
},
),
IconButton(
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: Icon(
Icons.undo,
color: control.paths.isNotEmpty
? Colors.white
: Colors.white.withAlpha(80),
),
onPressed: () {
if (control.paths.isEmpty) return;
skipNextEvent = true;
undoList.add(control.paths.last);
control.stepBack();
setState(() {});
},
),
IconButton(
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: Icon(
Icons.redo,
color: undoList.isNotEmpty
? Colors.white
: Colors.white.withAlpha(80),
),
onPressed: () {
if (undoList.isEmpty) return;
control.paths.add(undoList.removeLast());
setState(() {});
},
),
IconButton(
padding: const EdgeInsets.symmetric(horizontal: 8),
icon: const Icon(Icons.check),
onPressed: () async {
if (control.paths.isEmpty) return Navigator.pop(context);
var data = await control.toImage(
color: currentColor,
height: widget.image.height,
width: widget.image.width,
);
if (!mounted) return;
return Navigator.pop(context, data!.buffer.asUint8List());
// var loadingScreen = showLoadingScreen(context);
// var image = await screenshotController.capture();
// loadingScreen.hide();
// if (!mounted) return;
// return Navigator.pop(context, image);
},
),
],
),
),
Positioned(
right: 0,
top: 50,
child: Container(
child: Container(
// height: 80,
padding: EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: const Color.fromARGB(130, 0, 0, 0),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Column(
children: <Widget>[
for (var color in colors)
ColorButton(
color: color,
onTap: (color) {
currentColor = color;
setState(() {});
},
isSelected: color == currentColor,
),
],
),
),
),
),
],
),
),
);
}
}
class ColorButton extends StatelessWidget {
final Color color;
final Function(Color) onTap;
final bool isSelected;
const ColorButton({
super.key,
required this.color,
required this.onTap,
this.isSelected = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onTap(color);
},
child: Container(
height: 17,
width: 17,
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? Colors.white : Colors.white54,
width: isSelected ? 2 : 1,
),
),
),
);
}
}

View file

@ -11,6 +11,8 @@ environment:
dependencies: dependencies:
camerawesome: ^2.1.0 camerawesome: ^2.1.0
# camerawesome:
# path: ../CamerAwesome
collection: ^1.18.0 collection: ^1.18.0
connectivity_plus: ^6.1.2 connectivity_plus: ^6.1.2
cv: ^1.1.3 cv: ^1.1.3