利用Flutter 實(shí)現(xiàn)任意tab切換效果

處理和響應(yīng)觸摸效果

我們可以用GestureDetector實(shí)現(xiàn)這個效果

GestureDetector(
  ///手勢觸摸移動開始,這里我們可以記錄開始的觸摸點(diǎn),用來判斷移動比例和動畫的初始點(diǎn)
  onHorizontalDragStart: onStart,
  ///手勢觸摸移動中,這里生成tab的切換效果,具體效果可以用戶自定義,效果代碼都在delegate類里.
  onHorizontalDragUpdate: onUpdate,
  ///手勢觸摸結(jié)束,這里判斷是切換到下一張卡片還是滑動失敗,回滾當(dāng)前tab
  onHorizontalDragEnd: onEnd,
  child: child,
);

觸摸開始

///記錄觸摸初始點(diǎn)
onStart(DragStartDetails details) {
  dragStart = details.globalPosition;
  ...
}

觸摸移動中

onUpdate(DragUpdateDetails details) {
  if (dragStart != null) {
    ///滑動方向,向左或向右
    SlideDirection slideDirection;
    ///滑動進(jìn)度.[0, 1]
    double slidePercent = 0.0;
    ///當(dāng)前觸摸的點(diǎn)
    final newPosition = details.globalPosition;

    ///拖動距離,如果大于零是向右拖動,如果小于零是向左拖動.
    ///當(dāng)前點(diǎn)的x軸位置減去觸摸起始點(diǎn)的x軸位置
    final dx = newPosition.dx - dragStart.dx;
    slidePercent = (dx / FULL_TRANSITION_PX).abs().clamp(0.0, 1.0).toDouble();
    if (dx > 0) {
      slideDirection = SlideDirection.leftToRight;
    } else if (dx < 0) {
      slideDirection = SlideDirection.rightToLeft;
    } else {
      slideDirection = SlideDirection.none;
      slidePercent = 0;
    }
   ...
  }
}

動畫處理

我們在觸摸手勢結(jié)束后開始動畫處理,動畫分為兩個,一個是滑動成功的動畫切換到下一個tab,一個是滑動失敗(比如滑動距離很小,不需要跳轉(zhuǎn)到下一個頁面).這里的value是觸摸手勢的滑動比例和Animation的value,它們兩個的值是相同的,這樣可以有連貫的動畫效果.

onAnimatedStart({SlideUpdate slideUpdate}) {
  Duration duration;
  ///判斷是否成功, 滑動的值 是否大于我們設(shè)置的滑動成功的比例,我們這里設(shè)置的是0.5.
  _isSlideSuccess = value >= slideSuccessProportion;
  ///成功
  if (_isSlideSuccess) {
    final slideRemaining = 1.0 - value;
    ///計(jì)算tab切換的時間
    duration = Duration(
        milliseconds: (slideRemaining / PERCENT_PER_MILLISECOND).round());
    _animationController.duration = duration;
    ///動畫向前運(yùn)行到1,動畫結(jié)束后切換當(dāng)前tab為下一頁tab
    _animationController.forward(from: value).whenComplete(() =>
        animationCompleted());
  } else {
    ///失敗,回退當(dāng)當(dāng)前tab
    duration =
        Duration(milliseconds: (value / PERCENT_PER_MILLISECOND).round());
    _animationController.duration = duration;
    ///將動畫值回退到0.
    _animationController.reverse(from: value);
  }
}

效果自定義

這里用了AnyTabDelegate抽象類,我們可以繼承這個抽象類來實(shí)現(xiàn)任意效果.這樣做最大的好處就是分離ui和邏輯的處理.

abstract class AnyTabDelegate {
  ///tab列表
  List<Widget> tabs;

  AnyTabDelegate({@required this.tabs});

  int get length => tabs.length;

  ///邏輯處理后調(diào)用的build
  Widget build(
    BuildContext context,
    ///當(dāng)前tab頁
    int activeIndex,
    ///下一頁
    int nextPageIndex,
    ///動畫值,它的value就是手勢觸摸的值和動畫執(zhí)行的值.
    Animation animation,
    ///觸摸的初始點(diǎn),用于動畫的初始點(diǎn)
    Offset startingOffset,
  );
}

這里我們來看一下CircularAnyTabDelegate的實(shí)現(xiàn),這里我們用了ClipOval來剪裁下一頁要顯示的tab,如果傳入的percentage是0則完全不顯示,是1這完全顯示.

class CircularAnyTabDelegate extends AnyTabDelegate {
  CircularAnyTabDelegate({@required List<Widget> tabs})
      : assert(tabs != null && tabs.length > 0),
        super(tabs: tabs);

  @override
  Widget build(BuildContext context, int activeIndex, int nextPageIndex,
      Animation animation, Offset startingOffset) {
    return Stack(
      children: [
        tabs[activeIndex],
        ClipOval(
          clipper: CircularClipper(
            percentage: animation.value,
            offset: startingOffset,
          ),
          child: tabs[nextPageIndex],
        )
      ],
    );
  }
}

再往下看一下CircularClipper的代碼.

class CircularClipper extends CustomClipper<Rect> {
  ///百分比, 0-> 1,1 => 全部顯示
  final double percentage;
  ///初始點(diǎn)
  final Offset offset;

  const CircularClipper({this.percentage = 0, this.offset = Offset.zero});

  @override
  Rect getClip(Size size) {
    ///計(jì)算觸摸初始點(diǎn)到邊緣四個角的最大距離,也就是我們剪裁圓的半徑
    double maxValue = maxLength(size, offset) * percentage;
    return Rect.fromLTRB(-maxValue + offset.dx, -maxValue + offset.dy, maxValue + offset.dx, maxValue + offset.dy);
  }

  @override
  bool shouldReclip(CircularClipper oldClipper) {
    return percentage != oldClipper.percentage || offset != oldClipper.offset;
  }

  ///     |
  ///   1 |  2
  /// ---------
  ///   3 |  4
  ///     |
  /// 計(jì)算矩形內(nèi)點(diǎn)到邊緣的最大距離,這里我們把矩形分成四塊,
  /// 點(diǎn)在那一塊,最大的距離就是這個點(diǎn)到對角矩形最遠(yuǎn)那個點(diǎn)的距離
  double maxLength(Size size, Offset offset) {
    double centerX = size.width / 2;
    double centerY = size.height / 2;
    if (offset.dx < centerX && offset.dy < centerY) {
      ///1
      return getEdge(size.width - offset.dx, size.height - offset.dy);
    } else if (offset.dx > centerX && offset.dy < centerY) {
      ///2
      return getEdge(offset.dx, size.height - offset.dy);
    } else if (offset.dx < centerX && offset.dy > centerY) {
      ///3
      return getEdge(size.width - offset.dx, offset.dy);
    } else {
      ///4
      return getEdge(offset.dx, offset.dy);
    }
  }

  double getEdge(double width, double height) {
    return sqrt(pow(width, 2) + pow(height, 2));
  }
}

原文鏈接:https://juejin.cn/post/6900062734994538509

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巷蚪,一起剝皮案震驚了整個濱河市叶雹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弧呐,老刑警劉巖管引,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戳葵,死亡現(xiàn)場離奇詭異,居然都是意外死亡汉匙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門生蚁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來噩翠,“玉大人,你說我怎么就攤上這事邦投∩嗣” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵志衣,是天一觀的道長屯援。 經(jīng)常有香客問我猛们,道長,這世上最難降的妖魔是什么狞洋? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任弯淘,我火速辦了婚禮,結(jié)果婚禮上吉懊,老公的妹妹穿的比我還像新娘庐橙。我一直安慰自己,他們只是感情好借嗽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布态鳖。 她就那樣靜靜地躺著,像睡著了一般恶导。 火紅的嫁衣襯著肌膚如雪浆竭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天惨寿,我揣著相機(jī)與錄音邦泄,去河邊找鬼。 笑死缤沦,一個胖子當(dāng)著我的面吹牛虎韵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缸废,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼包蓝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了企量?” 一聲冷哼從身側(cè)響起测萎,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎届巩,沒想到半個月后硅瞧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恕汇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年腕唧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘾英。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡枣接,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缺谴,到底是詐尸還是另有隱情但惶,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站膀曾,受9級特大地震影響县爬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜添谊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一财喳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碉钠,春花似錦纲缓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至污筷,卻和暖如春工闺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓣蛀。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工陆蟆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惋增。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓叠殷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诈皿。 傳聞我的和親對象是個殘疾皇子林束,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容