Commit 5323925c authored by 汪林玲's avatar 汪林玲

Initial commit

parents
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe
channel: unknown
project_type: package
## [0.0.1] - TODO: Add release date.
* TODO: Describe initial release.
TODO: Add your license here.
# navigation_bar
通用NavigationBar
## DEMO
```
Container( // 必须使用确定高度的Widget包裹起来
height: 35,
child: NavigationBar(
selectColor: Colors.green, // 选中字体颜色
normalColor: Colors.red, // 未选中字体颜色
controller: tabController, // 控制器
selectStyle: TextStyle(fontWeight: FontWeight.bold), // 选中字体样式,color无效
items: ["TAB_1", "TAB_2", "TAB_3"], // 数据列表
isScrollable: true, // 是否可以滚动,false时item平分tabbar宽度
indicatorSize: TabBarIndicatorSize.tab, // 指示器宽度,tab占满item,label对齐item文字宽度,对ImageIndicator和设置了宽度RoundRectIndicator无效
indicator: ImageIndicator( // 指示器
image: 'assets/test/home_choose_logo.png',
width: 12,
height: 6,
marginBottom: 3,
color: Colors.green
),
onChange: (index) { // index改变事件
}
),
)
```
## 指示器
```
ImageIndicator( // 图片指示器
image: 'assets/test/home_choose_logo.png', // 图片地址,仅支持本地
width: 12, // 图片显示宽度
height: 6, // 图片显示高度
marginBottom: 3, // 图片距离底部边距
color: Colors.green, // 颜色,可以改变图片颜色
)
RoundRectIndicator( // 圆角矩形指示器
round: 2, // 指示器圆角
width: 30, // 显示宽度
height: 2, // 显示高度
marginBottom: 3, // 距离底部边距
color: Colors.green, // 颜色
)
```
// 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 'dart:async';
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
const double _kTabHeight = 46.0;
const double _kTextAndIconTabHeight = 72.0;
/// Defines how the bounds of the selected tab indicator are computed.
///
/// See also:
///
/// * [ITabBar], which displays a row of tabs.
/// * [TabBarView], which displays a widget for the currently selected tab.
/// * [ITabBar.indicator], which defines the appearance of the selected tab
/// indicator relative to the tab's bounds.
/// A material design [ITabBar] tab.
///
/// If both [icon] and [text] are provided, the text is displayed below
/// the icon.
///
/// See also:
///
/// * [ITabBar], which displays a row of tabs.
/// * [TabBarView], which displays a widget for the currently selected tab.
/// * [TabController], which coordinates tab selection between a [ITabBar] and a [TabBarView].
/// * <https://material.io/design/components/tabs.html>
class ITab extends StatelessWidget {
/// Creates a material design [ITabBar] tab.
///
/// At least one of [text], [icon], and [child] must be non-null. The [text]
/// and [child] arguments must not be used at the same time. The
/// [iconMargin] is only useful when [icon] and either one of [text] or
/// [child] is non-null.
const ITab({
Key? key,
this.text,
this.icon,
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.child,
}) : assert(text != null || child != null || icon != null),
assert(!(text != null &&
null !=
child)), // https://github.com/dart-lang/sdk/issues/34180
super(key: key);
/// The text to display as the tab's label.
///
/// Must not be used in combination with [child].
final String? text;
/// The widget to be used as the tab's label.
///
/// Usually a [Text] widget, possibly wrapped in a [Semantics] widget.
///
/// Must not be used in combination with [text].
final Widget? child;
/// An icon to display as the tab's label.
final Widget? icon;
/// The margin added around the tab's icon.
///
/// Only useful when used in combination with [icon], and either one of
/// [text] or [child] is non-null.
final EdgeInsetsGeometry iconMargin;
Widget _buildLabelText() {
return child ?? Text(text!, softWrap: false, overflow: TextOverflow.fade);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
double height;
Widget? label;
if (icon == null) {
height = _kTabHeight;
label = _buildLabelText();
} else if (text == null && child == null) {
height = _kTabHeight;
label = icon;
} else {
height = _kTextAndIconTabHeight;
label = Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
child: icon,
margin: iconMargin,
),
_buildLabelText(),
],
);
}
return SizedBox(
height: height,
child: Center(
child: label,
widthFactor: 1.0,
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('text', text, defaultValue: null));
properties
.add(DiagnosticsProperty<Widget>('icon', icon, defaultValue: null));
}
}
class _TabStyle extends AnimatedWidget {
const _TabStyle({
Key? key,
required Animation<double> animation,
this.selected,
this.labelColor,
this.unselectedLabelColor,
this.labelStyle,
this.unselectedLabelStyle,
required this.child,
}) : super(key: key, listenable: animation);
final TextStyle? labelStyle;
final TextStyle? unselectedLabelStyle;
final bool? selected;
final Color? labelColor;
final Color? unselectedLabelColor;
final Widget? child;
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final Animation<double> animation = listenable as Animation<double>;
// To enable TextStyle.lerp(style1, style2, value), both styles must have
// the same value of inherit. Force that to be inherit=true here.
final TextStyle defaultUnselectedStyle = (unselectedLabelStyle ??
tabBarTheme.unselectedLabelStyle ??
labelStyle ??
themeData.primaryTextTheme.bodyText1)!
.copyWith(inherit: true);
final TextStyle defaultStyle = (labelStyle ??
tabBarTheme.labelStyle ??
themeData.primaryTextTheme.bodyText1)!
.copyWith(inherit: true).copyWith(fontSize:defaultUnselectedStyle.fontSize);
final TextStyle textStyle = selected!
? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)!
: TextStyle.lerp(defaultUnselectedStyle, defaultStyle, animation.value)!;
final double multiple = labelStyle!.fontSize! / unselectedLabelStyle!.fontSize!;
final double _scale = selected!
? lerpDouble(multiple, 1, animation.value)!
: lerpDouble(1, multiple, animation.value)!;
final Color? selectedColor = labelColor ??
tabBarTheme.labelColor ??
themeData.primaryTextTheme.bodyText1!.color;
final Color unselectedColor = unselectedLabelColor ??
tabBarTheme.unselectedLabelColor ??
selectedColor!.withAlpha(0xB2); // 70% alpha
final Color? color = selected!
? Color.lerp(selectedColor, unselectedColor, animation.value)
: Color.lerp(unselectedColor, selectedColor, animation.value);
return DefaultTextStyle(
style: textStyle.copyWith(color: color),
// child: child,
child: IconTheme.merge(
data: IconThemeData(
size: 24.0,
color: color,
),
child: Transform.scale(scale: _scale, child: child),
),
);
}
}
typedef _LayoutCallback = void Function(
List<double> xOffsets, TextDirection? textDirection, double width);
class _TabLabelBarRenderer extends RenderFlex {
_TabLabelBarRenderer({
List<RenderBox>? children,
required Axis direction,
required MainAxisSize mainAxisSize,
required MainAxisAlignment mainAxisAlignment,
required CrossAxisAlignment crossAxisAlignment,
required TextDirection textDirection,
required VerticalDirection verticalDirection,
required this.onPerformLayout,
}) : assert(onPerformLayout != null),
super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
);
_LayoutCallback? onPerformLayout;
@override
void performLayout() {
super.performLayout();
RenderBox? child = firstChild;
final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData = child.parentData as FlexParentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
assert(textDirection != null);
if(textDirection == TextDirection.rtl){
xOffsets.insert(0, size.width);
}else if(textDirection == TextDirection.ltr){
xOffsets.add(size.width);
}
onPerformLayout!(xOffsets, textDirection, size.width);
}
}
// This class and its renderer class only exist to report the widths of the tabs
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
// or in response to input.
class _TabLabelBar extends Flex {
_TabLabelBar({
Key? key,
List<Widget?> children = const <Widget>[],
this.onPerformLayout,
}) : super(
key: key,
children: children as List<Widget>,
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
verticalDirection: VerticalDirection.down,
);
final _LayoutCallback? onPerformLayout;
@override
RenderFlex createRenderObject(BuildContext context) {
return _TabLabelBarRenderer(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context)!,
verticalDirection: verticalDirection,
onPerformLayout: onPerformLayout!,
);
}
@override
void updateRenderObject(
BuildContext context, _TabLabelBarRenderer renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
double _indexChangeProgress(TabController controller) {
final double controllerValue = controller.animation!.value;
final double previousIndex = controller.previousIndex.toDouble();
final double currentIndex = controller.index.toDouble();
// The controller's offset is changing because the user is dragging the
// TabBarView's PageView to the left or right.
if (!controller.indexIsChanging)
return (currentIndex - controllerValue).abs().clamp(0.0, 1.0);
// The TabController animation's value is changing from previousIndex to currentIndex.
return (controllerValue - currentIndex).abs() /
(currentIndex - previousIndex).abs();
}
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
required this.indicator,
required this.indicatorSize,
required this.tabKeys,
_IndicatorPainter? old,
}) : super(repaint: controller.animation) {
if (old != null)
saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
}
final TabController controller;
final Decoration indicator;
final TabBarIndicatorSize? indicatorSize;
final List<GlobalKey>? tabKeys;
List<double>? _currentTabOffsets;
TextDirection? _currentTextDirection = TextDirection.rtl;
Rect? _currentRect;
BoxPainter? _painter;
bool _needsPaint = false;
void markNeedsPaint() {
_needsPaint = true;
}
void dispose() {
_painter?.dispose();
}
void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
_currentTabOffsets = tabOffsets;
_currentTextDirection = textDirection;
}
// _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
// _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
int get maxTabIndex => _currentTabOffsets!.length - 2;
double centerOf(int tabIndex) {
assert(_currentTabOffsets != null);
assert(_currentTabOffsets!.isNotEmpty);
assert(tabIndex >= 0);
assert(tabIndex <= maxTabIndex);
return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
2.0;
}
Rect indicatorRect(Size tabBarSize, int tabIndex) {
assert(_currentTabOffsets != null);
assert(_currentTextDirection != null);
assert(_currentTabOffsets!.isNotEmpty);
assert(tabIndex >= 0);
assert(tabIndex <= maxTabIndex);
double? tabLeft = 0 , tabRight = 0;
if(_currentTextDirection == TextDirection.rtl){
tabLeft = _currentTabOffsets![tabIndex + 1];
tabRight = _currentTabOffsets![tabIndex];
}else if(_currentTextDirection == TextDirection.ltr){
tabLeft = _currentTabOffsets![tabIndex];
tabRight = _currentTabOffsets![tabIndex + 1];
}
if (indicatorSize == TabBarIndicatorSize.label) {
final double tabWidth = tabKeys![tabIndex].currentContext!.size!.width;
final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
tabLeft += delta;
tabRight -= delta;
}
return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height);
}
@override
void paint(Canvas canvas, Size size) {
_needsPaint = false;
_painter ??= indicator.createBoxPainter(markNeedsPaint);
if (controller.indexIsChanging) {
// The user tapped on a tab, the tab controller's animation is running.
final Rect targetRect = indicatorRect(size, controller.index);
_currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect,
_indexChangeProgress(controller));
} else {
// The user is dragging the TabBarView's PageView left or right.
final int currentIndex = controller.index;
final Rect? previous =
currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null;
final Rect middle = indicatorRect(size, currentIndex);
final Rect? next = currentIndex < maxTabIndex
? indicatorRect(size, currentIndex + 1)
: null;
final double index = controller.index.toDouble();
final double value = controller.animation!.value;
if (value == index - 1.0)
_currentRect = previous ?? middle;
else if (value == index + 1.0)
_currentRect = next ?? middle;
else if (value == index)
_currentRect = middle;
else if (value < index)
_currentRect = previous == null
? middle
: Rect.lerp(middle, previous, index - value);
else
_currentRect =
next == null ? middle : Rect.lerp(middle, next, value - index);
}
assert(_currentRect != null);
final ImageConfiguration configuration = ImageConfiguration(
size: _currentRect!.size,
textDirection: _currentTextDirection,
);
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
}
static bool _tabOffsetsEqual(List<double>? a, List<double>? b) {
// The following null check should be replaced when a fix
// for https://github.com/flutter/flutter/issues/40014 is available.
if (a == null || b == null || a.length != b.length) return false;
for (int i = 0; i < a.length; i += 1) {
if (a[i] != b[i]) return false;
}
return true;
}
@override
bool shouldRepaint(_IndicatorPainter old) {
return _needsPaint ||
controller != old.controller ||
indicator != old.indicator ||
tabKeys!.length != old.tabKeys!.length ||
(!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets)) ||
_currentTextDirection != old._currentTextDirection;
}
}
class _ChangeAnimation extends Animation<double>
with AnimationWithParentMixin<double> {
_ChangeAnimation(this.controller);
final TabController? controller;
@override
Animation<double> get parent => controller!.animation!;
@override
void removeStatusListener(AnimationStatusListener listener) {
super.removeStatusListener(listener);
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
}
@override
double get value => _indexChangeProgress(controller!);
}
class _DragAnimation extends Animation<double>
with AnimationWithParentMixin<double> {
_DragAnimation(this.controller, this.index);
final TabController? controller;
final int index;
@override
Animation<double> get parent => controller!.animation!;
@override
void removeStatusListener(AnimationStatusListener listener) {
super.removeStatusListener(listener);
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
}
@override
double get value {
assert(!controller!.indexIsChanging);
return (controller!.animation!.value - index.toDouble()).abs().clamp(0.0, 1.0);
}
}
// This class, and TabBarScrollController, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known.
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
_TabBarScrollPosition({
required ScrollPhysics physics,
required ScrollContext context,
ScrollPosition? oldPosition,
this.tabBar,
}) : super(
physics: physics,
context: context,
initialPixels: null,
oldPosition: oldPosition,
);
final _ITabBarState? tabBar;
bool? _initialViewportDimensionWasZero;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
bool result = true;
if (_initialViewportDimensionWasZero != true) {
_initialViewportDimensionWasZero = viewportDimension != 0.0;
correctPixels(tabBar!._initialScrollOffset(
viewportDimension, minScrollExtent, maxScrollExtent));
result = false;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
result;
}
}
// This class, and TabBarScrollPosition, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex.
class _TabBarScrollController extends ScrollController {
_TabBarScrollController(this.tabBar);
final _ITabBarState tabBar;
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition? oldPosition) {
return _TabBarScrollPosition(
physics: physics,
context: context,
oldPosition: oldPosition,
tabBar: tabBar,
);
}
}
/// A material design widget that displays a horizontal row of tabs.
///
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// conjunction with a [TabBarView].
///
/// If a [TabController] is not provided, then a [DefaultTabController] ancestor
/// must be provided instead. The tab controller's [TabController.length] must
/// equal the length of the [tabs] list and the length of the
/// [TabBarView.children] list.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// Uses values from [TabBarTheme] if it is set in the current context.
///
/// To see a sample implementation, visit the [TabController] documentation.
///
/// See also:
///
/// * [TabBarView], which displays page views that correspond to each tab.
class ITabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design tab bar.
///
/// The [tabs] argument must not be null and its length must match the [controller]'s
/// [TabController.length].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
///
/// The [indicatorWeight] parameter defaults to 2, and must not be null.
///
/// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null.
///
/// If [indicator] is not null, then [indicatorWeight], [indicatorPadding], and
/// [indicatorColor] are ignored.
const ITabBar({
Key? key,
required this.tabs,
this.controller,
this.isScrollable = false,
this.indicatorColor,
this.indicatorWeight = 2.0,
this.indicatorPadding = EdgeInsets.zero,
this.indicator,
this.indicatorSize,
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,
}) : assert(indicator != null ||
(indicatorWeight > 0.0)),
assert(indicator != null),
super(key: key);
/// Typically a list of two or more [ITab] widgets.
///
/// The length of this list must match the [controller]'s [TabController.length]
/// and the length of the [TabBarView.children] list.
final List<Widget> tabs;
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
/// will be used.
final TabController? controller;
/// Whether this tab bar can be scrolled horizontally.
///
/// If [isScrollable] is true, then each tab is as wide as needed for its label
/// and the entire [ITabBar] is scrollable. Otherwise each tab gets an equal
/// share of the available space.
final bool isScrollable;
/// The color of the line that appears below the selected tab.
///
/// If this parameter is null, then the value of the Theme's indicatorColor
/// property is used.
///
/// If [indicator] is specified, this property is ignored.
final Color? indicatorColor;
/// The thickness of the line that appears below the selected tab.
///
/// The value of this parameter must be greater than zero and its default
/// value is 2.0.
///
/// If [indicator] is specified, this property is ignored.
final double indicatorWeight;
/// The horizontal padding for the line that appears below the selected tab.
///
/// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align
/// the indicator with the tab's text for [ITab] widgets and all but the
/// shortest [ITab.text] values.
///
/// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the
/// [indicatorPadding] are ignored.
///
/// The default value of [indicatorPadding] is [EdgeInsets.zero].
///
/// If [indicator] is specified, this property is ignored.
final EdgeInsetsGeometry indicatorPadding;
/// Defines the appearance of the selected tab indicator.
///
/// If [indicator] is specified, the [indicatorColor], [indicatorWeight],
/// and [indicatorPadding] properties are ignored.
///
/// The default, underline-style, selected tab indicator can be defined with
/// [UnderlineTabIndicator].
///
/// The indicator's size is based on the tab's bounds. If [indicatorSize]
/// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space
/// occupied by the tab in the tab bar. If [indicatorSize] is
/// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as
/// the tab widget itself.
final Decoration? indicator;
/// Defines how the selected tab indicator's size is computed.
///
/// The size of the selected tab indicator is defined relative to the
/// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab]
/// (the default) or relative to the bounds of the tab's widget if
/// [indicatorSize] is [TabBarIndicatorSize.label].
///
/// The selected tab's location appearance can be refined further with
/// the [indicatorColor], [indicatorWeight], [indicatorPadding], and
/// [indicator] properties.
final TabBarIndicatorSize? indicatorSize;
/// The color of selected tab labels.
///
/// Unselected tab labels are rendered with the same color rendered at 70%
/// opacity unless [unselectedLabelColor] is non-null.
///
/// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s
/// bodyText1 text color is used.
final Color? labelColor;
/// The color of unselected tab labels.
///
/// If this property is null, unselected tab labels are rendered with the
/// [labelColor] with 70% opacity.
final Color? unselectedLabelColor;
/// The text style of the selected tab labels.
///
/// If [unselectedLabelStyle] is null, then this text style will be used for
/// both selected and unselected label styles.
///
/// If this property is null, then the text style of the
/// [ThemeData.primaryTextTheme]'s bodyText1 definition is used.
final TextStyle? labelStyle;
/// The padding added to each of the tab labels.
///
/// If this property is null, then kTabLabelPadding is used.
final EdgeInsetsGeometry? labelPadding;
/// The text style of the unselected tab labels
///
/// If this property is null, then the [labelStyle] value is used. If [labelStyle]
/// is null, then the text style of the [ThemeData.primaryTextTheme]'s
/// bodyText1 definition is used.
final TextStyle? unselectedLabelStyle;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// An optional callback that's called when the [ITabBar] is tapped.
///
/// The callback is applied to the index of the tab where the tap occurred.
///
/// This callback has no effect on the default handling of taps. It's for
/// applications that want to do a little extra work when a tab is tapped,
/// even if the tap doesn't change the TabController's index. TabBar [onTap]
/// callbacks should not make changes to the TabController since that would
/// interfere with the default tap handler.
final ValueChanged<int>? onTap;
/// A size whose height depends on if the tabs have both icons and text.
///
/// [AppBar] uses this size to compute its own preferred size.
@override
Size get preferredSize {
for (final Widget item in tabs) {
if (item is ITab) {
final ITab tab = item;
if ((tab.text != null || tab.child != null) && tab.icon != null)
return Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight);
}
}
return Size.fromHeight(_kTabHeight + indicatorWeight);
}
@override
_ITabBarState createState() => _ITabBarState();
}
class _ITabBarState extends State<ITabBar> {
ScrollController? _scrollController;
TabController? _controller;
_IndicatorPainter? _indicatorPainter;
int? _currentIndex;
late double _tabStripWidth;
List<GlobalKey>? _tabKeys;
@override
void initState() {
super.initState();
// If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
// the width of tab widget i. See _IndicatorPainter.indicatorRect().
_tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList();
}
Decoration? get _indicator {
if (widget.indicator != null) return widget.indicator;
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
if (tabBarTheme.indicator != null) return tabBarTheme.indicator;
Color color = widget.indicatorColor ?? Theme.of(context).indicatorColor;
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab bar is on a
// Material that isn't the primaryColor. In that case, if the indicator
// color ends up matching the material's color, then this overrides it.
// When that happens, automatic transitions of the theme will likely look
// ugly as the indicator color suddenly snaps to white at one end, but it's
// not clear how to avoid that any further.
//
// The material's color might be null (if it's a transparency). In that case
// there's no good way for us to find out what the color is so we don't.
if (color.value == Material.of(context)!.color?.value) color = Colors.white;
return UnderlineTabIndicator(
insets: widget.indicatorPadding,
borderSide: BorderSide(
width: widget.indicatorWeight,
color: color,
),
);
}
// If the TabBar 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.of(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!.removeListener(_handleTabControllerTick);
}
_controller = newController;
if (_controller != null) {
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
_controller!.addListener(_handleTabControllerTick);
_currentIndex = _controller!.index;
}
}
void _initIndicatorPainter() {
_indicatorPainter = !_controllerIsValid
? null
: _IndicatorPainter(
controller: _controller!,
indicator: _indicator!,
indicatorSize:
widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize,
tabKeys: _tabKeys,
old: _indicatorPainter,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(debugCheckHasMaterial(context));
_updateTabController();
_initIndicatorPainter();
}
@override
void didUpdateWidget(ITabBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
_updateTabController();
_initIndicatorPainter();
} else if (widget.indicatorColor != oldWidget.indicatorColor ||
widget.indicatorWeight != oldWidget.indicatorWeight ||
widget.indicatorSize != oldWidget.indicatorSize ||
widget.indicator != oldWidget.indicator) {
_initIndicatorPainter();
}
if (widget.tabs.length > oldWidget.tabs.length) {
final int delta = widget.tabs.length - oldWidget.tabs.length;
_tabKeys!.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
} else if (widget.tabs.length < oldWidget.tabs.length) {
_tabKeys!.removeRange(widget.tabs.length, oldWidget.tabs.length);
}
}
@override
void dispose() {
_indicatorPainter!.dispose();
if (_controllerIsValid) {
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
_controller!.removeListener(_handleTabControllerTick);
}
_controller = null;
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
int get maxTabIndex => _indicatorPainter!.maxTabIndex;
double _tabScrollOffset(
int? index, double viewportWidth, double minExtent, double maxExtent) {
if (!widget.isScrollable) return 0.0;
double tabCenter = _indicatorPainter!.centerOf(index!);
switch (Directionality.of(context)) {
case TextDirection.rtl:
tabCenter = _tabStripWidth - tabCenter;
break;
case TextDirection.ltr:
break;
}
return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent);
}
double _tabCenteredScrollOffset(int? index) {
final ScrollPosition position = _scrollController!.position;
return _tabScrollOffset(index, position.viewportDimension,
position.minScrollExtent, position.maxScrollExtent);
}
double _initialScrollOffset(
double viewportWidth, double minExtent, double maxExtent) {
return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
}
void _scrollToCurrentIndex() {
final double offset = _tabCenteredScrollOffset(_currentIndex);
_scrollController!.animateTo(offset,
duration: kTabScrollDuration, curve: Curves.ease);
}
void _scrollToControllerValue() {
final double? leadingPosition =
_currentIndex! > 0 ? _tabCenteredScrollOffset(_currentIndex! - 1) : null;
final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
final double? trailingPosition = _currentIndex! < maxTabIndex
? _tabCenteredScrollOffset(_currentIndex! + 1)
: null;
final double index = _controller!.index.toDouble();
final double value = _controller!.animation!.value;
double? offset;
if (value == index - 1.0)
offset = leadingPosition ?? middlePosition;
else if (value == index + 1.0)
offset = trailingPosition ?? middlePosition;
else if (value == index)
offset = middlePosition;
else if (value < index)
offset = leadingPosition == null
? middlePosition
: lerpDouble(middlePosition, leadingPosition, index - value);
else
offset = trailingPosition == null
? middlePosition
: lerpDouble(middlePosition, trailingPosition, value - index);
_scrollController!.jumpTo(offset!);
}
void _handleTabControllerAnimationTick() {
assert(mounted);
if (!_controller!.indexIsChanging && widget.isScrollable) {
// Sync the TabBar's scroll position with the TabBarView's PageView.
_currentIndex = _controller!.index;
_scrollToControllerValue();
}
}
void _handleTabControllerTick() {
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
if (widget.isScrollable) _scrollToCurrentIndex();
}
setState(() {
// Rebuild the tabs after a (potentially animated) index change
// has completed.
});
}
// Called each time layout completes.
void _saveTabOffsets(List<double> tabOffsets, TextDirection textDirection, double width) {
_tabStripWidth = width;
_indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
}
void _handleTap(int index) {
assert(index >= 0 && index < widget.tabs.length);
_controller!.animateTo(index);
if (widget.onTap != null) {
widget.onTap!(index);
}
}
Widget _buildStyledTab(
Widget? child, bool selected, Animation<double> animation) {
return _TabStyle(
animation: animation,
selected: selected,
labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
child: child,
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(() {
if (_controller!.length != widget.tabs.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.tabs.length}) present in TabBar's tabs property.");
}
return true;
}());
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
if (_controller!.length == 0) {
return Container(
height: _kTabHeight + widget.indicatorWeight,
);
}
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
final List<Widget?> wrappedTabs = List<Widget?>.empty(growable: true);
for (int i = 0; i < widget.tabs.length; i += 1) {
wrappedTabs[i] = Center(
heightFactor: 1.0,
child: Padding(
padding: widget.labelPadding ??
tabBarTheme.labelPadding ??
kTabLabelPadding,
child: KeyedSubtree(
key: _tabKeys![i],
child: widget.tabs[i],
),
),
);
}
// If the controller was provided by DefaultTabController and we're part
// of a Hero (typically the AppBar), then we will not be able to find the
// controller during a Hero transition. See https://github.com/flutter/flutter/issues/213.
if (_controller != null) {
final int previousIndex = _controller!.previousIndex;
if (_controller!.indexIsChanging) {
// The user tapped on a tab, the tab controller's animation is running.
assert(_currentIndex != previousIndex);
final Animation<double> animation = _ChangeAnimation(_controller);
wrappedTabs[_currentIndex!] =
_buildStyledTab(wrappedTabs[_currentIndex!], true, animation);
wrappedTabs[previousIndex] =
_buildStyledTab(wrappedTabs[previousIndex], false, animation);
} else {
// The user is dragging the TabBarView's PageView left or right.
final int tabIndex = _currentIndex!;
final Animation<double> centerAnimation =
_DragAnimation(_controller, tabIndex);
wrappedTabs[tabIndex] =
_buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
if (_currentIndex! > 0) {
final int tabIndex = _currentIndex! - 1;
final Animation<double> previousAnimation =
ReverseAnimation(_DragAnimation(_controller, tabIndex));
wrappedTabs[tabIndex] =
_buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
}
if (_currentIndex! < widget.tabs.length - 1) {
final int tabIndex = _currentIndex! + 1;
final Animation<double> nextAnimation =
ReverseAnimation(_DragAnimation(_controller, tabIndex));
wrappedTabs[tabIndex] =
_buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
}
}
}
// Add the tap handler to each tab. If the tab bar is not scrollable,
// then give all of the tabs equal flexibility so that they each occupy
// the same share of the tab bar's overall width.
final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) {
wrappedTabs[index] = InkWell(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onTap: () {
_handleTap(index);
},
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
child: Stack(
children: <Widget>[
wrappedTabs[index]!,
Semantics(
selected: index == _currentIndex,
label: localizations.tabLabel(
tabIndex: index + 1, tabCount: tabCount),
),
],
),
),
);
if (!widget.isScrollable)
wrappedTabs[index] = Expanded(child: wrappedTabs[index]!);
}
Widget tabBar = CustomPaint(
painter: _indicatorPainter,
child: _TabStyle(
animation: kAlwaysDismissedAnimation,
selected: false,
labelColor: widget.labelColor,
unselectedLabelColor: widget.unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
child: _TabLabelBar(//
onPerformLayout:(List<double> a1, TextDirection? a2, double a3){
_saveTabOffsets(a1,a2!,a3);
} ,
children: wrappedTabs,
),
),
);
if (widget.isScrollable) {
_scrollController ??= _TabBarScrollController(this);
tabBar = SingleChildScrollView(
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: Axis.horizontal,
controller: _scrollController,
child: tabBar,
);
}
return tabBar;
}
}
/// A page view that displays the widget which corresponds to the currently
/// selected tab.
///
/// This widget is typically used in conjunction with a [ITabBar].
///
/// 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 [ITabBar.tabs] list.
///
/// To see a sample implementation, visit the [TabController] documentation.
class TabBarView 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 TabBarView({
Key? key,
required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
}) : super(key: key);
/// 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 [ITabBar.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;
@override
_TabBarViewState createState() => _TabBarViewState();
}
final PageScrollPhysics _kTabBarViewPhysics =
const PageScrollPhysics().applyTo(const ClampingScrollPhysics());
class _TabBarViewState extends State<TabBarView> {
TabController? _controller;
PageController? _pageController;
List<Widget>? _children;
List<Widget>? _childrenWithKey;
int? _currentIndex;
int _warpUnderwayCount = 0;
// 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.of(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);
}
@override
void initState() {
super.initState();
_updateChildren();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTabController();
_currentIndex = _controller?.index;
_pageController = PageController(initialPage: _currentIndex ?? 0);
}
@override
void didUpdateWidget(TabBarView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) _updateTabController();
if (widget.children != oldWidget.children && _warpUnderwayCount == 0)
_updateChildren();
}
@override
void dispose() {
if (_controllerIsValid)
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
_controller = null;
// We don't own the _controller Animation, so it's not disposed here.
super.dispose();
}
void _updateChildren() {
_children = widget.children;
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
}
void _handleTabControllerAnimationTick() {
if (_warpUnderwayCount > 0 || !_controller!.indexIsChanging)
return; // This widget is driving the controller's animation.
if (_controller!.index != _currentIndex) {
_currentIndex = _controller!.index;
_warpToCurrentIndex();
}
}
Future<void> _warpToCurrentIndex() async {
if (!mounted) return Future<void>.value();
if (_pageController!.page == _currentIndex!.toDouble())
return Future<void>.value();
final int previousIndex = _controller!.previousIndex;
if ((_currentIndex! - previousIndex).abs() == 1)
return _pageController!.animateToPage(_currentIndex!,
duration: kTabScrollDuration, curve: Curves.ease);
assert((_currentIndex! - previousIndex).abs() > 1);
final int initialPage =
_currentIndex! > previousIndex ? _currentIndex! - 1 : _currentIndex! + 1;
final List<Widget>? originalChildren = _childrenWithKey;
setState(() {
_warpUnderwayCount += 1;
_childrenWithKey = List<Widget>.from(_childrenWithKey!, growable: false);
final Widget temp = _childrenWithKey![initialPage];
_childrenWithKey![initialPage] = _childrenWithKey![previousIndex];
_childrenWithKey![previousIndex] = temp;
});
_pageController!.jumpToPage(initialPage);
await _pageController!.animateToPage(_currentIndex!,
duration: kTabScrollDuration, curve: Curves.ease);
if (!mounted) return Future<void>.value();
setState(() {
_warpUnderwayCount -= 1;
if (widget.children != _children) {
_updateChildren();
} else {
_childrenWithKey = originalChildren;
}
});
}
// Called when the PageView scrolls
bool _handleScrollNotification(ScrollNotification notification) {
if (_warpUnderwayCount > 0) return false;
if (notification.depth != 0) return false;
_warpUnderwayCount += 1;
if (notification is ScrollUpdateNotification &&
!_controller!.indexIsChanging) {
if ((_pageController!.page! - _controller!.index).abs() > 1.0) {
_controller!.index = _pageController!.page!.floor();
_currentIndex = _controller!.index;
}
_controller!.offset =
(_pageController!.page! - _controller!.index).clamp(-1.0, 1.0);
} else if (notification is ScrollEndNotification) {
_controller!.index = _pageController!.page!.round();
_currentIndex = _controller!.index;
}
_warpUnderwayCount -= 1;
return false;
}
@override
Widget build(BuildContext context) {
assert(() {
if (_controller!.length != widget.children.length) {
throw FlutterError(
"Controller's length property (${_controller!.length}) does not match the "
"number of tabs (${widget.children.length}) present in TabBar's tabs property.");
}
return true;
}());
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView(
dragStartBehavior: widget.dragStartBehavior,
controller: _pageController,
physics: widget.physics == null
? _kTabBarViewPhysics
: _kTabBarViewPhysics.applyTo(widget.physics),
children: _childrenWithKey!,
),
);
}
}
/// Displays a single circle with the specified border and background colors.
///
/// Used by [TabPageSelector] to indicate the selected page.
class TabPageSelectorIndicator extends StatelessWidget {
/// Creates an indicator used by [TabPageSelector].
///
/// The [backgroundColor], [borderColor], and [size] parameters must not be null.
const TabPageSelectorIndicator({
Key? key,
required this.backgroundColor,
required this.borderColor,
required this.size,
}) : super(key: key);
/// The indicator circle's background color.
final Color backgroundColor;
/// The indicator circle's border color.
final Color borderColor;
/// The indicator circle's diameter.
final double size;
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
margin: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor),
shape: BoxShape.circle,
),
);
}
}
/// Displays a row of small circular indicators, one per tab.
///
/// The selected tab's indicator is highlighted. Often used in conjunction with
/// a [TabBarView].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
class TabPageSelector extends StatelessWidget {
/// Creates a compact widget that indicates which tab has been selected.
const TabPageSelector({
Key? key,
this.controller,
this.indicatorSize = 12.0,
this.color,
this.selectedColor,
}) : assert(indicatorSize > 0.0),
super(key: key);
/// This widget's selection and animation state.
///
/// If [TabController] is not provided, then the value of
/// [DefaultTabController.of] will be used.
final TabController? controller;
/// The indicator circle's diameter (the default value is 12.0).
final double indicatorSize;
/// The indicator circle's fill color for unselected pages.
///
/// If this parameter is null, then the indicator is filled with [Colors.transparent].
final Color? color;
/// The indicator circle's fill color for selected pages and border color
/// for all indicator circles.
///
/// If this parameter is null, then the indicator is filled with the theme's
/// accent color, [ThemeData.colorScheme.secondary].
final Color? selectedColor;
Widget _buildTabIndicator(
int tabIndex,
TabController tabController,
ColorTween selectedColorTween,
ColorTween previousColorTween,
) {
Color? background;
if (tabController.indexIsChanging) {
// The selection's animation is animating from previousValue to value.
final double t = 1.0 - _indexChangeProgress(tabController);
if (tabController.index == tabIndex)
background = selectedColorTween.lerp(t);
else if (tabController.previousIndex == tabIndex)
background = previousColorTween.lerp(t);
else
background = selectedColorTween.begin;
} else {
// The selection's offset reflects how far the TabBarView has / been dragged
// to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0).
final double offset = tabController.offset;
if (tabController.index == tabIndex) {
background = selectedColorTween.lerp(1.0 - offset.abs());
} else if (tabController.index == tabIndex - 1 && offset > 0.0) {
background = selectedColorTween.lerp(offset);
} else if (tabController.index == tabIndex + 1 && offset < 0.0) {
background = selectedColorTween.lerp(-offset);
} else {
background = selectedColorTween.begin;
}
}
return TabPageSelectorIndicator(
backgroundColor: background!,
borderColor: selectedColorTween.end!,
size: indicatorSize,
);
}
@override
Widget build(BuildContext context) {
final Color fixColor = color ?? Colors.transparent;
final Color fixSelectedColor =
selectedColor ?? Theme.of(context).colorScheme.secondary;
final ColorTween selectedColorTween =
ColorTween(begin: fixColor, end: fixSelectedColor);
final ColorTween previousColorTween =
ColorTween(begin: fixSelectedColor, end: fixColor);
final TabController tabController = controller ?? DefaultTabController.of(context)!;
final Animation<double> animation = CurvedAnimation(
parent: tabController.animation!,
curve: Curves.fastOutSlowIn,
);
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return Semantics(
label: 'Page ${tabController.index + 1} of ${tabController.length}',
child: Row(
mainAxisSize: MainAxisSize.min,
children:
List<Widget>.generate(tabController.length, (int tabIndex) {
return _buildTabIndicator(tabIndex, tabController,
selectedColorTween, previousColorTween);
}).toList(),
),
);
},
);
}
}
\ No newline at end of file
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
enum IndicatorAxisAlignment {
start,
end,
center,
}
class ImageIndicator extends Decoration {
final ui.Image? image;
final Color? color;
final double? width;
final double? height;
final double? marginBottom;
final double? marginRight;
final IndicatorAxisAlignment alignment;
ImageIndicator(
{required this.image,
this.color,
this.height,
this.width,
this.alignment = IndicatorAxisAlignment.center,
this.marginBottom = 0,
this.marginRight = 0});
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _ImageIndicatorPaint(dec: this, onChanged:onChanged);
}
}
class RoundRectIndicator extends Decoration {
final Color? color;
final double? width;
final double? height;
final double round;
final double? marginBottom;
final double? marginRight;
RoundRectIndicator(
{required this.color,
this.height = 2,
this.width,
this.round = 1,
this.marginBottom = 0,
this.marginRight=0});
@override
BoxPainter createBoxPainter([void Function()? onChanged]) {
return _RoundRectPainter(dec: this, onChanged:onChanged);
}
}
class _ImageIndicatorPaint extends BoxPainter {
final ImageIndicator dec;
final VoidCallback? onChanged;
late Paint painter;
_ImageIndicatorPaint({required this.dec,required this.onChanged}) : super() {
painter = Paint();
painter.style = PaintingStyle.stroke;
painter.isAntiAlias = true;
if (dec.color != null) {
painter.colorFilter = ColorFilter.mode(dec.color!, BlendMode.srcIn);
}
}
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration config) async {
double? width = dec.width;
double? height = dec.height;
Size iSize = config.size!;
if (width == null || height == null) {
width = dec.image!.width.toDouble();
height = dec.image!.height.toDouble();
}
// 计算图片绘制中心坐标
double xAxis = offset.dx;
double yAxis = offset.dy + iSize.height - height / 2 - dec.marginBottom!;
if (dec.alignment == IndicatorAxisAlignment.center) {
xAxis = offset.dx + iSize.width / 2;
} else if (dec.alignment == IndicatorAxisAlignment.end) {
xAxis = offset.dx + iSize.width + dec.marginRight!;
}
Offset centerOffset = Offset(xAxis, yAxis);
Rect rect =
Rect.fromCenter(center: centerOffset, width: width, height: height);
canvas.drawImageNine(dec.image!, rect, rect, painter);
}
}
class _RoundRectPainter extends BoxPainter {
final RoundRectIndicator dec;
final VoidCallback? onChanged;
late Paint painter;
_RoundRectPainter({required this.dec,required this.onChanged}) : super() {
painter = Paint();
painter.style = PaintingStyle.fill;
painter.isAntiAlias = true;
if (dec.color != null) {
painter.colorFilter = ColorFilter.mode(dec.color!, BlendMode.srcIn);
}
}
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration config) async {
Rect rect = _indicatorRectFor(offset & config.size!, config.textDirection!);
RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(dec.round));
canvas.drawRRect(rRect, painter);
}
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
Rect indicator = EdgeInsets.zero.resolve(textDirection).deflateRect(rect);
double? width = dec.width;
double left = indicator.left;
if (width == null) {
width = indicator.width;
} else {
left += (indicator.width - width) / 2;
}
return Rect.fromLTWH(
left,
indicator.bottom - dec.height! - dec.marginBottom!,
width,
dec.height!,
);
}
}
library navigation_bar;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'indicator.dart' as indicator;
import './i_tabbar.dart';
export 'indicator.dart' show IndicatorAxisAlignment;
class NavigationBar extends StatefulWidget {
final List<String>? items;
final double? labelWidth;
final ItemBuilderDelegate? builderDelegate;
final TabController? controller;
final Color selectColor;
final Color normalColor;
final TextStyle? selectStyle;
final TextStyle? normalStyle;
final _Indicator? indicator;
final bool isScrollable;
final TabBarIndicatorSize indicatorSize;
final EdgeInsetsGeometry? labelPadding;
final Function(int? index)? onChange;
final Function(int)? onTap;
NavigationBar(
{this.items,
this.labelWidth,
this.builderDelegate,
this.controller,
this.selectStyle,
this.normalStyle,
this.indicator,
this.onChange,
this.onTap,
this.labelPadding,
this.isScrollable = false,
this.selectColor = Colors.black,
this.normalColor = Colors.black12,
this.indicatorSize = TabBarIndicatorSize.label,
Key? key})
: super(key: key) {
assert(items != null || builderDelegate != null, '列表项不能为空');
}
@override
State<StatefulWidget> createState() => NavigationBarState();
}
class NavigationBarState extends State<NavigationBar> {
int? _index;
TabController? _oldController;
Future<ui.Image?> get indicatorImage async {
if (isImageIndicator) {
ImageIndicator indicator = widget.indicator as ImageIndicator;
var image = await loadImage(indicator.image);
return image;
}
return null;
}
Future<Decoration?> get indicatorDecoration async {
if (isImageIndicator) {
ImageIndicator imageIndicator = widget.indicator as ImageIndicator;
return indicator.ImageIndicator(
image: await indicatorImage,
color: imageIndicator.color,
width: imageIndicator.width,
height: imageIndicator.height,
alignment: imageIndicator.alignment,
marginBottom: imageIndicator.marginBottom,
marginRight: imageIndicator.marginRight);
} else {
RoundRectIndicator rectIndicator = widget.indicator as RoundRectIndicator;
return indicator.RoundRectIndicator(
color: rectIndicator.color,
width: rectIndicator.width,
height: rectIndicator.height,
round: rectIndicator.round,
marginBottom: rectIndicator.marginBottom,
marginRight: rectIndicator.marginRight,
);
}
}
bool get isImageIndicator {
return widget.indicator is ImageIndicator;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
initEvnet();
}
@override
void didUpdateWidget(covariant NavigationBar oldWidget) {
super.didUpdateWidget(oldWidget);
initEvnet();
}
@override
void dispose() {
if (_oldController != null) {
_oldController!.removeListener(listenerCallback);
}
super.dispose();
}
void initEvnet() {
if (_oldController != _getTabController()) {
_oldController = _getTabController();
_getTabController()!.removeListener(listenerCallback);
_getTabController()!.addListener(listenerCallback);
}
}
void listenerCallback() {
if (_index == _getTabController()!.index) {
return;
}
_index = _getTabController()!.index;
if (widget.onChange != null) {
widget.onChange!(_index);
}
}
TabController? _getTabController() {
if (widget.controller != null) {
return widget.controller;
} else {
return DefaultTabController.of(context);
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Decoration?>(
future: indicatorDecoration,
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
List<Widget> tabs;
if (widget.builderDelegate != null) {
tabs = List.generate(widget.builderDelegate!.childCount, (index) {
return widget.builderDelegate!.builder(context, index);
});
} else {
tabs = widget.items!.map((text) {
return Container(
width: widget.labelWidth,
height: double.infinity,
alignment: Alignment.center,
child: Text(
text,
));
}).toList();
}
return ITabBar(
tabs: tabs,
labelPadding: widget.labelPadding,
controller: widget.controller,
isScrollable: widget.isScrollable,
onTap: (index) {
if (widget.onTap != null) {
widget.onTap!(index);
}
},
labelStyle:
TextStyle.lerp(TextStyle(height: 1), widget.selectStyle, 1),
unselectedLabelStyle:
TextStyle.lerp(TextStyle(height: 1), widget.normalStyle, 1),
labelColor: widget.selectColor,
unselectedLabelColor: widget.normalColor,
indicatorSize: widget.indicatorSize,
indicator: snapshot.data,
indicatorColor: Colors.transparent,
);
});
}
Future<ui.Image> loadImage(String path) async {
var data = await rootBundle.load(path);
var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
var info = await codec.getNextFrame();
return info.image;
}
}
class _Indicator {
final Color? color;
final double? width;
final double? height;
final double? marginBottom;
final double? marginRight;
_Indicator(
{this.color,
this.height,
this.width,
this.marginBottom = 0,
this.marginRight = 0});
}
class ImageIndicator extends _Indicator {
String image;
indicator.IndicatorAxisAlignment alignment;
ImageIndicator(
{required this.image,
Color? color,
double? width,
double? height,
this.alignment = indicator.IndicatorAxisAlignment.center,
double marginBottom = 0,
double marginRight = 0})
: super(
color: color,
width: width,
height: height,
marginBottom: marginBottom,
marginRight: marginRight);
}
class RoundRectIndicator extends _Indicator {
double round;
RoundRectIndicator(
{this.round = 1,
required Color color,
double? width,
double height = 2,
double? marginBottom,
double? marginRight})
: super(
color: color,
width: width,
height: height,
marginBottom: marginBottom,
marginRight: marginRight);
}
class ItemBuilderDelegate {
final int childCount;
final Widget Function(BuildContext context, int index) builder;
ItemBuilderDelegate({required this.childCount, required this.builder});
}
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.8.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.7.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.4.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
sdks:
dart: ">=2.12.0 <3.0.0"
name: navigation_bar
description: A new Flutter package.
version: 1.0.0
homepage:
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment