diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b57e492df9..013cc4b118f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All user visible changes to this project will be documented in this file. This p - UI: - Home page: - Redesigned chats tab. ([#142]) + - Media panel: + - Video resizing when dragged. ([#191], [#190]) ### Fixed @@ -26,6 +28,8 @@ All user visible changes to this project will be documented in this file. This p - Context menu not opening over video previews. ([#198], [#196]) [#142]: /../../pull/142 +[#190]: /../../issues/190 +[#191]: /../../pull/191 [#192]: /../../issues/192 [#193]: /../../pull/193 [#196]: /../../issues/196 diff --git a/lib/ui/page/call/component/desktop.dart b/lib/ui/page/call/component/desktop.dart index b6894ab5a76..77191873b26 100644 --- a/lib/ui/page/call/component/desktop.dart +++ b/lib/ui/page/call/component/desktop.dart @@ -1351,6 +1351,15 @@ Widget _primaryView(CallController c) { }); }, decoratorBuilder: (_) => const ParticipantDecoratorWidget(), + itemConstraints: (_DragData data) { + if (data.participant.video.value != null || + data.participant.user.value?.user.value.callCover != null) { + final double size = (c.size.longestSide * 0.33).clamp(100, 250); + return BoxConstraints(maxWidth: size, maxHeight: size); + } + + return null; + }, itemBuilder: (_DragData data) { var participant = data.participant; return Obx(() { @@ -1849,8 +1858,16 @@ Widget _secondaryView(CallController c, BuildContext context) { ); }); }, - decoratorBuilder: (_DragData item) => - const ParticipantDecoratorWidget(), + decoratorBuilder: (_) => const ParticipantDecoratorWidget(), + itemConstraints: (_DragData data) { + if (data.participant.video.value != null || + data.participant.user.value?.user.value.callCover != null) { + final double size = (c.size.longestSide * 0.33).clamp(100, 250); + return BoxConstraints(maxWidth: size, maxHeight: size); + } + + return null; + }, itemBuilder: (_DragData data) { var participant = data.participant; return Obx( diff --git a/lib/ui/page/call/controller.dart b/lib/ui/page/call/controller.dart index baa6d8ccc69..769bf54c8c8 100644 --- a/lib/ui/page/call/controller.dart +++ b/lib/ui/page/call/controller.dart @@ -1738,6 +1738,8 @@ class CallController extends GetxController { primary.value = focused.isNotEmpty ? focused : [...locals, ...remotes]; secondary.value = focused.isNotEmpty ? [...locals, ...paneled, ...remotes] : paneled; + + applySecondaryConstraints(); } /// Returns all [Participant]s identified by an [id] and [source]. diff --git a/lib/ui/page/call/widget/reorderable_fit.dart b/lib/ui/page/call/widget/reorderable_fit.dart index 283f368f1de..f97c2810bf5 100644 --- a/lib/ui/page/call/widget/reorderable_fit.dart +++ b/lib/ui/page/call/widget/reorderable_fit.dart @@ -20,7 +20,9 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:collection/collection.dart'; import 'package:dough/dough.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '/ui/page/home/widget/gallery_popup.dart'; import 'animated_transition.dart'; @@ -62,6 +64,7 @@ class ReorderableFit extends StatelessWidget { this.useLongPress = false, this.allowEmptyTarget = false, this.allowDraggingLast = true, + this.itemConstraints, }) : super(key: key); /// Builder building the provided item. @@ -92,6 +95,9 @@ class ReorderableFit extends StatelessWidget { /// Size of a divider between [children]. final double dividerSize; + /// Callback, specifying a [BoxConstraints] of an item when it's dragged. + final BoxConstraints? Function(T)? itemConstraints; + /// Callback, called when an item is reordered. final Function(T, int)? onReorder; @@ -374,6 +380,7 @@ class ReorderableFit extends StatelessWidget { onOffset: onOffset, useLongPress: useLongPress, allowDraggingLast: allowDraggingLast, + itemConstraints: itemConstraints, ); }), ); @@ -413,6 +420,7 @@ class _ReorderableFit extends StatefulWidget { this.onOffset, this.useLongPress = false, this.allowDraggingLast = true, + this.itemConstraints, }) : super(key: key); /// Builder building the provided item. @@ -439,6 +447,9 @@ class _ReorderableFit extends StatefulWidget { /// Size of a divider between [children]. final double dividerSize; + /// Callback, specifying a [BoxConstraints] of an item when it's dragged. + final BoxConstraints? Function(T)? itemConstraints; + /// Callback, called when an item is reordered. final Function(T, int)? onReorder; @@ -579,7 +590,7 @@ class _ReorderableFitState extends State<_ReorderableFit> { if (widget.decoratorBuilder != null) widget.decoratorBuilder!.call(item.item), KeyedSubtree( - key: item.key, + key: item.cellKey, child: item.entry != null ? SizedBox( width: widget.wrapSize, @@ -587,8 +598,13 @@ class _ReorderableFitState extends State<_ReorderableFit> { ) : _ReorderableDraggable( item: item.item, - itemBuilder: widget.itemBuilder, + itemBuilder: (o) => KeyedSubtree( + key: item.itemKey, + child: widget.itemBuilder(o), + ), + itemConstraints: widget.itemConstraints, useLongPress: widget.useLongPress, + cellKey: item.cellKey, sharedKey: item.sharedKey, enabled: _items.map((e) => e.entry).whereNotNull().isEmpty && @@ -601,7 +617,7 @@ class _ReorderableFitState extends State<_ReorderableFit> { } }, onDragStarted: () { - item.dragStartedRect = item.key.globalPaintBounds; + item.dragStartedRect = item.cellKey.globalPaintBounds; widget.onDragStarted?.call(item.item); }, onDragCompleted: () => @@ -832,8 +848,8 @@ class _ReorderableFitState extends State<_ReorderableFit> { var from = _items[i]; var to = _items[index]; - var beginRect = from.key.globalPaintBounds!; - var endRect = to.key.globalPaintBounds!; + Rect beginRect = from.cellKey.globalPaintBounds!; + Rect endRect = to.cellKey.globalPaintBounds!; if (beginRect != endRect) { Offset offset = widget.onOffset?.call() ?? Offset.zero; @@ -875,15 +891,10 @@ class _ReorderableFitState extends State<_ReorderableFit> { void _animateReturn(_ReorderableItem to, Offset d) { if (to.dragStartedRect == null) return; - var beginRect = to.dragStartedRect ?? to.key.globalPaintBounds!; - var endRect = to.key.globalPaintBounds!; - - beginRect = Rect.fromLTRB( - d.dx, - d.dy, - (d.dx - beginRect.left) + beginRect.right, - (d.dy - beginRect.top) + beginRect.bottom, - ); + Rect beginRect = to.itemKey.globalPaintBounds ?? + to.dragStartedRect ?? + to.cellKey.globalPaintBounds!; + Rect endRect = to.cellKey.globalPaintBounds!; if (beginRect != endRect) { Offset offset = widget.onOffset?.call() ?? Offset.zero; @@ -930,6 +941,7 @@ class _ReorderableDraggable extends StatefulWidget { Key? key, required this.item, required this.sharedKey, + required this.cellKey, required this.itemBuilder, this.onDragEnd, this.onDragStarted, @@ -938,11 +950,15 @@ class _ReorderableDraggable extends StatefulWidget { this.onDoughBreak, this.useLongPress = false, this.enabled = true, + this.itemConstraints, }) : super(key: key); /// Item stored in this [_ReorderableDraggable]. final T item; + /// [GlobalKey] of a cell this [_ReorderableDraggable] occupies. + final GlobalKey cellKey; + /// [UniqueKey] of this [_ReorderableDraggable]. final UniqueKey sharedKey; @@ -971,6 +987,10 @@ class _ReorderableDraggable extends StatefulWidget { /// Indicator whether dragging is allowed. final bool enabled; + /// Callback, specifying a [BoxConstraints] of this [_ReorderableDraggable] + /// when it's dragged. + final BoxConstraints? Function(T)? itemConstraints; + @override State<_ReorderableDraggable> createState() => _ReorderableDraggableState(); @@ -980,7 +1000,15 @@ class _ReorderableDraggable extends StatefulWidget { class _ReorderableDraggableState extends State<_ReorderableDraggable> { /// Indicator whether this [_ReorderableDraggable] is dragged. - bool isDragged = false; + bool _isDragged = false; + + /// Reactive [Offset] of an anchor of this [_ReorderableDraggable] when it's + /// dragged. + final Rx _position = Rx(Offset.zero); + + /// Reactive [BoxConstraints] the [_ReorderableDraggable.item] should occupy + /// passed to a [_Resizable] to animate its changes. + final Rx _constraints = Rx(null); @override Widget build(BuildContext context) { @@ -995,35 +1023,75 @@ class _ReorderableDraggableState ), child: LayoutBuilder( builder: (context, constraints) { - var child = widget.itemBuilder(widget.item); + final Widget child = widget.itemBuilder(widget.item); + return DraggableDough( data: widget.item, longPress: widget.useLongPress, maxSimultaneousDrags: widget.enabled ? 1 : 0, onDragEnd: (d) { widget.onDragEnd?.call(d.offset); - isDragged = false; + _isDragged = false; }, onDragStarted: () { + _constraints.value = constraints; widget.onDragStarted?.call(); HapticFeedback.lightImpact(); - isDragged = true; + _isDragged = true; + }, + dragAnchorStrategy: ( + Draggable draggable, + BuildContext context, + Offset position, + ) { + _position.value = position; + final RenderBox renderObject = + context.findRenderObject()! as RenderBox; + return renderObject.globalToLocal(position); + }, + onDragCompleted: () { + widget.onDragCompleted?.call(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _constraints.value = constraints; + }); + }, + onDraggableCanceled: (_, d) { + widget.onDraggableCanceled?.call(d); + SchedulerBinding.instance.addPostFrameCallback((_) { + _constraints.value = constraints; + }); }, - onDragCompleted: widget.onDragCompleted, - onDraggableCanceled: (_, d) => widget.onDraggableCanceled?.call(d), onDoughBreak: () { - if (widget.enabled && isDragged) { + if (widget.enabled && _isDragged) { widget.onDoughBreak?.call(); + + final BoxConstraints? itemConstraints = + widget.itemConstraints?.call(widget.item); + + if (itemConstraints != null && + itemConstraints.biggest.longestSide < + constraints.biggest.longestSide) { + final double coefficient = constraints.biggest.longestSide / + itemConstraints.biggest.longestSide; + + _constraints.value = BoxConstraints( + maxWidth: constraints.maxWidth / coefficient, + maxHeight: constraints.maxHeight / coefficient, + ); + } else { + _constraints.value = constraints; + } + HapticFeedback.lightImpact(); } }, - feedback: SizedBox( - width: constraints.maxWidth, - height: constraints.maxHeight, - child: KeyedSubtree( - key: widget.sharedKey, - child: child, - ), + feedback: _Resizable( + key: widget.sharedKey, + cellKey: widget.cellKey, + layout: constraints, + position: _position, + constraints: _constraints, + child: child, ), childWhenDragging: KeyedSubtree( key: widget.sharedKey, @@ -1044,6 +1112,64 @@ class _ReorderableDraggableState } } +/// [Widget] animating its size changes from the provided [layout] to the +/// specified reactive [constraints]. +class _Resizable extends StatelessWidget { + const _Resizable({ + Key? key, + required this.cellKey, + required this.layout, + required this.position, + required this.constraints, + required this.child, + }) : super(key: key); + + /// [GlobalKey] of a cell this [_Resizable] occupies. + final GlobalKey cellKey; + + /// Initial [BoxConstraints] of this [_Resizable]. + final BoxConstraints layout; + + /// [Offset] position of a drag anchor. + final Rx position; + + /// Target [BoxConstraints] of this [_Resizable] to occupy. + final Rx constraints; + + /// [Widget] to animate. + final Widget child; + + @override + Widget build(BuildContext context) { + return Obx(() { + Offset offset = Offset.zero; + if (position.value != null && constraints.value != layout) { + final Rect delta = cellKey.globalPaintBounds ?? Rect.zero; + final Offset position = Offset( + this.position.value!.dx - delta.left, + this.position.value!.dy - delta.top, + ); + + offset = Offset( + position.dx - + (constraints.value!.maxWidth * position.dx / layout.maxWidth), + position.dy - + (constraints.value!.maxHeight * position.dy / layout.maxHeight), + ); + } + + return AnimatedContainer( + duration: 300.milliseconds, + curve: Curves.ease, + transform: Matrix4.translationValues(offset.dx, offset.dy, 0), + width: constraints.value?.maxWidth, + height: constraints.value?.maxHeight, + child: child, + ); + }); + } +} + /// Data of an [Object] used in a [_ReorderableFit] to be reordered around. class _ReorderableItem { _ReorderableItem(this.item); @@ -1051,9 +1177,11 @@ class _ReorderableItem { /// Reorderable [Object] itself. final T item; - /// [GlobalKey] of this [_ReorderableItem] representing the global position of - /// this [item]. - final GlobalKey key = GlobalKey(); + /// [GlobalKey] of a cell this [_ReorderableItem] occupies. + final GlobalKey cellKey = GlobalKey(); + + /// [GlobalKey] of an [item] this [_ReorderableItem] builds. + final GlobalKey itemKey = GlobalKey(); /// [UniqueKey] of this [_ReorderableItem] representing the position in a /// [_ReorderableFit] of this [item]. diff --git a/test/e2e/steps/wait_until_widget.dart b/test/e2e/steps/wait_until_widget.dart index f108e0fb478..f1d15475ad6 100644 --- a/test/e2e/steps/wait_until_widget.dart +++ b/test/e2e/steps/wait_until_widget.dart @@ -42,6 +42,7 @@ final StepDefinitionGeneric waitUntilKeyExists = context.world.appDriver.findByKeySkipOffstage(key.name), ); }, + timeout: const Duration(seconds: 30), ); }, );