From 03fbd7060739516d25c238c7d4116a678e7d1531 Mon Sep 17 00:00:00 2001 From: Ankit Bhankharia <71999020+cupcake08@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:32:06 +0530 Subject: [PATCH] refactor: replace HorizontalClipListView with ResponsiveWrapView (#2821) --- app/lib/pages/tabs/send_tab.dart | 4 +- app/lib/widget/horizontal_clip_list_view.dart | 144 ------------------ app/lib/widget/responsive_wrap_view.dart | 76 +++++++++ 3 files changed, 78 insertions(+), 146 deletions(-) delete mode 100644 app/lib/widget/horizontal_clip_list_view.dart create mode 100644 app/lib/widget/responsive_wrap_view.dart diff --git a/app/lib/pages/tabs/send_tab.dart b/app/lib/pages/tabs/send_tab.dart index 07b94857..5a260f08 100644 --- a/app/lib/pages/tabs/send_tab.dart +++ b/app/lib/pages/tabs/send_tab.dart @@ -25,7 +25,7 @@ import 'package:localsend_app/widget/custom_icon_button.dart'; import 'package:localsend_app/widget/dialogs/add_file_dialog.dart'; import 'package:localsend_app/widget/dialogs/send_mode_help_dialog.dart'; import 'package:localsend_app/widget/file_thumbnail.dart'; -import 'package:localsend_app/widget/horizontal_clip_list_view.dart'; +import 'package:localsend_app/widget/responsive_wrap_view.dart'; import 'package:localsend_app/widget/list_tile/device_list_tile.dart'; import 'package:localsend_app/widget/list_tile/device_placeholder_list_tile.dart'; import 'package:localsend_app/widget/opacity_slideshow.dart'; @@ -64,7 +64,7 @@ class SendTab extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), ), - HorizontalClipListView( + ResponsiveWrapView( outerHorizontalPadding: 15, outerVerticalPadding: 10, childPadding: 10, diff --git a/app/lib/widget/horizontal_clip_list_view.dart b/app/lib/widget/horizontal_clip_list_view.dart deleted file mode 100644 index f4ef7d16..00000000 --- a/app/lib/widget/horizontal_clip_list_view.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:flutter/material.dart'; - -/// A horizontal list that adjusts the width if the screen is too small. -/// In this case, the width increases until 10% - 50% of the next button is visible. -/// This is useful to communicate to the user that there are more buttons to the right. -class HorizontalClipListView extends StatelessWidget { - final double outerHorizontalPadding; - final double outerVerticalPadding; - final double childPadding; - final double minChildWidth; - final List children; - - const HorizontalClipListView({ - super.key, - required this.outerHorizontalPadding, - required this.outerVerticalPadding, - required this.childPadding, - required this.minChildWidth, - required this.children, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final childWidth = _calcOptimalButtonWidth( - availableWidth: constraints.maxWidth, - paddingLeft: outerHorizontalPadding, - childrenCount: children.length, - minChildWidth: minChildWidth, - childPadding: childPadding, - ); - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: outerHorizontalPadding, - vertical: outerVerticalPadding, - ), - child: Row( - children: [ - for (int i = 0; i < children.length; i++) - i == children.length - 1 - ? SizedBox( - width: childWidth, - child: children[i], - ) - : Padding( - padding: EdgeInsetsDirectional.only(end: childPadding), - child: SizedBox( - width: childWidth, - child: children[i], - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -double _calcOptimalButtonWidth({ - required double availableWidth, - required double paddingLeft, - required int childrenCount, - required double minChildWidth, - required double childPadding, -}) { - int childWidth = minChildWidth.toInt(); - while (true) { - if (_fitsOnScreen( - availableWidth: availableWidth, - paddingLeft: paddingLeft, - childrenCount: childrenCount, - childWidth: childWidth.toDouble(), - childPadding: childPadding, - ) || - _fitsPartially( - availableWidth: availableWidth, - paddingLeft: paddingLeft, - childrenCount: childrenCount, - childWidth: childWidth.toDouble(), - childPadding: childPadding, - )) { - return childWidth.toDouble(); - } - - childWidth++; - } -} - -bool _fitsOnScreen({ - required double availableWidth, - required double paddingLeft, - required int childrenCount, - required double childWidth, - required double childPadding, -}) { - return paddingLeft + childrenCount * childWidth + (childrenCount - 1) * childPadding <= availableWidth; -} - -bool _fitsPartially({ - required double availableWidth, - required double paddingLeft, - required int childrenCount, - required double childWidth, - required double childPadding, -}) { - for (int i = 2; i <= childrenCount; i++) { - final minWidth = _calcTotalWidthWithPartialLastItem( - paddingLeft: paddingLeft, - childrenCount: i, - childWidth: childWidth, - childPadding: childPadding, - lastItemPercentage: 0.1, - ); - final maxWidth = _calcTotalWidthWithPartialLastItem( - paddingLeft: paddingLeft, - childrenCount: i, - childWidth: childWidth, - childPadding: childPadding, - lastItemPercentage: 0.5, - ); - - if (minWidth <= availableWidth && maxWidth > availableWidth) { - return true; - } - } - return false; -} - -@pragma('vm:prefer-inline') -@pragma('dart2js:tryInline') -double _calcTotalWidthWithPartialLastItem({ - required double paddingLeft, - required int childrenCount, - required double childWidth, - required double childPadding, - required double lastItemPercentage, -}) { - return paddingLeft + (childrenCount - 1) * childWidth + childWidth * lastItemPercentage + (childrenCount - 1) * childPadding; -} diff --git a/app/lib/widget/responsive_wrap_view.dart b/app/lib/widget/responsive_wrap_view.dart new file mode 100644 index 00000000..4354357c --- /dev/null +++ b/app/lib/widget/responsive_wrap_view.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays a list of children in a responsive, wrapping grid. +/// +/// It lays out children horizontally, and when a row is full, it wraps +/// to the next line. It calculates an optimal, equal width for all children +/// to make them fill the available width, while ensuring they are never +/// smaller than [minChildWidth]. +class ResponsiveWrapView extends StatelessWidget { + final double outerHorizontalPadding; + final double outerVerticalPadding; + final double childPadding; + final double minChildWidth; + final List children; + + const ResponsiveWrapView({ + super.key, + required this.outerHorizontalPadding, + required this.outerVerticalPadding, + required this.childPadding, + required this.minChildWidth, + required this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: outerHorizontalPadding, + vertical: outerVerticalPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final double availableWidth = constraints.maxWidth; + + if (availableWidth < minChildWidth) { + return Wrap( + runSpacing: childPadding, + children: children + .map( + (child) => SizedBox( + width: availableWidth, + child: child, + ), + ) + .toList(), + ); + } + + // Calculate how many items *can* fit in a row at their minimum size. + // Formula: n * minChildWidth + (n - 1) * childPadding <= availableWidth + // This simplifies to: n <= (availableWidth + childPadding) / (minChildWidth + childPadding) + final int numItemsPerRow = ((availableWidth + childPadding) / (minChildWidth + childPadding)).floor(); + + // Calculate the *actual* width to make `numItemsPerRow` items + // fill the `availableWidth` perfectly. + // Formula: numItemsPerRow * actualWidth + (numItemsPerRow - 1) * childPadding = availableWidth + final double actualChildWidth = (availableWidth - (numItemsPerRow - 1) * childPadding) / numItemsPerRow; + + return Wrap( + spacing: childPadding, + runSpacing: childPadding, + children: children + .map( + (child) => SizedBox( + width: actualChildWidth, + child: child, + ), + ) + .toList(), + ); + }, + ), + ); + } +}