diff --git a/example/lib/main.dart b/example/lib/main.dart index 3400ddb..ac1c672 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,6 +11,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'Resizable Widget Example', theme: ThemeData.dark(), home: const MyPage(), @@ -18,48 +19,97 @@ class MyApp extends StatelessWidget { } } -class MyPage extends StatelessWidget { +class MyPage extends StatefulWidget { const MyPage({Key? key}) : super(key: key); + @override + State createState() => _MyPageState(); +} + +class _MyPageState extends State { + var textDirection = TextDirection.rtl; + @override Widget build(BuildContext context) { + final rtl = textDirection == TextDirection.rtl; return Scaffold( appBar: AppBar( - title: const Text('Resizable Widget Example'), - ), - body: ResizableWidget( - isHorizontalSeparator: false, - isDisabledSmartHide: false, - separatorColor: Colors.white12, - separatorSize: 4, - onResized: _printResizeInfo, - children: [ - Container(color: Colors.greenAccent), - ResizableWidget( - isHorizontalSeparator: true, - separatorColor: Colors.blue, - separatorSize: 10, - children: [ - Container(color: Colors.greenAccent), - ResizableWidget( - children: [ - Container(color: Colors.greenAccent), - Container(color: Colors.yellowAccent), - Container(color: Colors.redAccent), - ], - percentages: const [0.2, 0.5, 0.3], - ), - Container(color: Colors.redAccent), - ], + title: Text('Resizable Widget Example (${rtl ? 'Right-to-Left' : 'Left-to-Right'})'), + actions: [ + IconButton( + icon: Icon(rtl ? Icons.subdirectory_arrow_left : Icons.subdirectory_arrow_right), + onPressed: () { + setState(() => textDirection = rtl ? TextDirection.ltr : TextDirection.rtl); + }, ), - Container(color: Colors.redAccent), ], ), + body: Directionality( + textDirection: textDirection, + child: ResizableWidget( + isHorizontalSeparator: false, + isDisabledSmartHide: false, + separatorColor: Colors.white12, + separatorSize: 4, + onResized: _printResizeInfo, + constraints: const [ + BoxConstraints(minWidth: 100), + null, + null, + ], + children: [ + Container( + color: Colors.green, + child: const Center( + child: Text('Min Width is 100', style: TextStyle(color: Colors.black)), + ), + ), + ResizableWidget( + isHorizontalSeparator: true, + separatorColor: Colors.blue, + separatorSize: 10, + constraints: const [ + BoxConstraints(minHeight: 200), + BoxConstraints(minHeight: 100, maxHeight: 250), + null, + ], + children: [ + Container( + color: Colors.amber, + child: const Center( + child: Text('Min Height is 200', style: TextStyle(color: Colors.black)), + ), + ), + ResizableWidget( + constraints: const [ + null, + BoxConstraints(minHeight: 150, maxHeight: 200), + null, + ], + children: [ + Container(color: Colors.greenAccent), + Container( + color: Colors.yellowAccent, + child: const Center( + child: Text('Min Height is 100\nMax Height is 250', style: TextStyle(color: Colors.black)), + ), + ), + Container(color: Colors.redAccent), + ], + percentages: const [0.2, 0.5, 0.3], + ), + Container(color: Colors.redAccent), + ], + ), + Container(color: Colors.redAccent), + ], + ), + ), ); } void _printResizeInfo(List dataList) { // ignore: avoid_print - print(dataList.map((x) => '(${x.size}, ${x.percentage}%)').join(", ")); + // print(dataList.map((x) => '(${x.size}, ${x.percentage}%)').join(", ")); } } diff --git a/lib/src/resizable_widget.dart b/lib/src/resizable_widget.dart index c1ecc43..655df2b 100644 --- a/lib/src/resizable_widget.dart +++ b/lib/src/resizable_widget.dart @@ -23,6 +23,19 @@ class ResizableWidget extends StatefulWidget { /// If this value is [null], [children] will be split into the same size. final List? percentages; + /// Constraints among the [ResizableWidget] children. + /// + /// Used to force the widget to doesn't break constraints sizes. + /// + /// Sets the default [children] constraints sizes. + /// + /// If you set this value, + /// the length of [constraints] must match the one of [children]. + /// If you don't want to set constraints to one child, just set it to [null] or [BoxConstraints()]. + /// + /// If this value is [null], [children] will be resized without any constraints. + final List? constraints; + /// When set to true, creates horizontal separators. @Deprecated('Use [isHorizontalSeparator] instead') final bool isColumnChildren; @@ -53,8 +66,8 @@ class ResizableWidget extends StatefulWidget { Key? key, required this.children, this.percentages, - @Deprecated('Use [isHorizontalSeparator] instead') - this.isColumnChildren = false, + this.constraints, + @Deprecated('Use [isHorizontalSeparator] instead') this.isColumnChildren = false, this.isHorizontalSeparator = false, this.isDisabledSmartHide = false, this.separatorSize = 4, @@ -63,8 +76,8 @@ class ResizableWidget extends StatefulWidget { }) : super(key: key) { assert(children.isNotEmpty); assert(percentages == null || percentages!.length == children.length); - assert(percentages == null || - percentages!.reduce((value, element) => value + element) == 1); + assert(constraints == null || constraints!.length == children.length); + assert(percentages == null || percentages!.reduce((value, element) => value + element) == 1); } @override @@ -85,27 +98,29 @@ class _ResizableWidgetState extends State { @override Widget build(BuildContext context) => LayoutBuilder( - builder: (context, constraints) { - _controller.setSizeIfNeeded(constraints); - return StreamBuilder( - stream: _controller.eventStream.stream, - builder: (context, snapshot) => _info.isHorizontalSeparator - ? Column( - children: _controller.children.map(_buildChild).toList()) - : Row(children: _controller.children.map(_buildChild).toList()), - ); - }, + builder: (context, constraints) { + _controller.setSizeIfNeeded(constraints); + return StreamBuilder( + stream: _controller.eventStream.stream, + builder: (context, snapshot) => _info.isHorizontalSeparator + ? Column(children: _controller.children.map(_buildChild).toList()) + : Row(children: _controller.children.map(_buildChild).toList()), ); + }, + ); Widget _buildChild(ResizableWidgetChildData child) { if (child.widget is Separator) { return child.widget; } - return SizedBox( - width: _info.isHorizontalSeparator ? double.infinity : child.size, - height: _info.isHorizontalSeparator ? child.size : double.infinity, - child: child.widget, + return ConstrainedBox( + constraints: child.constraints ?? const BoxConstraints(), + child: SizedBox( + width: _info.isHorizontalSeparator ? double.infinity : child.size, + height: _info.isHorizontalSeparator ? child.size : double.infinity, + child: child.widget, + ), ); } } diff --git a/lib/src/resizable_widget_args_info.dart b/lib/src/resizable_widget_args_info.dart index 2e43b6b..921ae83 100644 --- a/lib/src/resizable_widget_args_info.dart +++ b/lib/src/resizable_widget_args_info.dart @@ -4,6 +4,7 @@ import 'resizable_widget.dart'; class ResizableWidgetArgsInfo { final List children; final List? percentages; + final List? constraints; final bool isHorizontalSeparator; final bool isDisabledSmartHide; final double separatorSize; @@ -13,6 +14,7 @@ class ResizableWidgetArgsInfo { ResizableWidgetArgsInfo(ResizableWidget widget) : children = widget.children, percentages = widget.percentages, + constraints = widget.constraints, isHorizontalSeparator = // TODO: delete the deprecated member on the next minor update. // ignore: deprecated_member_use_from_same_package diff --git a/lib/src/resizable_widget_child_data.dart b/lib/src/resizable_widget_child_data.dart index f504b87..d750dec 100644 --- a/lib/src/resizable_widget_child_data.dart +++ b/lib/src/resizable_widget_child_data.dart @@ -4,7 +4,8 @@ class ResizableWidgetChildData { final Widget widget; double? size; double? percentage; + BoxConstraints? constraints; double? defaultPercentage; double? hidingPercentage; - ResizableWidgetChildData(this.widget, this.percentage); + ResizableWidgetChildData(this.widget, this.percentage, this.constraints); } diff --git a/lib/src/resizable_widget_controller.dart b/lib/src/resizable_widget_controller.dart index f0f86a3..f656a50 100644 --- a/lib/src/resizable_widget_controller.dart +++ b/lib/src/resizable_widget_controller.dart @@ -21,15 +21,15 @@ class ResizableWidgetController { _model.callOnResized(); } - void resize(int separatorIndex, Offset offset) { - _model.resize(separatorIndex, offset); + void resize(BuildContext context, int separatorIndex, Offset offset) { + _model.resize(context, separatorIndex, offset); eventStream.add(this); _model.callOnResized(); } - void tryHideOrShow(int separatorIndex) { - final result = _model.tryHideOrShow(separatorIndex); + void tryHideOrShow(BuildContext context, int separatorIndex) { + final result = _model.tryHideOrShow(context, separatorIndex); if (result) { eventStream.add(this); diff --git a/lib/src/resizable_widget_model.dart b/lib/src/resizable_widget_model.dart index 3971178..ed96d60 100644 --- a/lib/src/resizable_widget_model.dart +++ b/lib/src/resizable_widget_model.dart @@ -11,95 +11,156 @@ class ResizableWidgetModel { final ResizableWidgetArgsInfo _info; final children = []; double? maxSize; - double? get maxSizeWithoutSeparators => maxSize == null - ? null - : maxSize! - (children.length ~/ 2) * _info.separatorSize; + + double? get maxSizeWithoutSeparators => maxSize == null ? null : maxSize! - (children.length ~/ 2) * _info.separatorSize; ResizableWidgetModel(this._info); void init(SeparatorFactory separatorFactory) { final originalChildren = _info.children; final size = originalChildren.length; - final originalPercentages = - _info.percentages ?? List.filled(size, 1 / size); - for (var i = 0; i < size - 1; i++) { - children.add(ResizableWidgetChildData( - originalChildren[i], originalPercentages[i])); - children.add(ResizableWidgetChildData( - separatorFactory.call(SeparatorArgsBasicInfo( - 2 * i + 1, - _info.isHorizontalSeparator, - _info.isDisabledSmartHide, - _info.separatorSize, - _info.separatorColor, - )), - null)); + final originalPercentages = _info.percentages ?? List.filled(size, 1 / size); + final originalConstraints = _info.constraints; + for (var index = 0; index < size - 1; index++) { + children.add( + ResizableWidgetChildData( + originalChildren[index], + originalPercentages[index], + originalConstraints?.elementAt(index), + ), + ); + final separatorWidget = separatorFactory.call( + SeparatorArgsBasicInfo( + 2 * index + 1, + _info.isHorizontalSeparator, + _info.isDisabledSmartHide, + _info.separatorSize, + _info.separatorColor, + ), + ); + children.add( + ResizableWidgetChildData(separatorWidget, null, null), + ); } - children.add(ResizableWidgetChildData( - originalChildren[size - 1], originalPercentages[size - 1])); + + // Last widget + children.add( + ResizableWidgetChildData( + originalChildren[size - 1], + originalPercentages[size - 1], + originalConstraints?.elementAt(size - 1), + ), + ); } void setSizeIfNeeded(BoxConstraints constraints) { - final max = _info.isHorizontalSeparator - ? constraints.maxHeight - : constraints.maxWidth; - var isMaxSizeChanged = maxSize == null || maxSize! != max; - if (!isMaxSizeChanged || children.isEmpty) { - return; - } + final max = _info.isHorizontalSeparator ? constraints.maxHeight : constraints.maxWidth; + if (maxSize != null && maxSize == max) return; maxSize = max; final remain = maxSizeWithoutSeparators!; - for (var c in children) { - if (c.widget is Separator) { - c.percentage = 0; - c.size = _info.separatorSize; + for (var index = 0; index < children.length; index++) { + if (children[index].widget is Separator) { + children[index].percentage = 0; + children[index].size = _info.separatorSize; } else { - c.size = remain * c.percentage!; - c.defaultPercentage = c.percentage; + children[index].size = remain * children[index].percentage!; + children[index].defaultPercentage = children[index].percentage; } } + + for (var index = 1; index < children.length - 1; index += 2) { + final originalSize = (children[index - 1].size ?? 0) + (children[index + 1].size ?? 0); + _updateSizes(index - 1, index + 1, 0, originalSize); + } } - void resize(int separatorIndex, Offset offset) { - final leftSize = _resizeImpl(separatorIndex - 1, offset); - final rightSize = _resizeImpl(separatorIndex + 1, offset * (-1)); - - if (leftSize < 0) { - _resizeImpl( - separatorIndex - 1, - _info.isHorizontalSeparator - ? Offset(0, -leftSize) - : Offset(-leftSize, 0)); - _resizeImpl( - separatorIndex + 1, - _info.isHorizontalSeparator - ? Offset(0, leftSize) - : Offset(leftSize, 0)); + void resize(BuildContext context, int separatorIndex, Offset offset) { + if (!_info.isHorizontalSeparator && Directionality.of(context) == TextDirection.rtl) { + offset = offset * -1; } - if (rightSize < 0) { - _resizeImpl( - separatorIndex - 1, - _info.isHorizontalSeparator - ? Offset(0, rightSize) - : Offset(rightSize, 0)); - _resizeImpl( - separatorIndex + 1, - _info.isHorizontalSeparator - ? Offset(0, -rightSize) - : Offset(-rightSize, 0)); + + var delta = _info.isHorizontalSeparator ? offset.dy : offset.dx; + final originalSize = (children[separatorIndex - 1].size ?? 0) + (children[separatorIndex + 1].size ?? 0); + + _updateSizes(separatorIndex - 1, separatorIndex + 1, delta, originalSize); + } + + void _updateSizes(int index1, int index2, double delta, double originalSize) { + final childData1 = children[index1]; + final childData2 = children[index2]; + final constraints1 = childData1.constraints ?? const BoxConstraints(); + final constraints2 = childData2.constraints ?? const BoxConstraints(); + + var size1 = (childData1.size ?? 0) + delta; + var size2 = (childData2.size ?? 0) - delta; + + if (_info.isHorizontalSeparator) { + if (delta < 0) { + while (size1 < constraints1.minHeight || size2 > constraints2.maxHeight) { + if (size1 < constraints1.minHeight) { + size1 = constraints1.minHeight; + size2 = originalSize - size1; + } + if (size2 > constraints2.maxHeight) { + size2 = constraints2.maxHeight; + size1 = originalSize - size2; + } + } + } else { + while (size1 > constraints1.maxHeight || size2 < constraints2.minHeight) { + if (size1 > constraints1.maxHeight) { + size1 = constraints1.maxHeight; + size2 = originalSize - size1; + } + if (size2 < constraints2.minHeight) { + size2 = constraints2.minHeight; + size1 = originalSize - size2; + } + } + } + } else { + if (delta < 0) { + while (size1 < constraints1.minWidth || size2 > constraints2.maxWidth) { + if (size1 < constraints1.minWidth) { + size1 = constraints1.minWidth; + size2 = originalSize - size1; + } + if (size2 > constraints2.maxWidth) { + size2 = constraints2.maxWidth; + size1 = originalSize - size2; + } + } + } else { + while (size1 > constraints1.maxWidth || size2 < constraints2.minWidth) { + if (size1 > constraints1.maxWidth) { + size1 = constraints1.maxWidth; + size2 = originalSize - size1; + } + if (size2 < constraints2.minWidth) { + size2 = constraints2.minWidth; + size1 = originalSize - size2; + } + } + } } + + _resizeImpl(childData1, size1); + _resizeImpl(childData2, size2); } + void callOnResized() { - _info.onResized?.call(children - .where((x) => x.widget is! Separator) - .map((x) => WidgetSizeInfo(x.size!, x.percentage!)) - .toList()); + _info.onResized?.call( + children + .where((widgetData) => widgetData.widget is! Separator) + .map((widgetData) => WidgetSizeInfo(widgetData.size!, widgetData.percentage!, widgetData.constraints)) + .toList(), + ); } - bool tryHideOrShow(int separatorIndex) { + bool tryHideOrShow(BuildContext context, int separatorIndex) { if (_info.isDisabledSmartHide) { return false; } @@ -116,33 +177,24 @@ class ResizableWidgetModel { final coefficient = isLeft ? 1 : -1; if (_isNearlyZero(size)) { // show - final offsetScala = - maxSize! * (target.hidingPercentage ?? target.defaultPercentage!) - - size; - final offset = _info.isHorizontalSeparator - ? Offset(0, offsetScala * coefficient) - : Offset(offsetScala * coefficient, 0); - resize(separatorIndex, offset); + final offsetScala = maxSize! * (target.hidingPercentage ?? target.defaultPercentage!) - size; + final offset = _info.isHorizontalSeparator ? Offset(0, offsetScala * coefficient) : Offset(offsetScala * coefficient, 0); + resize(context, separatorIndex, offset); } else { // hide target.hidingPercentage = target.percentage!; final offsetScala = maxSize! * target.hidingPercentage!; - final offset = _info.isHorizontalSeparator - ? Offset(0, -offsetScala * coefficient) - : Offset(-offsetScala * coefficient, 0); - resize(separatorIndex, offset); + final offset = _info.isHorizontalSeparator ? Offset(0, -offsetScala * coefficient) : Offset(-offsetScala * coefficient, 0); + resize(context, separatorIndex, offset); } return true; } - double _resizeImpl(int widgetIndex, Offset offset) { - final size = children[widgetIndex].size ?? 0; - children[widgetIndex].size = - size + (_info.isHorizontalSeparator ? offset.dy : offset.dx); - children[widgetIndex].percentage = - children[widgetIndex].size! / maxSizeWithoutSeparators!; - return children[widgetIndex].size!; + double _resizeImpl(ResizableWidgetChildData childData, double newSize) { + childData.size = newSize; + childData.percentage = childData.size! / maxSizeWithoutSeparators!; + return childData.size!; } bool _isNearlyZero(double size) { diff --git a/lib/src/separator.dart b/lib/src/separator.dart index 210d465..caa75aa 100644 --- a/lib/src/separator.dart +++ b/lib/src/separator.dart @@ -39,7 +39,7 @@ class _SeparatorState extends State { height: _info.isHorizontalSeparator ? _info.size : double.infinity, ), ), - onPanUpdate: (details) => _controller.onPanUpdate(details, context), - onDoubleTap: () => _controller.onDoubleTap(), + onPanUpdate: (details) => _controller.onPanUpdate(context, details), + onDoubleTap: () => _controller.onDoubleTap(context), ); } diff --git a/lib/src/separator_controller.dart b/lib/src/separator_controller.dart index 8dd0579..c388f34 100644 --- a/lib/src/separator_controller.dart +++ b/lib/src/separator_controller.dart @@ -7,11 +7,11 @@ class SeparatorController { const SeparatorController(this._index, this._parentController); - void onPanUpdate(DragUpdateDetails details, BuildContext context) { - _parentController.resize(_index, details.delta); + void onPanUpdate(BuildContext context, DragUpdateDetails details) { + _parentController.resize(context, _index, details.delta); } - void onDoubleTap() { - _parentController.tryHideOrShow(_index); + void onDoubleTap(BuildContext context) { + _parentController.tryHideOrShow(context, _index); } } diff --git a/lib/src/widget_size_info.dart b/lib/src/widget_size_info.dart index 751bdb7..ca574dc 100644 --- a/lib/src/widget_size_info.dart +++ b/lib/src/widget_size_info.dart @@ -1,3 +1,5 @@ +import 'package:flutter/widgets.dart'; + /// Information about an internal widget size of [ResizableWidget]. class WidgetSizeInfo { /// The actual pixel size. @@ -11,6 +13,11 @@ class WidgetSizeInfo { /// because the ratio of the internal widgets will be maintained. final double percentage; + /// Constraints among the [ResizableWidget] children. + /// + /// Used to force the widget to doesn't break constraints sizes. + final BoxConstraints? constraints; + /// Creates [WidgetSizeInfo]. - const WidgetSizeInfo(this.size, this.percentage); + const WidgetSizeInfo(this.size, this.percentage, this.constraints); }