亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

如何利用Flutter實(shí)現(xiàn)酷狗流暢Tabbar效果

 更新時(shí)間:2022年02月20日 16:33:02   作者:Karl_wei  
這篇文章主要給大家介紹了關(guān)于如何利用Flutter實(shí)現(xiàn)酷狗流暢Tabbar效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下

前言

在2021年末,酷狗發(fā)布了最新版11.0.0版本,這是一次重大的UI重構(gòu),更新完打開(kāi)著實(shí)讓我耳目一新。在原有風(fēng)格上,整個(gè)App變得更加清爽,流暢。其中Tabbar的風(fēng)格讓我非常感興趣,如果用Flutter來(lái)實(shí)現(xiàn),或許是一個(gè)很有趣的事情。

效果圖

分析效果

研究酷狗Tabbar的動(dòng)畫(huà)可以發(fā)現(xiàn),默認(rèn)狀態(tài)下在當(dāng)前Tab的中心處展示圓點(diǎn),滑動(dòng)時(shí)的效果拆分成兩個(gè)以下部分:

  • 從單個(gè)Tab A的中心根據(jù)X軸平移到Tab B的中心位置;
  • 指示器的長(zhǎng)度從圓點(diǎn)變長(zhǎng),再縮短為圓點(diǎn)。其中最大長(zhǎng)度是可變的,跟兩個(gè)Tab的大小和距離都有關(guān)系;
  • 指示器雖然依賴Tab的size和offset來(lái)變換,但和Tab卻基本是同一時(shí)間渲染的,整個(gè)過(guò)程非常順滑;
  • 總的來(lái)說(shuō),酷狗的效果就是改變了指示器的渲染動(dòng)畫(huà)而已。

開(kāi)發(fā)思路

從上面的分析可以明確,指示器的滑動(dòng)效果一定跟每個(gè)Tab的size和offset相關(guān)。那在Flutter中,獲取渲染信息我們馬上能想到GlobalKey,通過(guò)GlobalKey的currentContext對(duì)象獲取Rander信息,但這必須在視圖渲染完成后才能獲取,也就是說(shuō)Tab渲染完才能開(kāi)始計(jì)算并渲染指示器。很顯然不符合體驗(yàn)要求,同時(shí)頻繁使用GlobalKey也會(huì)導(dǎo)致性能較差。

轉(zhuǎn)變思路,我們需要在Tab渲染的不斷把信息傳給指示器,然后更新指示器,這種方式自然想到了CustomPainter【之前寫(xiě)了很多Canvas的控件,都是根據(jù)傳入的值進(jìn)行繪制,從而實(shí)現(xiàn)控件的變化了layout類】。在Tab updateWidget的時(shí)候,不斷把Rander的信息傳給畫(huà)筆Painter,然后更新繪制,理論上這樣做是完全行得通的。

Flutter Tabbar 解析源碼

為了驗(yàn)證我的思路,我開(kāi)始研究官方Tabbar是如何寫(xiě)的:

  • 進(jìn)入TabBar類,直接查看build方法,可以看到為每個(gè)Tab加入了Globalkey,然后指示器用CustomPaint進(jìn)行繪制;
Widget build(BuildContext context) {
  
  // ...此處省略部分代碼...
  
  final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
    const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
    EdgeInsetsGeometry? adjustedPadding;
    // 這里為tab加入Globalkey,以便后續(xù)獲取Tab的渲染信息
    if (widget.tabs[index] is PreferredSizeWidget) {
      final PreferredSizeWidget tab = widget.tabs[index] as PreferredSizeWidget;
      if (widget.tabHasTextAndIcon && tab.preferredSize.height == _kTabHeight) {
        if (widget.labelPadding != null || tabBarTheme.labelPadding != null) {
          adjustedPadding = (widget.labelPadding ?? tabBarTheme.labelPadding!).add(const EdgeInsets.symmetric(vertical: verticalAdjustment));
        }
        else {
          adjustedPadding = const EdgeInsets.symmetric(vertical: verticalAdjustment, horizontal: 16.0);
        }
      }
    }
    
    // ...此處省略部分代碼...
    
    // 可以看到指示器是CustomPaint對(duì)象
    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: _saveTabOffsets,
              children: wrappedTabs,
        ),
      ),
    );
  • 繪制指示器用CustomPaint跟我們的預(yù)想一致,那如何把繪制的size和offset傳進(jìn)去呢。我們來(lái)看_TabLabelBar繼承于Flex,而Flex又繼承自MultiChildRenderObjectWidget,重寫(xiě)其createRenderObject方法;
class _TabLabelBar extends Flex {
  _TabLabelBar({
    Key? key,
    List<Widget> children = const <Widget>[],
    required this.onPerformLayout,
  }) : super(
    key: key,
    children: children,
    direction: Axis.horizontal,
    mainAxisSize: MainAxisSize.max,
    mainAxisAlignment: MainAxisAlignment.start,
    crossAxisAlignment: CrossAxisAlignment.center,
    verticalDirection: VerticalDirection.down,
  );

  final _LayoutCallback onPerformLayout;

  @override
  RenderFlex createRenderObject(BuildContext context) {
    // 查看下_TabLabelBarRenderer
    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;
  }
}

查看真實(shí)的渲染對(duì)象:_TabLabelBarRenderer,在performLayout中返回渲染的size和offset,并通過(guò)TabBar傳入的_saveTabOffsets方法保存到_indicatorPainter中;_saveTabOffsets尤為重要,把Tabbar的渲染位移通知給Painter,從而讓Painter可以輕松算出tab之間的寬度差

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),
       assert(textDirection != null),
       super(
         children: children,
         direction: direction,
         mainAxisSize: mainAxisSize,
         mainAxisAlignment: mainAxisAlignment,
         crossAxisAlignment: crossAxisAlignment,
         textDirection: textDirection,
         verticalDirection: verticalDirection,
       );

  _LayoutCallback onPerformLayout;

  @override
  void performLayout() {
    super.performLayout();
    // xOffsets will contain childCount+1 values, giving the offsets of the
    // leading edge of the first tab as the first value, of the leading edge of
    // the each subsequent tab as each subsequent value, and of the trailing
    // edge of the last tab as the last value.
    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);
    switch (textDirection!) {
      case TextDirection.rtl:
        xOffsets.insert(0, size.width);
        break;
      case TextDirection.ltr:
        xOffsets.add(size.width);
        break;
    }
    onPerformLayout(xOffsets, textDirection!, size.width);
  }
}
  • 通過(guò)Tabbar中的didChangeDependencies和didUpdateWidget生命周期,更新指示器;
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  assert(debugCheckHasMaterial(context));
  final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  _updateTabController();
  _initIndicatorPainter(adjustedPadding, tabBarTheme);
}

@override
void didUpdateWidget(KuGouTabBar oldWidget) {
  super.didUpdateWidget(oldWidget);
  final TabBarTheme tabBarTheme = TabBarTheme.of(context);
  if (widget.controller != oldWidget.controller) {
    _updateTabController();
    _initIndicatorPainter(adjustedPadding, tabBarTheme);
  } else if (widget.indicatorColor != oldWidget.indicatorColor ||
      widget.indicatorWeight != oldWidget.indicatorWeight ||
      widget.indicatorSize != oldWidget.indicatorSize ||
      widget.indicator != oldWidget.indicator) {
    _initIndicatorPainter(adjustedPadding, tabBarTheme);
  }

  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);
  }
}
  • 然后重點(diǎn)就在指示器_IndicatorPainter如何進(jìn)行繪制了。

實(shí)現(xiàn)步驟

通過(guò)理解Flutter Tabbar的實(shí)現(xiàn)思路,大體跟我們預(yù)想的差不多。不過(guò)官方繼承了Flex來(lái)計(jì)算Offset和size,實(shí)現(xiàn)起來(lái)很優(yōu)雅。所以我也不班門(mén)弄斧了,直接改動(dòng)官方的Tabbar就可以了。

  • 創(chuàng)建KuGouTabbar,復(fù)制官方代碼,修改引用,刪除無(wú)關(guān)的類,只保留Tabbar相關(guān)的代碼。

2. 重點(diǎn)修改_IndicatorPainter,根據(jù)我們的需求來(lái)繪制指示器。在painter方法中,我們可以通過(guò)controller拿到當(dāng)前tab的index以及animation!.value, 我們模擬下切換的過(guò)程,當(dāng)tab從第0個(gè)移到第1個(gè),動(dòng)畫(huà)的值從0變成1,然后動(dòng)畫(huà)走到0.5時(shí),tab的index會(huì)從0突然變?yōu)?,指示器應(yīng)該是先變長(zhǎng),然后在動(dòng)畫(huà)走到0.5時(shí),再變短。因此動(dòng)畫(huà)0.5之前,我們用動(dòng)畫(huà)的value-index作為指示器縮放的倍數(shù),指示器不斷增大;動(dòng)畫(huà)0.5之后,用index-value作為縮放倍數(shù),不斷縮小。

final double index = controller.index.toDouble();

final double value = controller.animation!.value;
/// 改動(dòng) ltr為false,表示索引還是0,動(dòng)畫(huà)執(zhí)行未超過(guò)50%;ltr為true,表示索引變?yōu)?,動(dòng)畫(huà)執(zhí)行超過(guò)50%
final bool ltr = index > value;
final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex);
final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex);

/// 改動(dòng) 通過(guò)ltr來(lái)決定是放大還是縮小倍數(shù),可以得出公式:ltr ? (index - value) : (value - index)
final Rect fromRect =
    indicatorRect(size, from, ltr ? (index - value) : (value - index));

/// 改動(dòng)
final Rect toRect =
    indicatorRect(size, to, ltr ? (index - value) : (value - index));
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());

而指示器接收縮放倍數(shù)的前提還需要計(jì)算指示器最大的寬度,并且上面是根據(jù)動(dòng)畫(huà)的0.5作為最大的寬度,也就是移動(dòng)到一半的時(shí)候,指示器應(yīng)該達(dá)到最大寬度。因此指示器最大的寬度是需要??2的。請(qǐng)看下面代碼:

class _IndicatorPainter extends CustomPainter {
  ......此處省略部分代碼......

  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;
  }

  /// 接收上面代碼分析中傳入的倍數(shù) scale
  Rect indicatorRect(Size tabBarSize, int tabIndex, double scale) {
    assert(_currentTabOffsets != null);
    assert(_currentTextDirection != null);
    assert(_currentTabOffsets!.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    double tabLeft, tabRight, tabWidth = 0;
    switch (_currentTextDirection!) {
      case TextDirection.rtl:
        tabLeft = _currentTabOffsets![tabIndex + 1];
        tabRight = _currentTabOffsets![tabIndex];
        break;
      case TextDirection.ltr:
        tabLeft = _currentTabOffsets![tabIndex];
        tabRight = _currentTabOffsets![tabIndex + 1];
        break;
    }

    /// 改動(dòng),通過(guò)GlobalKey計(jì)算出渲染的文本的寬度
    tabWidth = tabKeys[tabIndex].currentContext!.size!.width;
    final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0;
    tabLeft += delta;
    tabRight -= delta;

    final EdgeInsets insets = indicatorPadding.resolve(_currentTextDirection);

    /// 改動(dòng),算出指示器的最大寬度,記得*2
    double maxLen = (tabRight - tabLeft + insets.horizontal) * 2;

    double res =
        scale == 0 ? minWidth : maxLen * (scale < 0.5 ? scale : 1 - scale);

    /// 改動(dòng)
    final Rect rect = Rect.fromLTWH(tabLeft + tabWidth / 2 - minWidth / 2, 0.0, res > minWidth ? res : minWidth, tabBarSize.height);

    if (!(rect.size >= insets.collapsedSize)) {
      throw FlutterError(
        'indicatorPadding insets should be less than Tab Size\n'
        'Rect Size : ${rect.size}, Insets: ${insets.toString()}',
      );
    }
    return insets.deflateRect(rect);
   }
}
  • 如上,指示器的寬度我們根據(jù)controller切換時(shí)的index和動(dòng)畫(huà)值進(jìn)行轉(zhuǎn)化,實(shí)現(xiàn)寬度的變化。而Offset的最小值和最大值分別是切換前后兩個(gè)Tab的中心點(diǎn),這里應(yīng)該做下相應(yīng)的的限制,然后傳給Rect.fromLTWH。

【由于時(shí)間和精力問(wèn)題,我并沒(méi)有去做這一步的實(shí)現(xiàn),而且酷狗那邊動(dòng)畫(huà)跟滑動(dòng)邏輯的關(guān)系需要UI給出具體的公式,才能百分百還原?!?/p>

最后就是加多一個(gè)參數(shù),讓業(yè)務(wù)方傳入指示器的最小寬度。

/// 指示器的最小寬度
final double indicatorMinWidth;

業(yè)務(wù)使用

在上面我們已經(jīng)把簡(jiǎn)單的動(dòng)畫(huà)效果改完了,接下來(lái)就是傳入圓角的indicator、最小寬度indicatorMinWidth,就可以正常使用啦。

  • 圓角的指示器,我直接上源碼
import 'package:flutter/material.dart';

class RRecTabIndicator extends Decoration {
  const RRecTabIndicator(
      {this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
        this.insets = EdgeInsets.zero,
        this.radius = 0,
        this.color = Colors.white});

  final double radius;
  final Color color;
  final BorderSide borderSide;
  final EdgeInsetsGeometry insets;

  @override
  Decoration? lerpFrom(Decoration? a, double t) {
    if (a is RRecTabIndicator) {
      return RRecTabIndicator(
        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  Decoration? lerpTo(Decoration? b, double t) {
    if (b is RRecTabIndicator) {
      return RRecTabIndicator(
        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  _UnderlinePainter createBoxPainter([VoidCallback? onChanged]) {
    return _UnderlinePainter(this, onChanged);
  }

  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
    return Rect.fromLTWH(
      indicator.left,
      indicator.bottom - borderSide.width,
      indicator.width,
      borderSide.width,
    );
  }

  @override
  Path getClipPath(Rect rect, TextDirection textDirection) {
    return Path()..addRect(_indicatorRectFor(rect, textDirection));
  }
}

class _UnderlinePainter extends BoxPainter {
  _UnderlinePainter(this.decoration, VoidCallback? onChanged)
      : super(onChanged);

  final RRecTabIndicator decoration;

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final Rect rect = offset & configuration.size!;
    final TextDirection textDirection = configuration.textDirection!;
    final Rect indicator = decoration._indicatorRectFor(rect, textDirection);
    final Paint paint = decoration.borderSide.toPaint()
      ..strokeCap = StrokeCap.square
      ..color = decoration.color;
    final RRect rRect =
    RRect.fromRectAndRadius(indicator, Radius.circular(decoration.radius));
    canvas.drawRRect(rRect, paint);
  }
}
  • 調(diào)用非常簡(jiǎn)單,跟原來(lái)官方代碼一模一樣。
Scaffold(
  appBar: AppBar(
    // Here we take the value from the MyHomePage object that was created by
    // the App.build method, and use it to set our appbar title.
    title: Text(widget.title),
    bottom: KuGouTabBar(
      tabs: const [Tab(text: "音樂(lè)"), Tab(text: "動(dòng)態(tài)"), Tab(text: "語(yǔ)文")],
      // labelPadding: EdgeInsets.symmetric(horizontal: 8),
      controller: _tabController,
      // indicatorSize: TabBarIndicatorSize.label,
      // isScrollable: true,
      padding: EdgeInsets.zero,
      indicator: const RRecTabIndicator(
          radius: 4, insets: EdgeInsets.only(bottom: 5)),
      indicatorMinWidth: 6,
    ),
  ),
);  

寫(xiě)在最后

模仿酷狗的Tabbar效果,就分享到這里啦,重點(diǎn)在于實(shí)現(xiàn)步驟的第2、3步,涉及到一些簡(jiǎn)單的數(shù)學(xué)知識(shí)。說(shuō)說(shuō)心得吧,F(xiàn)lutter UI層面的問(wèn)題,其實(shí)技術(shù)棧已經(jīng)很單一了。只要跟著官方的實(shí)現(xiàn)思路,能寫(xiě)出跟其類似的代碼,把Rander層理解透徹,筆者認(rèn)為已經(jīng)足夠了。往深了還是得往原生、混編、解決Flutter痛點(diǎn)問(wèn)題為主。 希望一起共勉?。?!

實(shí)現(xiàn)源碼

https://github.com/WxqKb/KuGouTabbar.git

到此這篇關(guān)于如何利用Flutter實(shí)現(xiàn)酷狗流暢Tabbar效果的文章就介紹到這了,更多相關(guān)Flutter實(shí)現(xiàn)酷狗Tabbar效果內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論