text fields editing works

This commit is contained in:
otsmr 2025-02-02 20:23:47 +01:00
parent d72f0abfc0
commit d13de25f96
7 changed files with 185 additions and 116 deletions

View file

@ -5,10 +5,14 @@ import 'package:twonly/src/components/image_editor/data/image_item.dart';
class Layer { class Layer {
Offset offset; Offset offset;
double rotation, scale, opacity; double rotation, scale, opacity;
bool isEditing;
bool isDeleted;
Layer({ Layer({
this.offset = const Offset(64, 64), this.offset = const Offset(0, 0),
this.opacity = 1, this.opacity = 1,
this.isEditing = false,
this.isDeleted = false,
this.rotation = 0, this.rotation = 0,
this.scale = 1, this.scale = 1,
}); });
@ -35,6 +39,7 @@ class EmojiLayerData extends Layer {
super.opacity, super.opacity,
super.rotation, super.rotation,
super.scale, super.scale,
super.isEditing,
}); });
} }
@ -50,40 +55,19 @@ class ImageLayerData extends Layer {
super.opacity, super.opacity,
super.rotation, super.rotation,
super.scale, super.scale,
super.isEditing,
}); });
} }
/// Attributes used by [TextLayer] /// Attributes used by [TextLayer]
class TextLayerData extends Layer { class TextLayerData extends Layer {
String text; String text;
TextLayerData({ TextLayerData({
required this.text, this.text = "",
super.offset,
super.opacity,
super.rotation,
super.scale,
});
}
/// Attributes used by [TextLayer]
class LinkLayerData extends Layer {
String text;
double size;
Color color, background;
double backgroundOpacity;
TextAlign align;
LinkLayerData({
required this.text,
this.size = 64,
this.color = Colors.white,
this.background = Colors.transparent,
this.backgroundOpacity = 0,
this.align = TextAlign.left,
super.offset, super.offset,
super.opacity, super.opacity,
super.rotation, super.rotation,
super.scale, super.scale,
super.isEditing = true,
}); });
} }

View file

@ -5,13 +5,11 @@ import 'package:twonly/src/components/image_editor/data/layer.dart';
class BackgroundLayer extends StatefulWidget { class BackgroundLayer extends StatefulWidget {
final BackgroundLayerData layerData; final BackgroundLayerData layerData;
final VoidCallback? onUpdate; final VoidCallback? onUpdate;
final bool editable;
const BackgroundLayer({ const BackgroundLayer({
super.key, super.key,
required this.layerData, required this.layerData,
this.onUpdate, this.onUpdate,
this.editable = false,
}); });
@override @override

View file

@ -5,13 +5,11 @@ import 'package:twonly/src/components/image_editor/data/layer.dart';
class EmojiLayer extends StatefulWidget { class EmojiLayer extends StatefulWidget {
final EmojiLayerData layerData; final EmojiLayerData layerData;
final VoidCallback? onUpdate; final VoidCallback? onUpdate;
final bool editable;
const EmojiLayer({ const EmojiLayer({
super.key, super.key,
required this.layerData, required this.layerData,
this.onUpdate, this.onUpdate,
this.editable = false,
}); });
@override @override
@ -31,22 +29,20 @@ class _EmojiLayerState extends State<EmojiLayer> {
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: widget.editable ? () {} : null, onTap: () {},
onScaleUpdate: widget.editable onScaleUpdate: (detail) {
? (detail) {
if (detail.pointerCount == 1) { if (detail.pointerCount == 1) {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
widget.layerData.offset.dx + detail.focalPointDelta.dx, widget.layerData.offset.dx + detail.focalPointDelta.dx,
widget.layerData.offset.dy + detail.focalPointDelta.dy, widget.layerData.offset.dy + detail.focalPointDelta.dy,
); );
} else if (detail.pointerCount == 2) { } else if (detail.pointerCount == 2) {
widget.layerData.size = initialSize + widget.layerData.size =
detail.scale * 5 * (detail.scale > 1 ? 1 : -1); initialSize + detail.scale * 5 * (detail.scale > 1 ? 1 : -1);
} }
setState(() {}); setState(() {});
} },
: null,
child: Transform.rotate( child: Transform.rotate(
angle: widget.layerData.rotation, angle: widget.layerData.rotation,
child: Container( child: Container(

View file

@ -5,13 +5,11 @@ import 'package:twonly/src/components/image_editor/data/layer.dart';
class ImageLayer extends StatefulWidget { class ImageLayer extends StatefulWidget {
final ImageLayerData layerData; final ImageLayerData layerData;
final VoidCallback? onUpdate; final VoidCallback? onUpdate;
final bool editable;
const ImageLayer({ const ImageLayer({
super.key, super.key,
required this.layerData, required this.layerData,
this.onUpdate, this.onUpdate,
this.editable = false,
}); });
@override @override

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/share_image_editor_view.dart';
/// Text layer /// Text layer
class TextLayer extends StatefulWidget { class TextLayer extends StatefulWidget {
final TextLayerData layerData; final TextLayerData layerData;
final VoidCallback? onUpdate; final VoidCallback? onUpdate;
final bool editable;
const TextLayer({ const TextLayer({
super.key, super.key,
required this.layerData, required this.layerData,
this.onUpdate, this.onUpdate,
this.editable = false,
}); });
@override @override
createState() => _TextViewState(); createState() => _TextViewState();
@ -19,33 +19,116 @@ class TextLayer extends StatefulWidget {
class _TextViewState extends State<TextLayer> { class _TextViewState extends State<TextLayer> {
double initialRotation = 0; double initialRotation = 0;
bool deleteLayer = false;
bool isDeleted = false;
bool elementIsScaled = false;
final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey
final TextEditingController textController = TextEditingController();
@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(0, MediaQuery.of(context).size.height / 2 - 30);
});
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isDeleted) return Container();
if (widget.layerData.isEditing) {
return Positioned( return Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom - 100,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(100),
),
child: TextField(
controller: textController,
autofocus: true,
onEditingComplete: () {
setState(() {
widget.layerData.isEditing = false;
widget.layerData.text = textController.text;
});
},
onTapOutside: (a) {
widget.layerData.text = textController.text;
Future.delayed(Duration(milliseconds: 100), () {
setState(() {
widget.layerData.isEditing = false;
});
});
},
decoration: InputDecoration(
border: InputBorder.none, // Keine Umrandung
contentPadding: EdgeInsets.zero, // Kein Padding
),
// widget.layerData.text.toString(),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 17,
),
),
),
);
}
return Stack(
key: _widgetKey,
children: [
Positioned(
left: 0, left: 0,
right: 0, right: 0,
top: widget.layerData.offset.dy, top: widget.layerData.offset.dy,
child: GestureDetector( child: GestureDetector(
onTap: widget.editable ? () {} : null, onScaleStart: (d) {
onScaleUpdate: widget.editable setState(() {
? (detail) { elementIsScaled = true;
});
},
onScaleEnd: (d) {
if (deleteLayer) isDeleted = true;
elementIsScaled = false;
setState(() {});
},
onTap: () {
setState(() {
widget.layerData.isEditing = true;
});
},
onScaleUpdate: (detail) {
if (detail.pointerCount == 1) { if (detail.pointerCount == 1) {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
0, 0, widget.layerData.offset.dy + detail.focalPointDelta.dy);
widget.layerData.offset.dy + detail.focalPointDelta.dy, }
); final RenderBox renderBox =
_widgetKey.currentContext!.findRenderObject() as RenderBox;
if (widget.layerData.offset.dy > renderBox.size.height - 80) {
deleteLayer = true;
} else {
deleteLayer = false;
} }
setState(() {}); setState(() {});
} },
: null,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withAlpha(100), color: Colors.black.withAlpha(100),
), ),
child: Text( child: Text(
widget.layerData.text.toString(), widget.layerData.text,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
@ -54,6 +137,26 @@ class _TextViewState extends State<TextLayer> {
), ),
), ),
), ),
),
if (elementIsScaled)
Positioned(
left: 0,
right: 0,
bottom: 20,
child: Center(
child: GestureDetector(
onTapUp: (d) {
textController.text = "";
},
child: FaIcon(
FontAwesomeIcons.trashCan,
shadows: ShareImageEditorView.iconShadow,
color: deleteLayer ? Colors.red : Colors.white,
),
),
),
),
],
); );
} }
} }

View file

@ -9,12 +9,10 @@ import 'package:twonly/src/components/image_editor/layers/text_layer.dart';
class LayersViewer extends StatelessWidget { class LayersViewer extends StatelessWidget {
final List<Layer> layers; final List<Layer> layers;
final Function()? onUpdate; final Function()? onUpdate;
final bool editable;
const LayersViewer({ const LayersViewer({
super.key, super.key,
required this.layers, required this.layers,
required this.editable,
this.onUpdate, this.onUpdate,
}); });
@ -28,7 +26,6 @@ class LayersViewer extends StatelessWidget {
return BackgroundLayer( return BackgroundLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
editable: editable,
); );
} }
@ -37,7 +34,6 @@ class LayersViewer extends StatelessWidget {
return ImageLayer( return ImageLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
editable: editable,
); );
} }
@ -46,7 +42,6 @@ class LayersViewer extends StatelessWidget {
return EmojiLayer( return EmojiLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
editable: editable,
); );
} }
@ -55,7 +50,6 @@ class LayersViewer extends StatelessWidget {
return TextLayer( return TextLayer(
layerData: layerItem, layerData: layerItem,
onUpdate: onUpdate, onUpdate: onUpdate,
editable: editable,
); );
} }

View file

@ -20,7 +20,12 @@ 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();
} }
@ -44,17 +49,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.dispose(); super.dispose();
} }
static List<Shadow> get iconShadow => [
Shadow(
color: const Color.fromARGB(122, 0, 0, 0),
blurRadius: 5.0,
)
];
List<Widget> get filterActions { List<Widget> get filterActions {
return [ return [
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.xmark, size: 30, shadows: iconShadow), icon: FaIcon(FontAwesomeIcons.xmark,
size: 30, shadows: ShareImageEditorView.iconShadow),
color: Colors.white, color: Colors.white,
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
@ -67,13 +66,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
color: layers.length > 1 || removedLayers.isNotEmpty color: layers.length > 1 || removedLayers.isNotEmpty
? Colors.white ? Colors.white
: Colors.grey, : Colors.grey,
shadows: iconShadow), shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (removedLayers.isNotEmpty) { if (removedLayers.isNotEmpty) {
layers.add(removedLayers.removeLast()); layers.add(removedLayers.removeLast());
setState(() {}); setState(() {});
return; return;
} }
layers = layers.where((x) => !x.isDeleted).toList();
if (layers.length <= 1) return; // do not remove image layer if (layers.length <= 1) return; // do not remove image layer
undoLayers.add(layers.removeLast()); undoLayers.add(layers.removeLast());
setState(() {}); setState(() {});
@ -83,7 +83,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
icon: FaIcon(FontAwesomeIcons.rotateRight, icon: FaIcon(FontAwesomeIcons.rotateRight,
color: undoLayers.isNotEmpty ? Colors.white : Colors.grey, color: undoLayers.isNotEmpty ? Colors.white : Colors.grey,
shadows: iconShadow), shadows: ShareImageEditorView.iconShadow),
onPressed: () { onPressed: () {
if (undoLayers.isEmpty) return; if (undoLayers.isEmpty) return;
@ -132,21 +132,32 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return Scaffold( return Scaffold(
backgroundColor: Colors.white.withAlpha(0), backgroundColor: Colors.white.withAlpha(0),
resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
MediaViewSizing( GestureDetector(
onTap: () {
if (layers.any((x) => x.isEditing)) {
return;
}
undoLayers.clear();
removedLayers.clear();
layers.add(TextLayerData());
setState(() {});
},
child: MediaViewSizing(
SizedBox( SizedBox(
height: currentImage.height / pixelRatio, height: currentImage.height / pixelRatio,
width: currentImage.width / pixelRatio, width: currentImage.width / pixelRatio,
child: Screenshot( child: Screenshot(
controller: screenshotController, controller: screenshotController,
child: LayersViewer( child: LayersViewer(
layers: layers, layers: layers.where((x) => !x.isDeleted).toList(),
onUpdate: () { onUpdate: () {
setState(() {}); setState(() {});
}, },
editable: true, ),
), ),
), ),
), ),
@ -165,17 +176,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
right: 0, right: 0,
top: 100, top: 100,
child: Container( child: Container(
// color: Colors.black45,
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
// height: 86 + MediaQuery.of(context).padding.bottom,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
decoration: const BoxDecoration(
// color: Colors.black87,
// shape: BoxShape.rectangle,
// boxShadow: [
// BoxShadow(blurRadius: 1),
// ],
),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -185,13 +187,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onTap: () async { onTap: () async {
undoLayers.clear(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
layers.add(TextLayerData());
layers.add(
TextLayerData(
text: "Test",
),
);
setState(() {}); setState(() {});
}, },
), ),
@ -346,7 +342,7 @@ class BottomButton extends StatelessWidget {
FaIcon( FaIcon(
icon, icon,
color: Colors.white, color: Colors.white,
shadows: _ShareImageEditorView.iconShadow, shadows: ShareImageEditorView.iconShadow,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],