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 {
Offset offset;
double rotation, scale, opacity;
bool isEditing;
bool isDeleted;
Layer({
this.offset = const Offset(64, 64),
this.offset = const Offset(0, 0),
this.opacity = 1,
this.isEditing = false,
this.isDeleted = false,
this.rotation = 0,
this.scale = 1,
});
@ -35,6 +39,7 @@ class EmojiLayerData extends Layer {
super.opacity,
super.rotation,
super.scale,
super.isEditing,
});
}
@ -50,40 +55,19 @@ class ImageLayerData extends Layer {
super.opacity,
super.rotation,
super.scale,
super.isEditing,
});
}
/// Attributes used by [TextLayer]
class TextLayerData extends Layer {
String text;
TextLayerData({
required 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,
this.text = "",
super.offset,
super.opacity,
super.rotation,
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 {
final BackgroundLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const BackgroundLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override

View file

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

View file

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

View file

@ -1,17 +1,17 @@
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/views/share_image_editor_view.dart';
/// Text layer
class TextLayer extends StatefulWidget {
final TextLayerData layerData;
final VoidCallback? onUpdate;
final bool editable;
const TextLayer({
super.key,
required this.layerData,
this.onUpdate,
this.editable = false,
});
@override
createState() => _TextViewState();
@ -19,41 +19,144 @@ class TextLayer extends StatefulWidget {
class _TextViewState extends State<TextLayer> {
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
Widget build(BuildContext context) {
return Positioned(
left: 0,
right: 0,
top: widget.layerData.offset.dy,
child: GestureDetector(
onTap: widget.editable ? () {} : null,
onScaleUpdate: widget.editable
? (detail) {
if (detail.pointerCount == 1) {
widget.layerData.offset = Offset(
0,
widget.layerData.offset.dy + detail.focalPointDelta.dy,
);
}
setState(() {});
}
: null,
if (isDeleted) return Container();
if (widget.layerData.isEditing) {
return Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom - 100,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(100),
),
child: Text(
widget.layerData.text.toString(),
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: 20,
fontSize: 17,
),
),
),
),
);
}
return Stack(
key: _widgetKey,
children: [
Positioned(
left: 0,
right: 0,
top: widget.layerData.offset.dy,
child: GestureDetector(
onScaleStart: (d) {
setState(() {
elementIsScaled = true;
});
},
onScaleEnd: (d) {
if (deleteLayer) isDeleted = true;
elementIsScaled = false;
setState(() {});
},
onTap: () {
setState(() {
widget.layerData.isEditing = true;
});
},
onScaleUpdate: (detail) {
if (detail.pointerCount == 1) {
widget.layerData.offset = Offset(
0, 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(() {});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withAlpha(100),
),
child: Text(
widget.layerData.text,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20,
),
),
),
),
),
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 {
final List<Layer> layers;
final Function()? onUpdate;
final bool editable;
const LayersViewer({
super.key,
required this.layers,
required this.editable,
this.onUpdate,
});
@ -28,7 +26,6 @@ class LayersViewer extends StatelessWidget {
return BackgroundLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
@ -37,7 +34,6 @@ class LayersViewer extends StatelessWidget {
return ImageLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
@ -46,7 +42,6 @@ class LayersViewer extends StatelessWidget {
return EmojiLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}
@ -55,7 +50,6 @@ class LayersViewer extends StatelessWidget {
return TextLayer(
layerData: layerItem,
onUpdate: onUpdate,
editable: editable,
);
}

View file

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