Flutter-自定義三角形評(píng)分控件

效果圖

屏幕錄制2024-05-18 20.21.33.gif

序言

在移動(dòng)應(yīng)用開(kāi)發(fā)中牢裳,顯示數(shù)據(jù)的方式多種多樣逢防,直觀的圖形展示常常能帶給用戶(hù)更好的體驗(yàn)。本文將介紹如何使用Flutter創(chuàng)建一個(gè)自定義三角形緯度評(píng)分控件蒲讯,該控件可以通過(guò)動(dòng)畫(huà)展示評(píng)分的變化忘朝,讓?xiě)?yīng)用界面更加生動(dòng)。

實(shí)現(xiàn)思路及步驟

思路

  1. 定義控件屬性:首先需要定義控件的基本屬性判帮,如寬度局嘁、高度溉箕、最大評(píng)分以及每個(gè)頂點(diǎn)的評(píng)分值。
  2. 實(shí)現(xiàn)動(dòng)畫(huà)效果:使用AnimationControllerCurvedAnimation來(lái)控制評(píng)分動(dòng)畫(huà)悦昵,使每個(gè)頂點(diǎn)的評(píng)分從0逐漸增加到對(duì)應(yīng)的評(píng)分值肴茄。
  3. 自定義繪制:使用CustomPainter繪制三角形和評(píng)分三角形,并在頂點(diǎn)處繪制空心圓點(diǎn)但指。

步驟

  1. 創(chuàng)建一個(gè)TriangleRatingAnimView小部件寡痰。
  2. 定義動(dòng)畫(huà)控制器和動(dòng)畫(huà)曲線。
  3. CustomPainter中繪制三角形及評(píng)分三角形棋凳。
  4. 使用AnimatedBuilder實(shí)現(xiàn)動(dòng)畫(huà)效果拦坠。

4. 代碼實(shí)現(xiàn)

以下是完整的代碼實(shí)現(xiàn):

import 'package:flutter/material.dart';

/// 三角形等級(jí)評(píng)分的控件
class TriangleRatingAnimView extends StatefulWidget {
  final double width; // 控件寬度
  final double height; // 控件高度
  final int maxRating; // 最大評(píng)分
  final int upRating; // 上頂點(diǎn)評(píng)分
  final int leftRating; // 左頂點(diǎn)評(píng)分
  final int rightRating; // 右頂點(diǎn)評(píng)分
  final Color strokeColor; // 三角形邊框顏色
  final double strokeWidth; // 三角形邊框?qū)挾?  final Color ratingStrokeColor; // 評(píng)分三角形邊框顏色
  final double ratingStrokeWidth; // 評(píng)分三角形邊框?qū)挾?
  const TriangleRatingAnimView({
    Key? key,
    required this.width,
    required this.height,
    this.maxRating = 5,
    this.upRating = 0,
    this.leftRating = 0,
    this.rightRating = 0,
    this.strokeColor = Colors.grey,
    this.strokeWidth = 1,
    this.ratingStrokeColor = Colors.red,
    this.ratingStrokeWidth = 2,
  }) : super(key: key);

  @override
  TriangleRatingAnimViewState createState() => TriangleRatingAnimViewState();
}

class TriangleRatingAnimViewState extends State<TriangleRatingAnimView>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
    _startAnimations();
  }

  @override
  void didUpdateWidget(TriangleRatingAnimView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.upRating != widget.upRating ||
        oldWidget.leftRating != widget.leftRating ||
        oldWidget.rightRating != widget.rightRating) {
      _startAnimations();
    }
  }

  void _startAnimations() {
    _controller.reset();
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return CustomPaint(
          size: Size(widget.width, widget.height),
          painter: TrianglePainter(
            upRating: (widget.upRating * _animation.value).toInt(),
            rightRating: (widget.rightRating * _animation.value).toInt(),
            leftRating: (widget.leftRating * _animation.value).toInt(),
            strokeWidth: widget.strokeWidth,
            ratingStrokeWidth: widget.ratingStrokeWidth,
            strokeColor: widget.strokeColor,
            ratingStrokeColor: widget.ratingStrokeColor,
            maxRating: widget.maxRating,
          ),
        );
      },
    );
  }
}

class TrianglePainter extends CustomPainter {
  final int maxRating;
  final int upRating;
  final int leftRating;
  final int rightRating;
  final Color strokeColor;
  final double strokeWidth;
  final Color ratingStrokeColor;
  final double ratingStrokeWidth;

  TrianglePainter({
    required this.maxRating,
    required this.upRating,
    required this.leftRating,
    required this.rightRating,
    required this.strokeWidth,
    required this.ratingStrokeWidth,
    required this.strokeColor,
    required this.ratingStrokeColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = strokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;

    final outerPaint = Paint()
      ..color = ratingStrokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = ratingStrokeWidth;

    final fillPaint = Paint()
      ..color = ratingStrokeColor.withOpacity(0.3)
      ..style = PaintingStyle.fill;

    final circlePaint = Paint()
      ..color = ratingStrokeColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    final circleFillPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    // 計(jì)算三角形頂點(diǎn)坐標(biāo)
    final p1 = Offset(size.width / 2, 0); // 頂部頂點(diǎn)
    final p2 = Offset(0, size.height); // 左下頂點(diǎn)
    final p3 = Offset(size.width, size.height); // 右下頂點(diǎn)

    // 繪制外部三角形
    final path = Path()
      ..moveTo(p1.dx, p1.dy)
      ..lineTo(p2.dx, p2.dy)
      ..lineTo(p3.dx, p3.dy)
      ..close();
    canvas.drawPath(path, paint);

    // 計(jì)算重心
    final centroid = Offset(
      (p1.dx + p2.dx + p3.dx) / 3,
      (p1.dy + p2.dy + p3.dy) / 3,
    );

    // 繪制頂點(diǎn)到重心的連線
    canvas.drawLine(p1, centroid, paint);
    canvas.drawLine(p2, centroid, paint);
    canvas.drawLine(p3, centroid, paint);

    // 根據(jù)評(píng)分計(jì)算動(dòng)態(tài)頂點(diǎn)
    final dynamicP1 = Offset(
      centroid.dx + (p1.dx - centroid.dx) * (upRating / maxRating),
      centroid.dy + (p1.dy - centroid.dy) * (upRating / maxRating),
    );
    final dynamicP2 = Offset(
      centroid.dx + (p2.dx - centroid.dx) * (leftRating / maxRating),
      centroid.dy + (p2.dy - centroid.dy) * (leftRating / maxRating),
    );
    final dynamicP3 = Offset(
      centroid.dx + (p3.dx - centroid.dx) * (rightRating / maxRating),
      centroid.dy + (p3.dy - centroid.dy) * (rightRating / maxRating),
    );

    // 繪制內(nèi)部動(dòng)態(tài)三角形
    final ratingPath = Path()
      ..moveTo(dynamicP1.dx, dynamicP1.dy)
      ..lineTo(dynamicP2.dx, dynamicP2.dy)
      ..lineTo(dynamicP3.dx, dynamicP3.dy)
      ..close();
    canvas.drawPath(ratingPath, outerPaint);
    canvas.drawPath(ratingPath, fillPaint);

    // 繪制動(dòng)態(tài)點(diǎn)上的空心圓
    const circleRadius = 5.0;
    canvas.drawCircle(dynamicP1, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP1, circleRadius - 1.5, circleFillPaint); // 填充白色
    canvas.drawCircle(dynamicP2, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP2, circleRadius - 1.5, circleFillPaint); // 填充白色
    canvas.drawCircle(dynamicP3, circleRadius, circlePaint);
    canvas.drawCircle(dynamicP3, circleRadius - 1.5, circleFillPaint); // 填充白色
  }

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

使用

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_xy/xydemo/rating/rating_anim_widget.dart';

import '../../widgets/xy_app_bar.dart';

class RatingPage extends StatefulWidget {
  const RatingPage({super.key});

  @override
  State<RatingPage> createState() => _RatingPageState();
}

class _RatingPageState extends State<RatingPage> {
  var upRating = 2;
  var leftRating = 3;
  var rightRating = 5;
  var maxRating = 5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.white,
        appBar: XYAppBar(
          title: "三角形評(píng)分控件",
          onBack: () {
            Navigator.pop(context);
          },
        ),
        body: Container(
          alignment: Alignment.center,
          child: Column(
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                "時(shí)間管理",
                style: TextStyle(
                  fontSize: 12.sp,
                ),
              ),
              SizedBox(height: 5.w),
              TriangleRatingAnimView(
                height: 200.w,
                width: 280.w,
                upRating: upRating,
                leftRating: leftRating,
                rightRating: rightRating,
                maxRating: maxRating,
                strokeWidth: 1.5.w,
                ratingStrokeWidth: 3.w,
              ),
              SizedBox(height: 5.w),
              Row(
                children: [
                  SizedBox(width: 10.w),
                  Text(
                    "成本控制",
                    style: TextStyle(
                      fontSize: 12.sp,
                    ),
                  ),
                  const Expanded(child: SizedBox.shrink()),
                  Text(
                    "質(zhì)量保證",
                    style: TextStyle(
                      fontSize: 12.sp,
                    ),
                  ),
                  SizedBox(width: 10.w),
                ],
              ),
              SizedBox(height: 50.w),
              ElevatedButton(
                onPressed: () {
                  updateRatingData();
                },
                child: const Text("更改數(shù)據(jù)"),
              )
            ],
          ),
        ));
  }

  /// 更新星數(shù)指標(biāo)數(shù)據(jù)
  void updateRatingData() {
    final random = Random();
    maxRating = 5 + random.nextInt(6);
    upRating = 1 + random.nextInt(maxRating);
    leftRating = 1 + random.nextInt(maxRating);
    rightRating = 1 + random.nextInt(maxRating);
    setState(() {});
  }
}

通過(guò)以上步驟和代碼,我們可以創(chuàng)建一個(gè)帶動(dòng)畫(huà)效果的三角形緯度評(píng)分控件剩岳,使評(píng)分展示更加生動(dòng)和直觀贞滨。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拍棕,隨后出現(xiàn)的幾起案子晓铆,更是在濱河造成了極大的恐慌,老刑警劉巖绰播,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尤蒿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幅垮,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)尾组,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)忙芒,“玉大人,你說(shuō)我怎么就攤上這事讳侨『侨” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵跨跨,是天一觀的道長(zhǎng)潮峦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)勇婴,這世上最難降的妖魔是什么忱嘹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮耕渴,結(jié)果婚禮上拘悦,老公的妹妹穿的比我還像新娘。我一直安慰自己橱脸,他們只是感情好础米,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布分苇。 她就那樣靜靜地躺著,像睡著了一般屁桑。 火紅的嫁衣襯著肌膚如雪医寿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天蘑斧,我揣著相機(jī)與錄音靖秩,去河邊找鬼。 笑死乌叶,一個(gè)胖子當(dāng)著我的面吹牛盆偿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播准浴,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼事扭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了乐横?” 一聲冷哼從身側(cè)響起求橄,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎葡公,沒(méi)想到半個(gè)月后罐农,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡催什,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年涵亏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒲凶。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡气筋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旋圆,到底是詐尸還是另有隱情宠默,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布灵巧,位于F島的核電站搀矫,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刻肄。R本人自食惡果不足惜瓤球,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肄方。 院中可真熱鬧冰垄,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蝴罪,卻和暖如春董济,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背要门。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工虏肾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人欢搜。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓封豪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親炒瘟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吹埠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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