Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 14.23.0
- **FEAT**(layers): Add `LayerInteraction.bounds`, `minScale` and `maxScale` to keep one layer inside an area and within a scale range.
- **FIX**(import-export): A history step that changes one interaction setting of a layer no longer resets its other settings on import.

## 14.22.0
- **FEAT**(layers): Add `LayerAnimationType.spin` and `LayerAnimationType.flip` (around a `FlipAxis`) to turn a layer by `LayerAnimation.turns`; a `switch` over `LayerAnimationType` must handle the new values.

Expand Down
7 changes: 7 additions & 0 deletions example/lib/features/layer/layer_group_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:material_ui/material_ui.dart';
import '/features/layer/inline_text_example.dart';
import '/features/layer/layer_export_example.dart';
import '/features/layer/layer_grouping_example.dart';
import '/features/layer/layer_limits_example.dart';
import '/features/layer/layer_rasterizer_example.dart';
import '/features/layer/layer_select_design_example.dart';
import '/features/layer/selectable_layer_example.dart';
Expand Down Expand Up @@ -46,6 +47,12 @@ class _LayerGroupPageState extends State<LayerGroupPage> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const SelectableLayerExample()),
),
ListTile(
leading: const Icon(Icons.fit_screen_outlined),
title: const Text('Move and Scale Limits'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const LayerLimitsExample()),
),
ListTile(
leading: const Icon(Icons.crop_free),
title: const Text('Resizable Text Boxes'),
Expand Down
143 changes: 143 additions & 0 deletions example/lib/features/layer/layer_limits_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Flutter imports:
import 'package:material_ui/material_ui.dart';

// Package imports:
import 'package:pro_image_editor/core/models/layers/layer_interaction.dart';
import 'package:pro_image_editor/pro_image_editor.dart';

// Project imports:
import '/core/constants/example_constants.dart';
import '/core/mixin/example_helper.dart';

/// A widget that demonstrates layers that stay inside an area and within a
/// range of sizes.
///
/// The editor opens with an emoji and a card inside a framed area. Drag,
/// pinch or turn them: they stop at the edges of the frame instead of
/// leaving it. The card does not turn and only scales between half and
/// twice its size.
///
/// Example usage:
/// ```dart
/// LayerLimitsExample();
/// ```
class LayerLimitsExample extends StatefulWidget {
/// Creates a new [LayerLimitsExample] widget.
const LayerLimitsExample({super.key});

@override
State<LayerLimitsExample> createState() => _LayerLimitsExampleState();
}

/// The state for the [LayerLimitsExample] widget.
class _LayerLimitsExampleState extends State<LayerLimitsExample>
with ExampleHelperState<LayerLimitsExample> {
@override
void initState() {
super.initState();
preCacheImage(assetPath: kImageEditorExampleAssetPath);
}

/// The area the layers stay inside, in the logical pixels of the editor's
/// body. It is derived from the size of the body, so it follows it when
/// the device turns or the window is resized.
static Rect _safeArea(Size editorBodySize) => Rect.fromLTRB(
editorBodySize.width * 0.1,
editorBodySize.height * 0.15,
editorBodySize.width * 0.9,
editorBodySize.height * 0.75,
);

void _onAfterViewInit() {
editorKey.currentState!
..addLayer(
EmojiLayer(
emoji: '🎯',
offset: const Offset(0, -60),
interaction: LayerInteraction(bounds: _safeArea),
),
)
..addLayer(
WidgetLayer(
offset: const Offset(0, 60),
widget: _buildCard(),
interaction: LayerInteraction(
enableRotate: false,
minScale: 0.5,
maxScale: 2,
bounds: _safeArea,
),
),
);
}

Widget _buildCard() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Stays inside',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
),
);
}

@override
Widget build(BuildContext context) {
if (!isPreCached) return const PrepareImageWidget();

return ProImageEditor.asset(
kImageEditorExampleAssetPath,
key: editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingStarted: onImageEditingStarted,
onImageEditingComplete: onImageEditingComplete,
onCloseEditor: (editorMode) => onCloseEditor(
editorMode: editorMode,
enablePop: !isDesktopMode(context),
),
mainEditorCallbacks: MainEditorCallbacks(
onAfterViewInit: _onAfterViewInit,
helperLines: HelperLinesCallbacks(onLineHit: vibrateLineHit),
),
),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
mainEditor: MainEditorConfigs(
enableCloseButton: !isDesktopMode(context),
widgets: MainEditorWidgets(
bodyItems: (editor, rebuildStream) => [
ReactiveWidget(
stream: rebuildStream,
builder: (_) => editor.isSubEditorOpen
? const SizedBox.shrink()
: _buildSafeAreaFrame(editor.sizesManager.bodySize),
),
],
),
),
),
);
}

/// Outlines [_safeArea], so it shows where the layers stop.
Widget _buildSafeAreaFrame(Size editorBodySize) {
if (editorBodySize.isEmpty || !editorBodySize.isFinite) {
return const SizedBox.shrink();
}
return Positioned.fromRect(
rect: _safeArea(editorBodySize),
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.white54, width: 2),
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
}
11 changes: 6 additions & 5 deletions lib/core/models/layers/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,11 @@ class Layer {
int maxDecimalPlaces = kMaxSafeDecimalPlaces,
bool enableMinify = false,
}) {
// Empty when only [LayerInteraction.bounds] changed, which is not stored.
final interactionChanges = interaction.toMapFromReference(
layer.interaction,
enableMinify: enableMinify,
);
return {
'id': id,
if (layer.offset.dx != offset.dx)
Expand All @@ -687,11 +692,7 @@ class Layer {
if (layer.flipX != flipX) 'flipX': flipX.minify(enableMinify),
if (layer.flipY != flipY) 'flipY': flipY.minify(enableMinify),
if (!mapIsEqual(layer.meta, meta)) 'meta': meta,
if (layer.interaction != interaction)
'interaction': interaction.toMapFromReference(
layer.interaction,
enableMinify: enableMinify,
),
if (interactionChanges.isNotEmpty) 'interaction': interactionChanges,
if (layer.boxConstraints != boxConstraints)
'boxConstraints': boxConstraints!.toMap(
maxDecimalPlaces: maxDecimalPlaces,
Expand Down
Loading
Loading