image editor in a good state

This commit is contained in:
otsmr 2025-02-03 22:18:09 +01:00
parent 5f45de620a
commit 5675437bc0
8 changed files with 67 additions and 132 deletions

View file

@ -14,7 +14,7 @@ class ActionButton extends StatelessWidget {
icon: FaIcon( icon: FaIcon(
icon, icon,
size: 30, size: 30,
color: color, color: color ?? Colors.white,
shadows: [ shadows: [
Shadow( Shadow(
color: const Color.fromARGB(122, 0, 0, 0), color: const Color.fromARGB(122, 0, 0, 0),

View file

@ -46,22 +46,6 @@ class EmojiLayerData extends Layer {
}); });
} }
/// Attributes used by [ImageLayer]
class ImageLayerData extends Layer {
ImageItem image;
double size;
ImageLayerData({
required this.image,
this.size = 64,
super.offset,
super.opacity,
super.rotation,
super.scale,
super.isEditing,
});
}
/// Attributes used by [TextLayer] /// Attributes used by [TextLayer]
class TextLayerData extends Layer { class TextLayerData extends Layer {
String text; String text;

View file

@ -17,36 +17,66 @@ class EmojiLayer extends StatefulWidget {
} }
class _EmojiLayerState extends State<EmojiLayer> { class _EmojiLayerState extends State<EmojiLayer> {
double initialSize = 0;
double initialRotation = 0; double initialRotation = 0;
Offset initialOffset = Offset.zero;
double initialScale = 1.0;
final GlobalKey _key = GlobalKey();
@override
void initState() {
super.initState();
if (widget.layerData.offset.dy == 0) {
// Set the initial offset to the center of the screen
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
widget.layerData.offset = Offset(
MediaQuery.of(context).size.width / 2 - 64,
MediaQuery.of(context).size.height / 2 - 64 - 100);
});
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
initialSize = widget.layerData.size;
initialRotation = widget.layerData.rotation;
return Positioned( return Positioned(
left: widget.layerData.offset.dx, left: widget.layerData.offset.dx,
top: widget.layerData.offset.dy, top: widget.layerData.offset.dy,
child: GestureDetector( child: GestureDetector(
onTap: () {}, onTap: () {},
onScaleUpdate: (detail) { onScaleStart: (details) {
if (detail.pointerCount == 1) { // Store the initial scale and rotation
widget.layerData.offset = Offset( initialScale = widget.layerData.size; // Reset initial scale
widget.layerData.offset.dx + detail.focalPointDelta.dx, initialRotation = widget.layerData.rotation;
widget.layerData.offset.dy + detail.focalPointDelta.dy, initialOffset = widget.layerData.offset;
); },
} else if (detail.pointerCount == 2) { onScaleUpdate: (details) {
widget.layerData.size = setState(() {
initialSize + detail.scale * 5 * (detail.scale > 1 ? 1 : -1); // Update the size based on the scale factor
}
setState(() {}); widget.layerData.size = initialScale * details.scale;
// Update the rotation based on the rotation angle
widget.layerData.rotation = initialRotation + details.rotation;
// Update the position based on the translation
final RenderBox renderBox =
_key.currentContext?.findRenderObject() as RenderBox;
var dx = details.focalPoint.dx - (renderBox.size.width / 2);
var dy = details.focalPoint.dy - (renderBox.size.height / 2 + 34);
widget.layerData.offset = Offset(dx, dy);
});
},
onScaleEnd: (details) {
// Optionally, you can handle the end of the scale gesture here
}, },
child: Transform.rotate( child: Transform.rotate(
key: _key,
angle: widget.layerData.rotation, angle: widget.layerData.rotation,
child: Container( child: Container(
padding: const EdgeInsets.all(64), padding: const EdgeInsets.all(34),
color: Colors.transparent,
child: Text( child: Text(
widget.layerData.text.toString(), widget.layerData.text.toString(),
style: TextStyle( style: TextStyle(

View file

@ -1,36 +0,0 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/components/image_editor/data/layer.dart';
/// Image layer that can be used to add overlay images and drawings
class ImageLayer extends StatefulWidget {
final ImageLayerData layerData;
final VoidCallback? onUpdate;
const ImageLayer({
super.key,
required this.layerData,
this.onUpdate,
});
@override
createState() => _ImageLayerState();
}
class _ImageLayerState extends State<ImageLayer> {
double initialSize = 0;
double initialRotation = 0;
@override
Widget build(BuildContext context) {
initialSize = widget.layerData.size;
initialRotation = widget.layerData.rotation;
return Positioned.fill(
child: SizedBox(
width: widget.layerData.image.width.toDouble(),
height: widget.layerData.image.height.toDouble(),
child: Image.memory(widget.layerData.image.bytes),
),
);
}
}

View file

@ -3,7 +3,6 @@ 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/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/text_layer.dart'; import 'package:twonly/src/components/image_editor/layers/text_layer.dart';
/// View stacked layers (unbounded height, width) /// View stacked layers (unbounded height, width)
@ -22,50 +21,34 @@ class LayersViewer extends StatelessWidget {
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// Background and Image layers at the bottom ...layers.whereType<BackgroundLayerData>().map((layerItem) {
...layers return BackgroundLayer(
.where((layerItem) =>
layerItem is BackgroundLayerData || layerItem is ImageLayerData)
.map((layerItem) {
if (layerItem is BackgroundLayerData) {
return BackgroundLayer(
layerData: layerItem,
onUpdate: onUpdate,
);
} else if (layerItem is ImageLayerData) {
return ImageLayer(
layerData: layerItem,
onUpdate: onUpdate,
);
}
return Container(); // Fallback, should not reach here
}),
// Draw layer (if needed, can be placed anywhere)
...layers.whereType<DrawLayerData>().map((layerItem) {
return DrawLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
}), }),
// Emoji and Text layers at the top
...layers ...layers
.where((layerItem) => .where((layerItem) =>
layerItem is EmojiLayerData || layerItem is TextLayerData) layerItem is EmojiLayerData || layerItem is DrawLayerData)
.map((layerItem) { .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) { } else if (layerItem is DrawLayerData) {
return TextLayer( return DrawLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
); );
} }
return Container(); // Fallback, should not reach here return Container();
}),
...layers.whereType<TextLayerData>().map((layerItem) {
return TextLayer(
layerData: layerItem,
onUpdate: onUpdate,
);
}), }),
], ],
); );

View file

@ -31,13 +31,6 @@ class _EmojisState extends State<Emojis> {
), ),
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
('Select Emoji'),
style: const TextStyle(color: Colors.white),
),
]),
const SizedBox(height: 16), const SizedBox(height: 16),
Container( Container(
height: 315, height: 315,

View file

@ -4,6 +4,7 @@ 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';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.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/components/permissions_view.dart'; import 'package:twonly/src/components/permissions_view.dart';
import 'package:twonly/src/views/camera_to_share/share_image_editor_view.dart'; import 'package:twonly/src/views/camera_to_share/share_image_editor_view.dart';
@ -173,22 +174,22 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
BottomButton( ActionButton(
icon: FontAwesomeIcons.repeat, FontAwesomeIcons.repeat,
onTap: () async { onPressed: () async {
cameraState.switchCameraSensor( cameraState.switchCameraSensor(
aspectRatio: aspectRatio:
CameraAspectRatios.ratio_16_9); CameraAspectRatios.ratio_16_9);
}, },
), ),
SizedBox(height: 20), // SizedBox(height: 20),
BottomButton( ActionButton(
icon: FontAwesomeIcons.bolt, FontAwesomeIcons.bolt,
color: isFlashOn color: isFlashOn
? const Color.fromARGB(255, 255, 230, 0) ? const Color.fromARGB(255, 255, 230, 0)
: const Color.fromARGB( : const Color.fromARGB(
158, 255, 255, 255), 158, 255, 255, 255),
onTap: () async { onPressed: () async {
if (isFlashOn) { if (isFlashOn) {
cameraState.sensorConfig cameraState.sensorConfig
.setFlashMode(FlashMode.none); .setFlashMode(FlashMode.none);

View file

@ -63,26 +63,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
BottomButton( BottomButton(
icon: FontAwesomeIcons.pencil, icon: FontAwesomeIcons.pencil,
onTap: () async { 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(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
layers.add(DrawLayerData()); layers.add(DrawLayerData());
// setState(() {});
// }
}, },
), ),
BottomButton( BottomButton(
@ -156,7 +139,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
double widthRatio = 1, heightRatio = 1, pixelRatio = 1; double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
/// obtain image Uint8List by merging layers
Future<Uint8List?> getMergedImage() async { Future<Uint8List?> getMergedImage() async {
Uint8List? image; Uint8List? image;
@ -165,8 +147,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} else if (layers.length == 1) { } else if (layers.length == 1) {
if (layers.first is BackgroundLayerData) { if (layers.first is BackgroundLayerData) {
image = (layers.first as BackgroundLayerData).image.bytes; image = (layers.first as BackgroundLayerData).image.bytes;
} else if (layers.first is ImageLayerData) {
image = (layers.first as ImageLayerData).image.bytes;
} }
} }
return image; return image;