improve bottom sheet fps

This commit is contained in:
Fabian Freund
2026-07-31 06:59:40 +02:00
parent 376fc7d4bb
commit 5b83a62a4f
6 changed files with 299 additions and 372 deletions
@@ -21,7 +21,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:fading_scroll/fading_scroll.dart';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -134,59 +133,51 @@ class _BrowserMenuSheet extends HookConsumerWidget {
// Scrollable content // Scrollable content
Expanded( Expanded(
child: FadingScroll( child: ListView(
controller: scrollController, controller: scrollController,
fadingSize: 25, padding: const EdgeInsets.symmetric(
builder: (context, controller) { horizontal: 16,
return ListView( vertical: 8,
controller: controller, ),
padding: const EdgeInsets.symmetric( children: [
horizontal: 16, // Quick toggles (Desktop / Reader / Gestures)
vertical: 8, if (selectedTabId != null) ...[
), _QuickTogglesGrid(selectedTabId: selectedTabId),
children: [ const SizedBox(height: 16),
// Quick toggles (Desktop / Reader / Gestures) ],
if (selectedTabId != null) ...[
_QuickTogglesGrid(selectedTabId: selectedTabId),
const SizedBox(height: 16),
],
// Page actions // Page actions
if (selectedTabId != null) ...[ if (selectedTabId != null) ...[
_PageActionsCard(selectedTabId: selectedTabId), _PageActionsCard(selectedTabId: selectedTabId),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Extensions // Extensions
_ExtensionsCard(), _ExtensionsCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
// Tab actions // Tab actions
if (selectedTabId != null) ...[ if (selectedTabId != null) ...[
_TabActionsCard(selectedTabId: selectedTabId), _TabActionsCard(selectedTabId: selectedTabId),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Quick links grid // Quick links grid
_QuickLinksGrid( _QuickLinksGrid(showContainerUi: settings.showContainerUi),
showContainerUi: settings.showContainerUi, const SizedBox(height: 16),
),
const SizedBox(height: 16),
// Connection (Tor + proxy profiles) // Connection (Tor + proxy profiles)
const _ConnectionCard(), const _ConnectionCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
// Profile // Profile
_ProfileCard(), _ProfileCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
// App // App
const _SettingsCard(), const _SettingsCard(),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
);
},
), ),
), ),
@@ -50,26 +50,7 @@ class DraggableScrollableHeader extends StatefulWidget {
_DraggableScrollableHeaderState(); _DraggableScrollableHeaderState();
} }
class _DraggableScrollableHeaderState extends State<DraggableScrollableHeader> class _DraggableScrollableHeaderState extends State<DraggableScrollableHeader> {
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double>? _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _animateToPosition( Future<void> _animateToPosition(
double targetSize, { double targetSize, {
Duration? customDuration, Duration? customDuration,
@@ -79,34 +60,17 @@ class _DraggableScrollableHeaderState extends State<DraggableScrollableHeader>
return; return;
} }
if (customDuration != null) { await widget.controller.animateTo(
_animationController.duration = customDuration; targetSize,
} else { duration: customDuration ?? widget.animationDuration,
_animationController.duration = widget.animationDuration; curve: widget.animationCurve,
} );
_animation = Tween<double>(begin: widget.controller.size, end: targetSize)
.animate(
CurvedAnimation(
parent: _animationController,
curve: widget.animationCurve,
),
);
_animationController.reset();
await _animationController.forward();
_animation!.addListener(() {
widget.controller.jumpTo(_animation!.value);
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
_animationController.stop();
// Apply drag sensitivity // Apply drag sensitivity
final adjustedDelta = details.delta.dy * widget.dragSensitivity; final adjustedDelta = details.delta.dy * widget.dragSensitivity;
@@ -18,7 +18,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -144,48 +143,42 @@ class ViewTabSheetWidget extends HookConsumerWidget {
), ),
), ),
], ],
body: FadingScroll( body: ListView(
fadingSize: 15, padding: EdgeInsets.zero,
controller: sheetScrollController, controller: sheetScrollController,
builder: (context, controller) { physics: const ClampingScrollPhysicsWithoutImplicit(),
return ListView( children: [
padding: EdgeInsets.zero, // Tracking Protection Section
controller: controller, TrackingProtectionSection(tabId: initialTabState.id),
physics: const ClampingScrollPhysicsWithoutImplicit(), const Divider(),
children: [ // Gesture Exclusion Section
// Tracking Protection Section GestureExclusionSection(url: initialTabState.url),
TrackingProtectionSection(tabId: initialTabState.id), const Divider(),
const Divider(), // Desktop Mode Section
// Gesture Exclusion Section DesktopModeSection(
GestureExclusionSection(url: initialTabState.url), tabId: initialTabState.id,
const Divider(), url: initialTabState.url,
// Desktop Mode Section ),
DesktopModeSection( const Divider(),
tabId: initialTabState.id, // App Link Section
url: initialTabState.url, AppLinkSection(
), url: initialTabState.url,
const Divider(), contextId: initialTabState.contextId,
// App Link Section ),
AppLinkSection( const Divider(),
url: initialTabState.url, // Permissions Section
contextId: initialTabState.contextId, PermissionsSection(
), origin: initialTabState.url.origin,
const Divider(), isPrivate: initialTabState.tabMode is PrivateTabMode,
// Permissions Section ),
PermissionsSection( const Divider(),
origin: initialTabState.url.origin, // Clear Site Data Section
isPrivate: initialTabState.tabMode is PrivateTabMode, ClearSiteDataSection(
), url: initialTabState.url,
const Divider(), onExpandedChanged: onClearSiteDataExpandedChanged,
// Clear Site Data Section ),
ClearSiteDataSection( const SizedBox(height: 16.0),
url: initialTabState.url, ],
onExpandedChanged: onClearSiteDataExpandedChanged,
),
const SizedBox(height: 16.0),
],
);
},
), ),
); );
} }
@@ -20,7 +20,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:fading_scroll/fading_scroll.dart';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -328,113 +327,107 @@ class _TabGridView extends HookConsumerWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FadingScroll( child: !reorderEnabled
fadingSize: 5, ? _TabGrid(
controller: scrollController, key: ValueKey(crossAxisCount),
builder: (context, controller) { crossAxisCount: crossAxisCount,
return !reorderEnabled itemCount: displayItemCount,
? _TabGrid( scrollController: scrollController,
key: ValueKey(crossAxisCount), itemBuilder: (widget, _) {
if (widget is CustomDraggable) {
if (widget.data case final TabDragData dragData) {
return TabContextMenuDraggable(
tabId: dragData.tabId,
data: dragData,
feedbackSize: itemSize,
child: widget.child,
);
}
}
return widget;
},
suggestedContainerId: containerId,
primaryRows: primaryRows,
suggestedTabEntities: suggestedTabEntities,
onClose: onClose,
)
: ReorderableBuilder.builder(
//Rebuild when cross axis count changes
key: ValueKey(crossAxisCount),
scrollController: scrollController,
itemCount: displayItemCount,
onDragStarted: (index) {
ref.read(willAcceptDropProvider.notifier).clear();
},
onReorderPositions: (positions) async {
assert(
positions.length == 1,
'Not ready for multiple reorders',
);
final oldIndex = positions.first.oldIndex;
final newIndex = positions.first.newIndex;
//Suggestions are at the end and not reorderable, so skip
if (oldIndex >= primaryRows.length) {
return;
}
final result = buildTabViewReorderResult(
visibleItems: primaryRows,
treeRows: treeRows,
collapsedGroups: collapsedGroups,
pinnedTabIds: pinnedTabIds,
oldIndex: oldIndex,
newIndex: newIndex,
tabListDirection: tabListDirection,
hierarchical: showHierarchicalTabs && !hasActiveSearch,
sortPinnedFirst: filterOptions.sortPinnedFirst,
);
if (result == null) return;
await ref
.read(tabDataRepositoryProvider.notifier)
.reorderTabs(
movingTabIds: result.movingTabIds,
previousTabId: result.previousTabId,
nextTabId: result.nextTabId,
parentChange: result.parentChange,
);
},
childBuilder: (reorderableItemBuilder) {
return _TabGrid(
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
itemCount: displayItemCount, itemCount: displayItemCount,
scrollController: controller, scrollController: scrollController,
itemBuilder: (widget, _) { itemBuilder: (widget, index) {
// Wrap with context menu before passing to reorderable
Widget wrapped = widget;
if (widget is CustomDraggable) { if (widget is CustomDraggable) {
if (widget.data case final TabDragData dragData) { if (widget.data case final TabDragData dragData) {
return TabContextMenuDraggable( wrapped = CustomDraggable(
tabId: dragData.tabId, key: widget.key!,
data: dragData, data: widget.data,
feedbackSize: itemSize, child: TabContextMenuDraggable(
child: widget.child, tabId: dragData.tabId,
feedbackSize: Size.zero,
externalDrag: true,
child: widget.child,
),
); );
} }
} }
return reorderableItemBuilder(wrapped, index);
return widget;
}, },
suggestedContainerId: containerId, suggestedContainerId: containerId,
primaryRows: primaryRows, primaryRows: primaryRows,
suggestedTabEntities: suggestedTabEntities, suggestedTabEntities: suggestedTabEntities,
onClose: onClose, onClose: onClose,
)
: ReorderableBuilder.builder(
//Rebuild when cross axis count changes
key: ValueKey(crossAxisCount),
scrollController: controller,
itemCount: displayItemCount,
onDragStarted: (index) {
ref.read(willAcceptDropProvider.notifier).clear();
},
onReorderPositions: (positions) async {
assert(
positions.length == 1,
'Not ready for multiple reorders',
);
final oldIndex = positions.first.oldIndex;
final newIndex = positions.first.newIndex;
//Suggestions are at the end and not reorderable, so skip
if (oldIndex >= primaryRows.length) {
return;
}
final result = buildTabViewReorderResult(
visibleItems: primaryRows,
treeRows: treeRows,
collapsedGroups: collapsedGroups,
pinnedTabIds: pinnedTabIds,
oldIndex: oldIndex,
newIndex: newIndex,
tabListDirection: tabListDirection,
hierarchical: showHierarchicalTabs && !hasActiveSearch,
sortPinnedFirst: filterOptions.sortPinnedFirst,
);
if (result == null) return;
await ref
.read(tabDataRepositoryProvider.notifier)
.reorderTabs(
movingTabIds: result.movingTabIds,
previousTabId: result.previousTabId,
nextTabId: result.nextTabId,
parentChange: result.parentChange,
);
},
childBuilder: (reorderableItemBuilder) {
return _TabGrid(
crossAxisCount: crossAxisCount,
itemCount: displayItemCount,
scrollController: controller,
itemBuilder: (widget, index) {
// Wrap with context menu before passing to reorderable
Widget wrapped = widget;
if (widget is CustomDraggable) {
if (widget.data case final TabDragData dragData) {
wrapped = CustomDraggable(
key: widget.key!,
data: widget.data,
child: TabContextMenuDraggable(
tabId: dragData.tabId,
feedbackSize: Size.zero,
externalDrag: true,
child: widget.child,
),
);
}
}
return reorderableItemBuilder(wrapped, index);
},
suggestedContainerId: containerId,
primaryRows: primaryRows,
suggestedTabEntities: suggestedTabEntities,
onClose: onClose,
);
},
); );
}, },
), ),
); );
} }
} }
@@ -20,7 +20,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:fading_scroll/fading_scroll.dart';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -366,139 +365,133 @@ class _TabListView extends HookConsumerWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FadingScroll( child: !reorderEnabled
fadingSize: 5, ? ListView.builder(
controller: scrollController, padding: const EdgeInsets.only(bottom: 56),
builder: (context, controller) { controller: scrollController,
return !reorderEnabled itemCount: displayItemCount,
? ListView.builder( itemExtent: _itemHeight,
padding: const EdgeInsets.only(bottom: 56), itemBuilder: (context, index) {
controller: scrollController, if (index < primaryRows.length) {
itemCount: displayItemCount, final row = primaryRows[index];
itemExtent: _itemHeight, final tab = CustomDraggable(
itemBuilder: (context, index) { key: Key(row.tabId),
if (index < primaryRows.length) { data: TabDragData(row.tabId),
final row = primaryRows[index]; child: _TabDraggable(
final tab = CustomDraggable( tabId: row.tabId,
key: Key(row.tabId), onClose: onClose,
data: TabDragData(row.tabId), sourceSearchQuery: row.sourceSearchQuery,
child: _TabDraggable( height: _itemHeight,
tabId: row.tabId, groupToggle: _listGroupToggleFor(row),
onClose: onClose, depth: _depthFor(row),
sourceSearchQuery: row.sourceSearchQuery, ),
height: _itemHeight, );
groupToggle: _listGroupToggleFor(row),
depth: _depthFor(row),
),
);
return TabDropTarget( return TabDropTarget(
targetTabId: row.tabId, targetTabId: row.tabId,
child: TabContextMenuDraggable( child: TabContextMenuDraggable(
tabId: row.tabId, tabId: row.tabId,
data: tab.data! as TabDragData, data: tab.data! as TabDragData,
feedbackSize: Size( feedbackSize: Size(
MediaQuery.of(context).size.width, MediaQuery.of(context).size.width,
_itemHeight, _itemHeight,
),
child: tab.child,
),
);
}
final suggestedIndex = index - primaryRows.length;
final entity = suggestedTabEntities.value[suggestedIndex];
final tab = CustomDraggable(
key: Key('suggested_${entity.tabId}'),
child: _TabDraggable(
tabId: entity.tabId,
onClose: onClose,
suggestedContainerId: containerId,
height: _itemHeight,
), ),
);
return TabDropTarget(
targetTabId: entity.tabId,
enabled: false,
child: tab.child, child: tab.child,
); ),
}, );
) }
: ReorderableListView.builder(
scrollController: controller,
padding: const EdgeInsets.only(bottom: 56),
itemCount: displayItemCount,
itemExtent: _itemHeight,
onReorderStart: (index) {
ref.read(willAcceptDropProvider.notifier).clear();
},
onReorderItem: (oldIndex, newIndex) async {
//Suggestions are at the end and not reorderable, so skip
if (oldIndex >= primaryRows.length) {
return;
}
final result = buildTabViewReorderResult( final suggestedIndex = index - primaryRows.length;
visibleItems: primaryRows, final entity = suggestedTabEntities.value[suggestedIndex];
treeRows: treeRows, final tab = CustomDraggable(
collapsedGroups: collapsedGroups, key: Key('suggested_${entity.tabId}'),
pinnedTabIds: pinnedTabIds, child: _TabDraggable(
oldIndex: oldIndex, tabId: entity.tabId,
newIndex: newIndex, onClose: onClose,
tabListDirection: tabListDirection, suggestedContainerId: containerId,
hierarchical: showHierarchicalTabs && !hasActiveSearch, height: _itemHeight,
sortPinnedFirst: filterOptions.sortPinnedFirst, ),
);
if (result == null) return;
await ref
.read(tabDataRepositoryProvider.notifier)
.reorderTabs(
movingTabIds: result.movingTabIds,
previousTabId: result.previousTabId,
nextTabId: result.nextTabId,
parentChange: result.parentChange,
);
},
itemBuilder: (context, index) {
if (index < primaryRows.length) {
final row = primaryRows[index];
return CustomDraggable(
key: Key(row.tabId),
data: TabDragData(row.tabId),
child: TabContextMenuDraggable(
tabId: row.tabId,
feedbackSize: Size.zero,
externalDrag: true,
child: _TabDraggable(
tabId: row.tabId,
onClose: onClose,
sourceSearchQuery: row.sourceSearchQuery,
height: _itemHeight,
groupToggle: _listGroupToggleFor(row),
depth: _depthFor(row),
),
),
);
} else {
final suggestedIndex = index - primaryRows.length;
final entity = suggestedTabEntities.value[suggestedIndex];
return CustomDraggable(
key: Key('suggested_${entity.tabId}'),
child: _TabDraggable(
tabId: entity.tabId,
onClose: onClose,
suggestedContainerId: containerId,
height: _itemHeight,
),
);
}
},
); );
}, return TabDropTarget(
), targetTabId: entity.tabId,
enabled: false,
child: tab.child,
);
},
)
: ReorderableListView.builder(
scrollController: scrollController,
padding: const EdgeInsets.only(bottom: 56),
itemCount: displayItemCount,
itemExtent: _itemHeight,
onReorderStart: (index) {
ref.read(willAcceptDropProvider.notifier).clear();
},
onReorderItem: (oldIndex, newIndex) async {
//Suggestions are at the end and not reorderable, so skip
if (oldIndex >= primaryRows.length) {
return;
}
final result = buildTabViewReorderResult(
visibleItems: primaryRows,
treeRows: treeRows,
collapsedGroups: collapsedGroups,
pinnedTabIds: pinnedTabIds,
oldIndex: oldIndex,
newIndex: newIndex,
tabListDirection: tabListDirection,
hierarchical: showHierarchicalTabs && !hasActiveSearch,
sortPinnedFirst: filterOptions.sortPinnedFirst,
);
if (result == null) return;
await ref
.read(tabDataRepositoryProvider.notifier)
.reorderTabs(
movingTabIds: result.movingTabIds,
previousTabId: result.previousTabId,
nextTabId: result.nextTabId,
parentChange: result.parentChange,
);
},
itemBuilder: (context, index) {
if (index < primaryRows.length) {
final row = primaryRows[index];
return CustomDraggable(
key: Key(row.tabId),
data: TabDragData(row.tabId),
child: TabContextMenuDraggable(
tabId: row.tabId,
feedbackSize: Size.zero,
externalDrag: true,
child: _TabDraggable(
tabId: row.tabId,
onClose: onClose,
sourceSearchQuery: row.sourceSearchQuery,
height: _itemHeight,
groupToggle: _listGroupToggleFor(row),
depth: _depthFor(row),
),
),
);
} else {
final suggestedIndex = index - primaryRows.length;
final entity = suggestedTabEntities.value[suggestedIndex];
return CustomDraggable(
key: Key('suggested_${entity.tabId}'),
child: _TabDraggable(
tabId: entity.tabId,
onClose: onClose,
suggestedContainerId: containerId,
height: _itemHeight,
),
);
}
},
),
); );
} }
} }
@@ -20,7 +20,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -335,24 +334,18 @@ class _TabTreesGrid extends HookConsumerWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FadingScroll( child: GridView.builder(
controller: scrollController, controller: scrollController,
fadingSize: 5, padding: const EdgeInsets.only(bottom: 56),
builder: (context, controller) { gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
return GridView.builder( //Sync values for itemHeight calculation _calculateItemHeight
controller: controller, childAspectRatio: 0.75,
padding: const EdgeInsets.only(bottom: 56), mainAxisSpacing: 8.0,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: 8.0,
//Sync values for itemHeight calculation _calculateItemHeight crossAxisCount: crossAxisCount,
childAspectRatio: 0.75, ),
mainAxisSpacing: 8.0, itemCount: tabs.length,
crossAxisSpacing: 8.0, itemBuilder: (context, index) => tabs[index],
crossAxisCount: crossAxisCount,
),
itemCount: tabs.length,
itemBuilder: (context, index) => tabs[index],
);
},
), ),
); );
} }