mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
mod: main: use tabbarview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
356
lib/common/widgets/tabs.dart
Normal file
356
lib/common/widgets/tabs.dart
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
|
import 'package:flutter/material.dart' hide TabBarView;
|
||||||
|
|
||||||
|
/// A page view that displays the widget which corresponds to the currently
|
||||||
|
/// selected tab.
|
||||||
|
///
|
||||||
|
/// This widget is typically used in conjunction with a [TabBar].
|
||||||
|
///
|
||||||
|
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
|
||||||
|
///
|
||||||
|
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
|
||||||
|
/// ancestor.
|
||||||
|
///
|
||||||
|
/// The tab controller's [TabController.length] must equal the length of the
|
||||||
|
/// [children] list and the length of the [TabBar.tabs] list.
|
||||||
|
///
|
||||||
|
/// To see a sample implementation, visit the [TabController] documentation.
|
||||||
|
class CustomTabBarView extends StatefulWidget {
|
||||||
|
/// Creates a page view with one child per tab.
|
||||||
|
///
|
||||||
|
/// The length of [children] must be the same as the [controller]'s length.
|
||||||
|
const CustomTabBarView({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
this.controller,
|
||||||
|
this.physics,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.viewportFraction = 1.0,
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
|
this.scrollDirection = Axis.horizontal,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// This widget's selection and animation state.
|
||||||
|
///
|
||||||
|
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||||
|
/// will be used.
|
||||||
|
final TabController? controller;
|
||||||
|
|
||||||
|
/// One widget per tab.
|
||||||
|
///
|
||||||
|
/// Its length must match the length of the [TabBar.tabs]
|
||||||
|
/// list, as well as the [controller]'s [TabController.length].
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
/// How the page view should respond to user input.
|
||||||
|
///
|
||||||
|
/// For example, determines how the page view continues to animate after the
|
||||||
|
/// user stops dragging the page view.
|
||||||
|
///
|
||||||
|
/// The physics are modified to snap to page boundaries using
|
||||||
|
/// [PageScrollPhysics] prior to being used.
|
||||||
|
///
|
||||||
|
/// Defaults to matching platform conventions.
|
||||||
|
final ScrollPhysics? physics;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.pageview.viewportFraction}
|
||||||
|
final double viewportFraction;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// Defaults to [Clip.hardEdge].
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
|
final Axis scrollDirection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomTabBarView> createState() => _CustomTabBarViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||||
|
TabController? _controller;
|
||||||
|
PageController? _pageController;
|
||||||
|
late List<Widget> _childrenWithKey;
|
||||||
|
int? _currentIndex;
|
||||||
|
int _warpUnderwayCount = 0;
|
||||||
|
int _scrollUnderwayCount = 0;
|
||||||
|
bool _debugHasScheduledValidChildrenCountCheck = false;
|
||||||
|
|
||||||
|
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||||
|
// dispose the old one. In that case the old controller's animation will be
|
||||||
|
// null and should not be accessed.
|
||||||
|
bool get _controllerIsValid => _controller?.animation != null;
|
||||||
|
|
||||||
|
void _updateTabController() {
|
||||||
|
final TabController? newController =
|
||||||
|
widget.controller ?? DefaultTabController.maybeOf(context);
|
||||||
|
assert(() {
|
||||||
|
if (newController == null) {
|
||||||
|
throw FlutterError(
|
||||||
|
'No TabController for ${widget.runtimeType}.\n'
|
||||||
|
'When creating a ${widget.runtimeType}, you must either provide an explicit '
|
||||||
|
'TabController using the "controller" property, or you must ensure that there '
|
||||||
|
'is a DefaultTabController above the ${widget.runtimeType}.\n'
|
||||||
|
'In this case, there was neither an explicit controller nor a default controller.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
if (newController == _controller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_controllerIsValid) {
|
||||||
|
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||||
|
}
|
||||||
|
_controller = newController;
|
||||||
|
if (_controller != null) {
|
||||||
|
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _jumpToPage(int page) {
|
||||||
|
_warpUnderwayCount += 1;
|
||||||
|
_pageController!.jumpToPage(page);
|
||||||
|
_warpUnderwayCount -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _animateToPage(
|
||||||
|
int page, {
|
||||||
|
required Duration duration,
|
||||||
|
required Curve curve,
|
||||||
|
}) async {
|
||||||
|
_warpUnderwayCount += 1;
|
||||||
|
await _pageController!
|
||||||
|
.animateToPage(page, duration: duration, curve: curve);
|
||||||
|
_warpUnderwayCount -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_updateTabController();
|
||||||
|
_currentIndex = _controller!.index;
|
||||||
|
if (_pageController == null) {
|
||||||
|
_pageController = PageController(
|
||||||
|
initialPage: _currentIndex!,
|
||||||
|
viewportFraction: widget.viewportFraction,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_pageController!.jumpToPage(_currentIndex!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CustomTabBarView oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.controller != oldWidget.controller) {
|
||||||
|
_updateTabController();
|
||||||
|
_currentIndex = _controller!.index;
|
||||||
|
_jumpToPage(_currentIndex!);
|
||||||
|
}
|
||||||
|
if (widget.viewportFraction != oldWidget.viewportFraction) {
|
||||||
|
_pageController?.dispose();
|
||||||
|
_pageController = PageController(
|
||||||
|
initialPage: _currentIndex!,
|
||||||
|
viewportFraction: widget.viewportFraction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// While a warp is under way, we stop updating the tab page contents.
|
||||||
|
// This is tracked in https://github.com/flutter/flutter/issues/31269.
|
||||||
|
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
|
||||||
|
_updateChildren();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_controllerIsValid) {
|
||||||
|
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||||
|
}
|
||||||
|
_controller = null;
|
||||||
|
_pageController?.dispose();
|
||||||
|
// We don't own the _controller Animation, so it's not disposed here.
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateChildren() {
|
||||||
|
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTabControllerAnimationTick() {
|
||||||
|
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||||
|
return;
|
||||||
|
} // This widget is driving the controller's animation.
|
||||||
|
|
||||||
|
if (_controller!.index != _currentIndex) {
|
||||||
|
_currentIndex = _controller!.index;
|
||||||
|
_warpToCurrentIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _warpToCurrentIndex() {
|
||||||
|
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool adjacentDestination =
|
||||||
|
(_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||||
|
if (adjacentDestination) {
|
||||||
|
_warpToAdjacentTab(_controller!.animationDuration);
|
||||||
|
} else {
|
||||||
|
_warpToNonAdjacentTab(_controller!.animationDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _warpToAdjacentTab(Duration duration) async {
|
||||||
|
if (duration == Duration.zero) {
|
||||||
|
_jumpToPage(_currentIndex!);
|
||||||
|
} else {
|
||||||
|
await _animateToPage(_currentIndex!,
|
||||||
|
duration: duration, curve: Curves.ease);
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_updateChildren();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Future<void>.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _warpToNonAdjacentTab(Duration duration) async {
|
||||||
|
final int previousIndex = _controller!.previousIndex;
|
||||||
|
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||||
|
|
||||||
|
// initialPage defines which page is shown when starting the animation.
|
||||||
|
// This page is adjacent to the destination page.
|
||||||
|
final int initialPage = _currentIndex! > previousIndex
|
||||||
|
? _currentIndex! - 1
|
||||||
|
: _currentIndex! + 1;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
|
||||||
|
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
|
||||||
|
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
|
||||||
|
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
|
||||||
|
final Widget temp = _childrenWithKey[initialPage];
|
||||||
|
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||||
|
_childrenWithKey[previousIndex] = temp;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make a first jump to the adjacent page.
|
||||||
|
_jumpToPage(initialPage);
|
||||||
|
|
||||||
|
// Jump or animate to the destination page.
|
||||||
|
if (duration == Duration.zero) {
|
||||||
|
_jumpToPage(_currentIndex!);
|
||||||
|
} else {
|
||||||
|
await _animateToPage(_currentIndex!,
|
||||||
|
duration: duration, curve: Curves.ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_updateChildren();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncControllerOffset() {
|
||||||
|
_controller!.offset =
|
||||||
|
clampDouble(_pageController!.page! - _controller!.index, -1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the PageView scrolls
|
||||||
|
bool _handleScrollNotification(ScrollNotification notification) {
|
||||||
|
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.depth != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_controllerIsValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollUnderwayCount += 1;
|
||||||
|
final double page = _pageController!.page!;
|
||||||
|
if (notification is ScrollUpdateNotification &&
|
||||||
|
!_controller!.indexIsChanging) {
|
||||||
|
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
|
||||||
|
if (pageChanged) {
|
||||||
|
_controller!.index = page.round();
|
||||||
|
_currentIndex = _controller!.index;
|
||||||
|
}
|
||||||
|
_syncControllerOffset();
|
||||||
|
} else if (notification is ScrollEndNotification) {
|
||||||
|
_controller!.index = page.round();
|
||||||
|
_currentIndex = _controller!.index;
|
||||||
|
if (!_controller!.indexIsChanging) {
|
||||||
|
_syncControllerOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_scrollUnderwayCount -= 1;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _debugScheduleCheckHasValidChildrenCount() {
|
||||||
|
if (_debugHasScheduledValidChildrenCountCheck) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
|
||||||
|
_debugHasScheduledValidChildrenCountCheck = false;
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert(() {
|
||||||
|
if (_controller!.length != widget.children.length) {
|
||||||
|
throw FlutterError(
|
||||||
|
"Controller's length property (${_controller!.length}) does not match the "
|
||||||
|
"number of children (${widget.children.length}) present in TabBarView's children property.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
}, debugLabel: 'TabBarView.validChildrenCountCheck');
|
||||||
|
_debugHasScheduledValidChildrenCountCheck = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(_debugScheduleCheckHasValidChildrenCount());
|
||||||
|
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: _handleScrollNotification,
|
||||||
|
child: PageView(
|
||||||
|
scrollDirection: widget.scrollDirection,
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
|
clipBehavior: widget.clipBehavior,
|
||||||
|
controller: _pageController,
|
||||||
|
physics: widget.physics == null
|
||||||
|
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||||
|
: const PageScrollPhysics().applyTo(widget.physics),
|
||||||
|
children: _childrenWithKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ class MainController extends GetxController {
|
|||||||
final StreamController<bool> bottomBarStream =
|
final StreamController<bool> bottomBarStream =
|
||||||
StreamController<bool>.broadcast();
|
StreamController<bool>.broadcast();
|
||||||
late bool hideTabBar;
|
late bool hideTabBar;
|
||||||
late PageController pageController;
|
late TabController controller;
|
||||||
RxInt selectedIndex = 0.obs;
|
RxInt selectedIndex = 0.obs;
|
||||||
RxBool isLogin = false.obs;
|
RxBool isLogin = false.obs;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/tabs.dart';
|
||||||
import 'package:PiliPlus/grpc/grpc_client.dart';
|
import 'package:PiliPlus/grpc/grpc_client.dart';
|
||||||
import 'package:PiliPlus/pages/mine/controller.dart';
|
import 'package:PiliPlus/pages/mine/controller.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
@@ -41,8 +42,11 @@ class _MainAppState extends State<MainApp>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
_mainController.pageController =
|
_mainController.controller = TabController(
|
||||||
PageController(initialPage: _mainController.selectedIndex.value);
|
vsync: this,
|
||||||
|
initialIndex: _mainController.selectedIndex.value,
|
||||||
|
length: _mainController.navigationBars.length,
|
||||||
|
);
|
||||||
enableMYBar =
|
enableMYBar =
|
||||||
GStorage.setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
GStorage.setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
|
||||||
useSideBar =
|
useSideBar =
|
||||||
@@ -110,7 +114,7 @@ class _MainAppState extends State<MainApp>
|
|||||||
|
|
||||||
if (value != _mainController.selectedIndex.value) {
|
if (value != _mainController.selectedIndex.value) {
|
||||||
_mainController.selectedIndex.value = value;
|
_mainController.selectedIndex.value = value;
|
||||||
_mainController.pageController.jumpToPage(value);
|
_mainController.controller.animateTo(value);
|
||||||
dynamic currentPage = _mainController.pages[value];
|
dynamic currentPage = _mainController.pages[value];
|
||||||
if (currentPage is HomePage) {
|
if (currentPage is HomePage) {
|
||||||
_checkDefaultSearch();
|
_checkDefaultSearch();
|
||||||
@@ -213,9 +217,12 @@ class _MainAppState extends State<MainApp>
|
|||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.06),
|
color: Theme.of(context).colorScheme.outline.withOpacity(0.06),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView(
|
child: CustomTabBarView(
|
||||||
|
scrollDirection: context.orientation == Orientation.portrait
|
||||||
|
? Axis.horizontal
|
||||||
|
: Axis.vertical,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
controller: _mainController.pageController,
|
controller: _mainController.controller,
|
||||||
children: _mainController.pages,
|
children: _mainController.pages,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user