From 41e5f6d0de311d270fd7a6a8913be9bb7ee0480f Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 18 Aug 2022 10:54:09 +0800 Subject: [PATCH] replace tabview with pageview to remove animation Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 29 +- .../lib/desktop/pages/desktop_tab_page.dart | 32 +- .../desktop/pages/file_manager_tab_page.dart | 31 +- .../lib/desktop/widgets/tabbar_widget.dart | 315 ++++++++++-------- flutter/pubspec.lock | 51 +-- flutter/pubspec.yaml | 1 + 6 files changed, 260 insertions(+), 199 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index eb8614dd4..5dd4a829a 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -20,28 +20,28 @@ class ConnectionTabPage extends StatefulWidget { State createState() => _ConnectionTabPageState(params); } -class _ConnectionTabPageState extends State - with TickerProviderStateMixin { +class _ConnectionTabPageState extends State { // refactor List when using multi-tab // this singleton is only for test RxList tabs = RxList.empty(growable: true); - late Rx tabController; - static final Rx _selected = 0.obs; static final Rx _fullscreenID = "".obs; - IconData icon = Icons.desktop_windows_sharp; + final IconData selectedIcon = Icons.desktop_windows_sharp; + final IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { if (params['id'] != null) { - tabs.add(TabInfo(label: params['id'], icon: icon)); + tabs.add(TabInfo( + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } } @override void initState() { super.initState(); - tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -50,8 +50,12 @@ class _ConnectionTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: id, icon: icon)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } else if (call.method == "onDestroy") { print( "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); @@ -74,18 +78,16 @@ class _ConnectionTabPageState extends State Obx(() => Visibility( visible: _fullscreenID.value.isEmpty, child: DesktopTabBar( - controller: tabController, tabs: tabs, onTabClose: onRemoveId, - selected: _selected, dark: isDarkTheme(), mainTab: false, ))), Expanded(child: Obx(() { WindowController.fromWindowId(windowId()) .setFullscreen(_fullscreenID.value.isNotEmpty); - return TabBarView( - controller: tabController.value, + return PageView( + controller: DesktopTabBar.controller.value, children: tabs .map((tab) => RemotePage( key: ValueKey(tab.label), @@ -103,7 +105,6 @@ class _ConnectionTabPageState extends State } void onRemoveId(String id) { - DesktopTabBar.onClose(this, tabController, tabs, id); ffi(id).close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 65ba37e45..5cbc7aece 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -13,20 +13,19 @@ class DesktopTabPage extends StatefulWidget { State createState() => _DesktopTabPageState(); } -class _DesktopTabPageState extends State - with TickerProviderStateMixin { - late Rx tabController; +class _DesktopTabPageState extends State { late RxList tabs; - static final Rx _selected = 0.obs; @override void initState() { super.initState(); tabs = RxList.from([ - TabInfo(label: kTabLabelHomePage, icon: Icons.home_sharp, closable: false) + TabInfo( + label: kTabLabelHomePage, + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false) ], growable: true); - tabController = - TabController(length: tabs.length, vsync: this, initialIndex: 0).obs; } @override @@ -35,17 +34,14 @@ class _DesktopTabPageState extends State body: Column( children: [ DesktopTabBar( - controller: tabController, tabs: tabs, - onTabClose: onTabClose, - selected: _selected, dark: isDarkTheme(), mainTab: true, onAddSetting: onAddSetting, ), Obx((() => Expanded( - child: TabBarView( - controller: tabController.value, + child: PageView( + controller: DesktopTabBar.controller.value, children: tabs.map((tab) { switch (tab.label) { case kTabLabelHomePage: @@ -62,12 +58,12 @@ class _DesktopTabPageState extends State ); } - void onTabClose(String label) { - DesktopTabBar.onClose(this, tabController, tabs, label); - } - void onAddSetting() { - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: kTabLabelSettingPage, icon: Icons.build)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: kTabLabelSettingPage, + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined)); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index aa8c60afc..5f12c873a 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -19,25 +19,25 @@ class FileManagerTabPage extends StatefulWidget { State createState() => _FileManagerTabPageState(params); } -class _FileManagerTabPageState extends State - with TickerProviderStateMixin { +class _FileManagerTabPageState extends State { // refactor List when using multi-tab // this singleton is only for test RxList tabs = List.empty(growable: true).obs; - late Rx tabController; - static final Rx _selected = 0.obs; - IconData icon = Icons.file_copy_sharp; + final IconData selectedIcon = Icons.file_copy_sharp; + final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { if (params['id'] != null) { - tabs.add(TabInfo(label: params['id'], icon: icon)); + tabs.add(TabInfo( + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } } @override void initState() { super.initState(); - tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -46,8 +46,12 @@ class _FileManagerTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: id, icon: icon)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } else if (call.method == "onDestroy") { print( "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); @@ -68,17 +72,15 @@ class _FileManagerTabPageState extends State body: Column( children: [ DesktopTabBar( - controller: tabController, tabs: tabs, onTabClose: onRemoveId, - selected: _selected, dark: isDarkTheme(), mainTab: false, ), Expanded( child: Obx( - () => TabBarView( - controller: tabController.value, + () => PageView( + controller: DesktopTabBar.controller.value, children: tabs .map((tab) => FileManagerPage( key: ValueKey(tab.label), @@ -92,8 +94,7 @@ class _FileManagerTabPageState extends State } void onRemoveId(String id) { - DesktopTabBar.onClose(this, tabController, tabs, id); - ffi(id).close(); + ffi("ft_$id").close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 4a2581705..d2acb87ad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -8,21 +8,22 @@ import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:scroll_pos/scroll_pos.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kAddIconSize = _kTabBarHeight - 15; -final tabBarKey = GlobalKey(); +final _tabBarKey = GlobalKey(); void closeTab(String? id) { - final tabBar = tabBarKey.currentWidget as TabBar?; + final tabBar = _tabBarKey.currentWidget as _ListView?; if (tabBar == null) return; - final tabs = tabBar.tabs as List<_Tab>; + final tabs = tabBar.tabs; if (id == null) { - final current = tabBar.controller?.index; - if (current == null) return; - tabs[current].onClose(); + if (tabBar.selected.value < tabs.length) { + tabs[tabBar.selected.value].onClose(); + } } else { for (final tab in tabs) { if (tab.label == id) { @@ -35,33 +36,45 @@ void closeTab(String? id) { class TabInfo { late final String label; - late final IconData icon; + late final IconData selectedIcon; + late final IconData unselectedIcon; late final bool closable; - TabInfo({required this.label, required this.icon, this.closable = true}); + TabInfo( + {required this.label, + required this.selectedIcon, + required this.unselectedIcon, + this.closable = true}); } class DesktopTabBar extends StatelessWidget { - late final Rx controller; late final RxList tabs; - late final Function(String) onTabClose; - late final Rx selected; + late final Function(String)? onTabClose; late final bool dark; late final _Theme _theme; late final bool mainTab; late final Function()? onAddSetting; + final ScrollPosController scrollController = + ScrollPosController(itemCount: 0); + static final Rx controller = PageController().obs; + static final Rx selected = 0.obs; DesktopTabBar({ Key? key, - required this.controller, required this.tabs, - required this.onTabClose, - required this.selected, + this.onTabClose, required this.dark, required this.mainTab, this.onAddSetting, }) : _theme = dark ? _Theme.dark() : _Theme.light(), - super(key: key); + super(key: key) { + scrollController.itemCount = tabs.length; + WidgetsBinding.instance.addPostFrameCallback((_) { + debugPrint("callback"); + scrollController.scrollToItem(selected.value, + center: true, animate: true); + }); + } @override Widget build(BuildContext context) { @@ -81,57 +94,29 @@ class DesktopTabBar extends StatelessWidget { ), Expanded( child: GestureDetector( - onPanStart: (_) { - if (mainTab) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: Obx(() => TabBar( - key: tabBarKey, - indicatorColor: _theme.indicatorColor, - labelPadding: const EdgeInsets.symmetric( - vertical: 0, horizontal: 0), - isScrollable: true, - indicatorPadding: EdgeInsets.zero, - physics: BouncingScrollPhysics(), - controller: controller.value, - tabs: tabs.asMap().entries.map((e) { - int index = e.key; - String label = e.value.label; - - return _Tab( - index: index, - label: label, - icon: e.value.icon, - closable: e.value.closable, - selected: selected.value, - onClose: () { - onTabClose(label); - if (index <= selected.value) { - selected.value = max(0, selected.value - 1); - } - controller.value.animateTo(selected.value, - duration: Duration.zero); - }, - onSelected: () { - selected.value = index; - controller.value - .animateTo(index, duration: Duration.zero); - }, - theme: _theme, - ); - }).toList())), - ), + onPanStart: (_) { + if (mainTab) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + key: _tabBarKey, + controller: controller, + scrollController: scrollController, + tabInfos: tabs, + selected: selected, + onTabClose: onTabClose, + theme: _theme)), ), Offstage( offstage: mainTab, child: _AddButton( theme: _theme, ).paddingOnly(left: 10), - ) + ), ], ), ), @@ -157,32 +142,16 @@ class DesktopTabBar extends StatelessWidget { ); } - static onClose( - TickerProvider vsync, - Rx controller, - RxList tabs, - String label, - ) { - tabs.removeWhere((tab) => tab.label == label); - controller.value = TabController( - length: tabs.length, - vsync: vsync, - initialIndex: max(0, tabs.length - 1)); - } - - static onAdd(TickerProvider vsync, Rx controller, - RxList tabs, Rx selected, TabInfo tab) { + static onAdd(RxList tabs, TabInfo tab) { int index = tabs.indexWhere((e) => e.label == tab.label); if (index >= 0) { - controller.value.animateTo(index, duration: Duration.zero); selected.value = index; } else { tabs.add(tab); - controller.value = TabController( - length: tabs.length, vsync: vsync, initialIndex: tabs.length - 1); - controller.value.animateTo(tabs.length - 1, duration: Duration.zero); selected.value = tabs.length - 1; + assert(selected.value >= 0); } + controller.value.jumpToPage(selected.value); } } @@ -265,10 +234,76 @@ class WindowActionPanel extends StatelessWidget { } } +class _ListView extends StatelessWidget { + late Rx controller; + final ScrollPosController scrollController; + final RxList tabInfos; + final Rx selected; + final Function(String label)? onTabClose; + final _Theme _theme; + late List<_Tab> tabs; + + _ListView({ + Key? key, + required this.controller, + required this.scrollController, + required this.tabInfos, + required this.selected, + required this.onTabClose, + required _Theme theme, + }) : _theme = theme, + super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + tabs = tabInfos.asMap().entries.map((e) { + int index = e.key; + String label = e.value.label; + return _Tab( + index: index, + label: label, + selectedIcon: e.value.selectedIcon, + unselectedIcon: e.value.unselectedIcon, + closable: e.value.closable, + selected: selected.value, + onClose: () { + tabInfos.removeWhere((tab) => tab.label == label); + onTabClose?.call(label); + if (index <= selected.value) { + selected.value = max(0, selected.value - 1); + } + assert(tabInfos.length == 0 || selected.value < tabInfos.length); + scrollController.itemCount = tabInfos.length; + if (tabInfos.length > 0) { + scrollController.scrollToItem(selected.value, + center: true, animate: true); + controller.value.jumpToPage(selected.value); + } + }, + onSelected: () { + selected.value = index; + scrollController.scrollToItem(index, center: true, animate: true); + controller.value.jumpToPage(index); + }, + theme: _theme, + ); + }).toList(); + return ListView( + controller: scrollController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + children: tabs); + }); + } +} + class _Tab extends StatelessWidget { late final int index; late final String label; - late final IconData icon; + late final IconData selectedIcon; + late final IconData unselectedIcon; late final bool closable; late final int selected; late final Function() onClose; @@ -280,7 +315,8 @@ class _Tab extends StatelessWidget { {Key? key, required this.index, required this.label, - required this.icon, + required this.selectedIcon, + required this.unselectedIcon, required this.closable, required this.selected, required this.onClose, @@ -292,59 +328,74 @@ class _Tab extends StatelessWidget { Widget build(BuildContext context) { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; - return Ink( - child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), - child: Row( - children: [ - Tab( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], + return Stack( + children: [ + Ink( + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Container( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, ), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !show_divider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: theme.dividerColor, - thickness: 1, - ), - ) - ], + ) + ], + ), + ), ), - ), + Positioned( + height: 2, + left: 0, + right: 0, + bottom: 0, + child: Center( + child: Container( + color: + is_selected ? theme.indicatorColor : Colors.transparent), + )) + ], ); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 679322df3..c27406913 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.8.2" + version: "2.9.0" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -235,15 +235,17 @@ packages: dash_chat_2: dependency: "direct main" description: - name: dash_chat_2 - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: feat_maxWidth + resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a" + url: "https://github.com/fufesou/Dash-Chat-2" + source: git version: "0.0.12" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: @@ -324,7 +326,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -607,14 +609,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -628,7 +630,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -705,7 +707,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -846,6 +848,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" + scroll_pos: + dependency: "direct main" + description: + name: scroll_pos + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" settings_ui: dependency: "direct main" description: @@ -948,7 +957,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: transitive description: @@ -990,7 +999,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1004,14 +1013,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -1029,10 +1038,12 @@ packages: tray_manager: dependency: "direct main" description: - name: tray_manager - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.7" + path: "." + ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + url: "https://github.com/Kingtous/rustdesk_tray_manager" + source: git + version: "0.1.8" tuple: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f25d5e341..f616e887a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: visibility_detector: ^0.3.3 contextmenu: ^3.0.0 desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 dev_dependencies: flutter_launcher_icons: ^0.9.1