TabBar is a very common UI component. The TabBar provided by Flutter can meet most of our business needs and is very simple to implement. We can swipe a Tab with just a few lines of code.

I won’t go through the basics of TabBar, but if you’re not familiar with it, you can go to Dojo and try it out.

Let’s take a look at some of the problems TabBar has encountered in its development.

Jitter problem

First of all, let’s take a look at TabBar jitter. This problem occurs when we set labelStyle and unselectedLabelStyle font sizes to be inconsistent. This problem is also common. But the TabBar of Flutter started to shake during the slide. I thought it was a Debug package problem, but later I found out that the Release was the same.

The Issue of Flutter already has this problem. The address is as follows:

Github.com/flutter/flu…

But so far, this problem has not been fixed, probably in the foreign design, again there is no such design bar. However, there are many solutions mentioned in the Issue to fix this problem, one of the better solutions is to modify the source code, in the TabBar source code _TabStyle build function, change the implementation to the following solution.

Final double _magnification = labelStyle! .fontSize! / unselectedLabelStyle! .fontSize! ; final double _scale = (selected ? lerpDouble(_magnification, 1, animation.value) : lerpDouble(1, _magnification, animation.value))! ; return DefaultTextStyle( style: textStyle.copyWith( color: color, fontSize: unselectedLabelStyle! .fontsize,), child: IconTheme. Merge (data: IconThemeData(size: 24.0, color: color,), child: transform.scale (scale: _scale, child: child, ), ), );Copy the code

Is there any other solution? In fact, the Issue has already given the source of the problem, which is actually the jitter caused by the inconsistency of Text Baseline in the process of calculating Scala, so, Another way to think about it is to set labelStyle and unselectedLabelStyle to the same font size so that there is no shaking. (MDZZ)

Of course, then we do away with the need…

In fact, we implemented Scala effects outside. In TabBar tabs, we passed in sliding percentages and implemented Scala effects with implicit animation, avoiding the jitter problem.

AnimatedScale(scale: 1 + progress * 0.3, duration: const duration (milliseconds: 100), child: Text(tabName),),Copy the code

Is it another village, very simple to solve this problem.

indicator

Indicator is another of TabBar’s annoying goblin-makers. Thanks to the Indicator, TabBar is a great place for designers to have their own freedom. Although The Flutter provides a nice interface for developers to create indicators, But it doesn’t stop some designers from coming up with ideas.

Let’s take a look at some of the more common implementations of Indicator.

Indicator is an Decoration. The inheritance relationship of Decoration in Flutter is as follows.

Shape indicator

The UnderlineTabIndicator is the default Tab implementation. We can change it to ShapeDecoration so that we can implement a simple box indicator.

indicator: ShapeDecoration(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(8),
  ),
  color: Colors.cyan.shade200,
)
Copy the code

The result is as follows.

In this way, we can implement many Shape Indicator. With the help of ShapeDecoration, we can achieve a variety of color styles such as solid color and gradient.

indicatorWeight & indicatorPadding

These two parameters are used to control the size of indicator, as shown in the code below.

indicatorWeight: 4,
indicatorPadding: EdgeInsets.symmetric(vertical: 8),
Copy the code

If you want the indicator to be closer in vertical distance use the indicatorPadding parameter, if you want the indicator to be thinner use the indicatorWeight parameter.

Custom Indicator

You can never escape customization.

First, Copy the UnderlineTabIndicator implemented by the system default. This is our customized template.

After reading the source code, you will understand that the last BoxPainter is the core for drawing the Indicator. Here, according to the Offset and ImageConfiguration, you can get the parameters of the current Indicator and draw it.

For example, our simplest drawing Indicator as a circle actually requires only modifying the final draw function, as shown below.

class _UnderlinePainter extends BoxPainter { _UnderlinePainter(this.decoration, VoidCallback? onChanged) : assert(decoration ! = null), super(onChanged); final QDTabIndicator decoration; final Paint _paint = Paint() .. color = Colors.red .. style = PaintingStyle.fill; Final radius = 4.0; @override void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { assert(configuration ! = null); assert(configuration.size ! = null); final Rect rect = offset & configuration.size! ; canvas.drawCircle( Offset(rect.bottomCenter.dx, rect.bottomCenter.dy - radius), radius, _paint, ); }}Copy the code

The effect is shown below.

One more:

Var width = 20.0; Var height = 4.0; Var bottomMargin = 10.0; final centerX = configuration.size! .width/ 2 + offset.dx; final bottom = configuration.size! .height - bottomMargin; final halfWidth = width / 2; canvas.drawRRect( RRect.fromLTRBR( centerX - halfWidth, bottom - height, centerX + halfWidth, bottom, Radius.circular(radius), ), _paint, );Copy the code

The effect is shown below.

There’s an inside smell. So have you noticed that everything is given to you? How to draw depends on your own skills.

Note, however, that these indicators are based on the UnderlineTabIndicator implementation, so there are some limitations to the style. For example, it must be able to slide under a Tab, and the size cannot be changed while sliding.

Fixed Indicator & slide coefficient monitor

What if I need to remove the indicator slide? There are two options, one is to modify the TabBar source code, and the other is to implement fixed indicators in tabs instead of indicators.

These two methods have their own advantages and disadvantages, modify the source code is to remove from the bottom, can achieve all the effects, the same, the cost is high, another method is simple, but need to get the sliding coefficient.

Now let’s see how to do this by using the second method, and at the same time, to complete the final implementation of the jitter problem introduced earlier (it also needs to get the sliding coefficient).

First, we need to know where to get the sliding coefficient, which we can get by _tabController. This contains all the parameters for TabBar sliding, such as:

  • _tabController.animation! Value: indicates the sliding change range
  • _tabController.offset: sliding direction
  • The Index of _tabController. PreviousIndex: before sliding
  • _tabController.index: indicates the index that is slid to
  • _tabController. IndexIsChanging: sliding or click on the Tab of sliding

These things, these are our raw data, from which we can get the slip coefficient of the current slide.

tabs: widget.tabs .asMap() .entries .map( (entry) => AnimatedBuilder( animation: _tabController.animation! , builder: (ctx, snapshot) { final forward = _tabController.offset > 0; final backward = _tabController.offset < 0; int _fromIndex; int _toIndex; double progress; // Tab if (_tabController.indexIsChanging) { _fromIndex = _tabController.previousIndex; _toIndex = _tabController.index; progress = (_tabController.animation! .value - _fromIndex).abs() / (_toIndex - _fromIndex).abs(); } else { // Scroll _fromIndex = _tabController.index; _toIndex = forward ? _fromIndex + 1 : backward ? _fromIndex - 1 : _fromIndex; progress = (_tabController.animation! .value - _fromIndex).abs(); } var flag = entry.key == _fromIndex ? 1 - progress : entry.key == _toIndex ? Progress: 0.0; return buildTabContainer(entry.value, flag); }, ), ) .toList(),Copy the code

First, we use offset to determine the sliding direction, then use indexIsChanging to determine whether the current click or slide, and finally obtain the current sliding coefficient according to animation.

With sliding coefficients, we can easily animate Tab titles in Scala and fixed indicators at the same time.

buildTabContainer(String tabName, double alpha) { return Container( padding: const EdgeInsets.symmetric(vertical: Number of milliseconds, child: Stack(children: [AnimatedScale(scale: 1 + alpha * 0.3, duration: const duration (milliseconds: 100), child: Text(tabName), ), Positioned( bottom: 0, left: 0, child: Transform.translate( offset: const Offset(-8, 0), child: Opacity( opacity: alpha, child: Container( width: 30, height: 8, decoration: const BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.only( topLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), gradient: LinearGradient(colors: [ QDColors.tab_gradient_start, QDColors.tab_gradient_end, ]), ), ), ), ), ), ], ), ); }Copy the code

I’m not going to leave you with the picture.

Material effect indicator

In response to the calls of our friends, we have added Material effect Indicators. It is very strange that the official government does not support this Material style, with flexible indicators, Native support.

The original Indicator is fixed size when sliding. In the Tabbar source code, we find _IndicatorPainter. This CustomPainter is responsible for drawing the Indicator. We have to change the width of the drawing. Obviously, we go to paint, and there we find two Rects — fromRect and toRect — that perform the LERP operation that we want for Indicator.

So, the idea comes out naturally, we just need to change the width of the current Rect according to the progress of the current slide.

Then there is another question, is the Material style flex animation, how to achieve?

Open CircularProgressIndicator source, we look at it how animation is done.

Static final Animatable<double> _strokeHeadTween = CurveTween(curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), ).chain(CurveTween( curve: const SawTooth(_pathCount), )); Static final Animatable<double> _strokeTailTween = CurveTween(curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), ).chain(CurveTween( curve: const SawTooth(_pathCount), ));Copy the code

Obviously, the animation is divided into two parts, increasing with the scaling factor at 0-0.5 and decreasing with the scaling factor at 0.5-1.

So, first, we add a parameter scale to indicatorRect, which passes in the sliding ratio between the current two tabs.

// if (indicatorSize == FixedTabBarIndicatorSize.label) { final double tabWidth = tabKeys[tabIndex].currentContext! .size! .width; Final double delta = ((tabright-tableft) -tabWidth) / 2.0; tabLeft += delta; tabRight -= delta; // } final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection); Double minWidth = 10.0; Double increment = 40.0; Double currentWidth = minWidth + increment * (scale < 0.5? scale : 1 - scale); // Final Rect Rect = rect.fromltwh (tabLeft, 0.0, tabright-tableft, tabbarsie.height); // Final Rect = rect.fromltwh (tabLeft, 0.0, tabright-tableft, tabbarsie.height); Final Rect Rect = rect. fromLTWH(tabLeft + tabWidth / 2-minwidth / 2, 0.0, math.max(currentWidth, minWidth), tabBarSize.height);Copy the code

The second change is to change the left and width of the rect, left to center the rect. Width is to change the width of the animation, the code is very simple.

Next, I need to get the scale, which is actually half written in the source code.

// final Rect fromRect = indicatorRect(size, from);
final Rect fromRect = indicatorRect(size, from, (index - value).abs());
// final Rect toRect = indicatorRect(size, to);
final Rect toRect = indicatorRect(size, to, (index - value).abs());
Copy the code

Again, the commented out line is the source code, so let’s add a scale, which is actually the absolute value of index and value, because of the direction.

So, in just a few lines of code, we’ve changed the Material function, but this is just the most basic change. If you want to form a complete lib, you need to extract these parameters as configuration options, which I won’t do because I’m lazy.

The previous fixed Indicator is also shown along the way.

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit