mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 16:28:40 +00:00
image editor in a good state
This commit is contained in:
parent
5f45de620a
commit
5675437bc0
8 changed files with 67 additions and 132 deletions
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue