Flutter | 擼一個高大上的星級評分控制元件

sponmas發表於2019-09-04

因為工作中有一個需求是展示遊戲評分,而Flutter系統並沒有提供現成的評分控制元件,網上也能找到相關的自定義評分控制元件的文章,總是有點不符合我自己的需求,所以就有接下來的這篇文章了。國際慣例,上圖

 Flutter | 擼一個高大上的星級評分控制元件

關鍵點

關鍵是有三個點的效果要想好

  1. 未選中星展示

  2. 滿星展示

  3. 未滿星展示

星際評分原理就是使用未選中星星作為背景,上層使用滿星和未滿星來覆蓋展示,達到百分比的效果

  1. 使用Stack層疊佈局來控制兩種樣式的疊加

  2. 使用裁剪ClipRect來顯示未滿星的效果

具體實現

首先我們實現一個靜態的評分效果。

我們可以提前設計一下提供給其他人使用時,可以自定義的內容,比如說星星個數、星星的間距、星星的大小等等,這裡我把自己寫的元件引數先提取了,你們可以根據自己的需要來擴充套件

話不多說,上程式碼

class RatingBar extends StatefulWidget {
  final int count; 
  final double maxRating;
  final double value;
  final double size;
  final double padding;
  final String nomalImage;
  final String selectImage;
  final bool selectAble;
  final ValueChanged<String> onRatingUpdate;

  RatingBar({
    this.maxRating = 10.0,
    this.count = 5,
    this.value = 10.0,
    this.size = 20,
    this.nomalImage,
    this.selectImage,
    this.padding,
    this.selectAble = false,
    @required this.onRatingUpdate
  }) : assert(nomalImage != null),
        assert(selectImage != null);

  @override
  _RatingBarState createState() => _RatingBarState();
}複製程式碼

1、背景星

我們只做橫版效果,所以可以使用Row來做出星星的排列效果

List<Widget> buildNomalRow() {
    List<Widget> children = [];
    for(int i = 0; i < widget.count; i ++) {
      children.add(Image.asset(widget.nomalImage,height: widget.size,width: widget.size,));
      if(i < widget.count - 1) {
        children.add(SizedBox(width: widget.padding,));
      }
    }
    return children;
}複製程式碼

每個星星之間增加一個SizedBox作為間隔

2、滿星與未滿星

我們根據評分和星星最大數量來計算每顆星星所對應的分數比例,然後根據分數比例來計算滿星數量與未滿星的裁剪比例

int fullStars() {
    if(value != null) {
      return (value /(widget.maxRating/widget.count)).floor();
    }
    return 0;
}

double star() {
    if(value != null) {
        if(widget.count / fullStars() == widget.maxRating / value ) {
        return 0;
        }
        return (value % (widget.maxRating/widget.count))/(widget.maxRating/widget.count);
    }
    return 0;
}

List<Widget> buildRow() {
    int full = fullStars();
    List<Widget> children = [];
    for(int i = 0; i < full; i ++) {
      children.add(Image.asset(widget.selectImage,height: widget.size,width: widget.size,));
      if(i < widget.count - 1) {
        children.add(SizedBox(width: widget.padding,),);
      }
    }
    if(full < widget.count) {
      children.add(ClipRect(
        clipper: SMClipper(rating: star() * widget.size),
        child: Image.asset(widget.selectImage,height: widget.size,width: widget.size),
      ));
    }
    return children;
}複製程式碼

滿星個數計算:
    每顆星星對應分數值 = 最大分值/星星數量 
    當前評分 / 星星分數值 取整就是當前滿星的個數
未滿星裁剪比例:
    當前評分制除以星星分數值取餘,就是取出滿星後的分數值,這個值再除以 星星分數值就是當前未滿星星的裁剪比例 

裁剪

因為我們只需要豎向裁剪,所以用ClipRect就能滿足需求了

class SMClipper extends CustomClipper<Rect>{
  final double rating;
  SMClipper({
    this.rating
  }): assert(rating != null);
  @override
  Rect getClip(Size size) {
    return Rect.fromLTRB(0.0, 0.0, rating , size.height);
  }

  @override
  bool shouldReclip(SMClipper oldClipper) {
    return rating != oldClipper.rating;
  }
}複製程式碼

至此,一個靜態的評分展示控制元件就已經寫完了。看下效果

Flutter | 擼一個高大上的星級評分控制元件

動態

既然後展示評分,那肯定就需要一個能評分的了,我們就需要在原有的基礎上對控制元件做一個 觸控和點選事件監聽,對結束觸控的點進行計算,來得到當前點代表的評分值

監聽移動

監聽手勢動作的話,我們就可以用到Listener了,這裡需要用到它的兩個回撥方法onPointerMove和onPointerDown(或者onPointerUp),前者用來監聽滑動,後者用來監聽點選

Listener(
      child: buildRowRating(),
      onPointerDown: (PointerDownEvent event){
        double x = event.localPosition.dx;
        if (x < 0) x = 0;
        pointValue(x);
      },
      onPointerMove: (PointerMoveEvent event) {
        double x = event.localPosition.dx;
        if (x < 0) x = 0;
        pointValue(x);
      },
      onPointerUp: (_) {
      },
      behavior: HitTestBehavior.deferToChild,
    )複製程式碼

根據手勢的x座標來計算當前手指位置代表的評分值,得到後觸發重新構建,這裡有個問題就是要去掉星星之間的間隔,保證精確

pointValue(double dx) {
    if(!widget.selectAble) {
      return;
    }
    if(dx >= widget.size * widget.count  + widget.padding * (widget.count - 1)) {
      value = widget.maxRating;
    }else {
      for(double i = 1; i < widget.count + 1;i ++) {
        if(dx > widget.size * i + widget.padding *(i -1) && dx < widget.size * i + widget.padding * i) {
          value = i * (widget.maxRating/widget.count);
          break;
        }else if(dx > widget.size * (i -1) + widget.padding*(i -1) && dx < widget.size * i+ widget.padding*i )  {
          value = (dx - widget.padding *(i -1))/(widget.size * widget.count ) *widget.maxRating;
          break;
        }
      }
    }
    setState(() {
      widget.onRatingUpdate(value.toStringAsFixed(1));
    });
  }複製程式碼

這樣,一個可以動態評分的空間就完成了,怎麼用呢,看下面

RatingBar(
    value: 9,
    size: 30,
     padding: 5,
     nomalImage: 'img/star_nomal.png',
     selectImage: 'img/star.png',
     selectAble: true,
     onRatingUpdate: (value) {},
     maxRating: 10,
     count: 6,
     )複製程式碼

各引數說明

  • value:當前評分值
  • size:星星大小
  • padding:星星間距
  • nomalImage:空星圖片
  • selectImage:滿星圖片
  • selectAble:是否可以點選滑動修改評分值
  • onRatingUpdate:點選滑動修改評分值回撥,引數是String型別的評分值
  • maxRating:最大評分值
  • count:星星個數

附上github地址


相關文章