Flutter自定義之刻度尺

效果圖
ruler.gif
案例分析
1愿汰、效果功能分析
  • 滑動選擇刻度尺
  • 支持中間選擇刻度值
  • 支持設(shè)置最大最小值
  • 支持設(shè)置默認(rèn)值
  • 支持設(shè)置大刻度的子刻度數(shù)
  • 支持設(shè)置步長
  • 支持設(shè)置刻度尺、數(shù)字的顏色及大小
  • 支持滑動選中回調(diào)
  • 支持刻度尺回彈效果
2续搀、功能拆解
  • 自定義Widget(繼承StatefulWidget)。
  • 使用ListView實(shí)現(xiàn)水平滑動效果(3個(gè)子Widget,左右為空白扛拨,中間為刻度尺)猫缭。
  • 繪制刻度尺Widget(刻度線葱弟、刻度值)。
  • 監(jiān)聽滑動獲取中間值并回調(diào)猜丹。
  • 手指抬起滑動停止粘性回彈芝加。
3、功能參數(shù)
  • 默認(rèn)值
  • 最小值
  • 最大值
  • 步長
  • 刻度尺的寬高
  • 大刻度子子刻度數(shù)
  • 單刻度寬度
  • 刻度線顏色及寬度
  • 刻度尺數(shù)值顏色及寬度
  • 中間刻度線顏色
  • 選擇回調(diào)
4射窒、功能代碼實(shí)現(xiàn)

小知識點(diǎn):
NotificationListener:

if (notification is ScrollStartNotification) {
  print('滾動開始');
}
if (notification is ScrollUpdateNotification) {
  print('滾動中');
}
if (notification is ScrollEndNotification) {
  print('停止?jié)L動');
  if (_scrollController.position.extentAfter == 0) {
    print('滾動到底部');
  }
  if (_scrollController.position.extentBefore == 0) {
    print('滾動到頭部');
  }
}
完整代碼
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

///自定義尺子
class RulerView extends StatefulWidget {
  //默認(rèn)值
  final int value;

  //最小值
  final int minValue;

  //最大值
  final int maxValue;

  //步數(shù) 一個(gè)刻度的值
  final int step;

  //尺子的寬度
  final int width;

  //尺子的高度
  final int height;

  //每個(gè)大刻度的子刻度數(shù)
  final int subScaleCountPerScale;

  //每一刻度的寬度
  final int subScaleWidth;

  //左右空白間距寬度
  double paddingItemWidth;

  //刻度尺選擇回調(diào)
  final void Function(int) onSelectedChanged;

  //刻度顏色
  final Color scaleColor;

  //指示器顏色
  final Color indicatorColor;

  //刻度文字顏色
  final Color scaleTextColor;

  //刻度文字的大小
  final double scaleTextWidth;

  //刻度線的大小
  final double scaleWidth;

  //計(jì)算總刻度數(shù)
  int totalSubScaleCount;

  RulerView({
    Key key,
    this.value = 10,
    this.minValue = 0,
    this.maxValue = 100,
    this.step = 1,
    this.width = 200,
    this.height = 60,
    this.subScaleCountPerScale = 10,
    this.subScaleWidth = 8,
    this.scaleColor = Colors.black,
    this.scaleWidth = 2,
    this.scaleTextColor = Colors.black,
    this.scaleTextWidth = 15,
    this.indicatorColor = Colors.red,
    @required this.onSelectedChanged,
  }) : super(key: key) {
    //檢查最大數(shù)-最小數(shù)必須是步數(shù)的倍數(shù)
    if ((maxValue - minValue) % step != 0) {
      throw Exception("(maxValue - minValue)必須是 step 的整數(shù)倍");
    }
    //默認(rèn)值 不能低于最小值 或者大于最大值
    if (value < minValue || value > maxValue) {
      throw Exception(
          "value 必須在minValue和maxValue范圍內(nèi)(minValue<=value<=maxValue)");
    }
    //總刻度數(shù)
    totalSubScaleCount = (maxValue - minValue) ~/ step;

    //檢查總刻度數(shù)必須是大刻度子刻度數(shù)的倍數(shù)
    if (totalSubScaleCount % subScaleCountPerScale != 0) {
      throw Exception(
          "(maxValue - minValue)~/step 必須是 subScaleCountPerScale 的整數(shù)倍");
    }
    //空白item的寬度
    paddingItemWidth = width / 2;
  }

  @override
  State<StatefulWidget> createState() {
    return RulerState();
  }
}

class RulerState extends State<RulerView> {
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController(
      //初始位置
      initialScrollOffset:
          // ((默認(rèn)值-最小值)/步長 )=第幾個(gè)刻度藏杖,再乘以每個(gè)刻度的寬度就是初始位置
          (widget.value - widget.minValue) / widget.step * widget.subScaleWidth,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width.toDouble(),
      height: widget.height.toDouble(),
      child: Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          NotificationListener(
            onNotification: _onNotification,
            child: ListView.builder(
              physics: ClampingScrollPhysics(),
              padding: EdgeInsets.all(0),
              controller: _scrollController,
              scrollDirection: Axis.horizontal,
              itemCount: 3,
              itemBuilder: (BuildContext context, int index) {
                //2邊的空白控件
                if (index == 0 || index == 2) {
                  return Container(
                    width: widget.paddingItemWidth,
                    height: 0,
                  );
                } else {
                  //刻度尺
                  return Container(
                    child: RealRulerView(
                      subGridCount: widget.totalSubScaleCount,
                      subScaleWidth: widget.subScaleWidth,
                      step: widget.step,
                      minValue: widget.minValue,
                      height: widget.height,
                      scaleColor: widget.scaleColor,
                      scaleWidth: widget.scaleWidth,
                      scaleTextWidth: widget.scaleTextWidth,
                      scaleTextColor: widget.scaleTextColor,
                      subScaleCountPerScale: widget.subScaleCountPerScale,
                    ),
                  );
                }
              },
            ),
          ),
          //指示器
          Container(
            width: 2,
            height: widget.height / 2,
            color: widget.indicatorColor,
          ),
        ],
      ),
    );
  }

  ///監(jiān)聽刻度尺滾動通知
  bool _onNotification(Notification notification) {
    //ScrollNotification是基類 (ScrollStartNotification/ScrollUpdateNotification/ScrollEndNotification)
    if (notification is ScrollNotification) {
      print("-------metrics.pixels-------${notification.metrics.pixels}");
      //距離widget中間最近的刻度值
      int centerValue = widget.minValue +
          //notification.metrics.pixels水平滾動的偏移量
          //先計(jì)算出滾動偏移量是滾動了多少個(gè)刻度,然后取整脉顿,在乘以每個(gè)刻度的刻度值就是當(dāng)前選中的值
          (notification.metrics.pixels / widget.subScaleWidth).round() *
              widget.step;

      // 選中值回調(diào)
      if (widget.onSelectedChanged != null) {
        widget.onSelectedChanged(centerValue);
      }
      //如果是否滾動停止蝌麸,停止則滾動到centerValue
      if (_scrollingStopped(notification, _scrollController)) {
        select(centerValue);
      }
    }
    return true; //停止通知
  }

  ///判斷是否滾動停止
  bool _scrollingStopped(
    Notification notification,
    ScrollController scrollController,
  ) {
    return
        //停止?jié)L動
        notification is UserScrollNotification
            //沒有滾動正在進(jìn)行
            &&
            notification.direction == ScrollDirection.idle &&
            scrollController.position.activity is! HoldScrollActivity;
  }

  ///選中值
  void select(int centerValue) {
    //根據(jù)(中間值-最小值)/步長=第幾個(gè)刻度,然后第幾個(gè)刻度乘以每個(gè)刻度的寬度就是移動的寬度
    double x =
        (centerValue - widget.minValue) / widget.step * widget.subScaleWidth;
    _scrollController.animateTo(x,
        duration: Duration(milliseconds: 200), curve: Curves.decelerate);
  }
}

///真實(shí)刻度尺View
class RealRulerView extends StatelessWidget {
  const RealRulerView({
    Key key,
    this.subGridCount,
    this.subScaleWidth,
    this.minValue,
    this.height,
    this.step,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  }) : super(key: key);

  //刻度總數(shù)
  final int subGridCount;

  //每個(gè)刻度的寬度
  final int subScaleWidth;

  //刻度尺的高度
  final int height;

  //刻度尺最小值
  final int minValue;

  //每個(gè)大刻度的小刻度數(shù)
  final int subScaleCountPerScale;

  //步長 一刻度的值
  final int step;

  //刻度尺顏色
  final Color scaleColor;

  //刻度尺寬度
  final double scaleTextWidth;

  //刻度線寬度
  final double scaleWidth;

  //數(shù)字顏色
  final Color scaleTextColor;

  @override
  Widget build(BuildContext context) {
    double rulerWidth = (subScaleWidth * subGridCount).toDouble();
    double rulerHeight = this.height.toDouble();
    return CustomPaint(
      size: Size(rulerWidth, rulerHeight),
      painter: RulerViewPainter(
        this.subScaleWidth,
        this.step,
        this.minValue,
        this.scaleColor,
        this.scaleWidth,
        this.scaleTextColor,
        this.scaleTextWidth,
        this.subScaleCountPerScale,
      ),
    );
  }
}

class RulerViewPainter extends CustomPainter {
  final int subScaleWidth;

  final int step;

  final int minValue;

  final Color scaleColor;

  final Color scaleTextColor;

  final double scaleTextWidth;

  final int subScaleCountPerScale;

  final double scaleWidth;

  Paint linePaint;

  TextPainter textPainter;

  RulerViewPainter(
    this.subScaleWidth,
    this.step,
    this.minValue,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  ) {
    //刻度尺
    linePaint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeWidth = scaleWidth
      ..color = scaleColor;

    //數(shù)字
    textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
  }

  @override
  void paint(Canvas canvas, Size size) {
    //繪制線
    drawLine(canvas, size);
    //繪制數(shù)字
    drawNum(canvas, size);
  }

  ///繪制線
  void drawLine(Canvas canvas, Size size) {
    //繪制橫線
    canvas.drawLine(
      Offset(0, 0 + scaleWidth / 2),
      Offset(size.width, 0 + scaleWidth / 2),
      linePaint,
    );
    //第幾個(gè)小格子
    int index = 0;
    //繪制豎線
    for (double x = 0; x <= size.width; x += subScaleWidth) {
      if (index % subScaleCountPerScale == 0) {
        canvas.drawLine(
            Offset(x, 0), Offset(x, size.height * 3 / 8), linePaint);
      } else {
        canvas.drawLine(Offset(x, 0), Offset(x, size.height / 4), linePaint);
      }
      index++;
    }
  }

  ///繪制數(shù)字
  void drawNum(Canvas canvas, Size size) {
    canvas.save();
    //坐標(biāo)移動(0弊予,0)點(diǎn)
    canvas.translate(0, 0);
    //每個(gè)大格子的寬度
    double offsetX = (subScaleWidth * subScaleCountPerScale).toDouble();
    int index = 0;
    //繪制數(shù)字
    for (double x = 0; x <= size.width; x += offsetX) {
      textPainter.text = TextSpan(
        text: "${minValue + index * step * subScaleCountPerScale}",
        style: TextStyle(color: scaleTextColor, fontSize: scaleTextWidth),
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        new Offset(
          -textPainter.width / 2,
          size.height - textPainter.height,
        ),
      );
      index++;
      canvas.translate(offsetX, 0);
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

github:https://github.com/yixiaolunhui/my_flutter
好了祥楣,話不多說,一笑輪回~~~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市误褪,隨后出現(xiàn)的幾起案子责鳍,更是在濱河造成了極大的恐慌,老刑警劉巖兽间,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件历葛,死亡現(xiàn)場離奇詭異,居然都是意外死亡嘀略,警方通過查閱死者的電腦和手機(jī)恤溶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帜羊,“玉大人咒程,你說我怎么就攤上這事∷嫌” “怎么了帐姻?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奶段。 經(jīng)常有香客問我饥瓷,道長,這世上最難降的妖魔是什么痹籍? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任呢铆,我火速辦了婚禮,結(jié)果婚禮上蹲缠,老公的妹妹穿的比我還像新娘棺克。我一直安慰自己,他們只是感情好线定,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布逆航。 她就那樣靜靜地躺著,像睡著了一般渔肩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拇惋,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天周偎,我揣著相機(jī)與錄音,去河邊找鬼撑帖。 笑死蓉坎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胡嘿。 我是一名探鬼主播蛉艾,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勿侯?” 一聲冷哼從身側(cè)響起拓瞪,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎助琐,沒想到半個(gè)月后祭埂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兵钮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年蛆橡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掘譬。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泰演,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出葱轩,到底是詐尸還是另有隱情睦焕,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布酿箭,位于F島的核電站复亏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏缭嫡。R本人自食惡果不足惜缔御,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妇蛀。 院中可真熱鬧耕突,春花似錦、人聲如沸评架。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纵诞。三九已至上祈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浙芙,已是汗流浹背登刺。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗡呼,地道東北人纸俭。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像南窗,于是被迫代替她去往敵國和親揍很。 傳聞我的和親對象是個(gè)殘疾皇子郎楼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

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