精华内容
参与话题
问答
  • 添加圆点/区域LineRendererConfig折线图-虚线dashPatternFn折线图-自定义颜色charts.ColorUtil.fromDartColor(Color(0xFFE41E31))折线图-多条线折线图-针对单个线特殊处理customSeriesRenderers柱状图柱状图-左右...


    使用第三方charts_flutter:https://pub.dev/packages/charts_flutter

    • Google出品,没有文档(可以在GitHub代码里的issues查找问题)
    • 支持动画
    • 支持左右滑动
    • 支持自定义颜色

    折线图

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'package:charts_flutter/flutter.dart' as charts;
    
    class ChartTestPage extends StatelessWidget {
      const ChartTestPage({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("chart_flutter")),
          body: Column(children: [Container(height: 240, child: _simpleLine())]),
        );
      }
    
      Widget _simpleLine() {
        var random = Random();
    
        var data = [
          LinearSales(0, random.nextInt(100)),
          LinearSales(1, random.nextInt(100)),
          LinearSales(2, random.nextInt(100)),
          LinearSales(3, random.nextInt(100)),
        ];
    
        var seriesList = [
          charts.Series<LinearSales, int>(
            id: 'Sales',
            colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            data: data,
          )
        ];
    
        return charts.LineChart(seriesList, animate: true);
      }
    }
    
    class LinearSales {
      final int year;
      final int sales;
    
      LinearSales(this.year, this.sales);
    }
    

    在这里插入图片描述

    折线图-添加圆点/区域LineRendererConfig

        return charts.LineChart(seriesList,
            animate: true,
            defaultRenderer:
                charts.LineRendererConfig(
                // 圆点大小
                radiusPx: 5.0,
                stacked: false,
                // 线的宽度
                strokeWidthPx: 2.0,
                // 是否显示线
                includeLine: true,
                // 是否显示圆点
                includePoints: true,
                // 是否显示包含区域
                includeArea: true,
                // 区域颜色透明度 0.0-1.0
                areaOpacity: 0.2 ,
                ));
    

    在这里插入图片描述

    折线图-虚线dashPatternFn

        var seriesList = [
          charts.Series<LinearSales, int>(
            id: 'Sales',
            colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data,
          )
        ];
    

    在这里插入图片描述

    折线图-自定义颜色charts.ColorUtil.fromDartColor(Color(0xFFE41E31))

        var seriesList = [
          charts.Series<LinearSales, int>(
            id: 'Sales',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFFE41E31)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data,
          )
        ];
    

    折线图-多条线

        var seriesList = [
          charts.Series<LinearSales, int>(
            id: 'Sales',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFFE41E31)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data1,
          ),
          charts.Series<LinearSales, int>(
            id: 'User',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFF13A331)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            // dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data2,
          ),
          charts.Series<LinearSales, int>(
            id: 'Dart',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFF6300A1)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            // dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data3,
          )
        ];
    

    在这里插入图片描述

    折线图-针对单个线特殊处理customSeriesRenderers

        var seriesList = [
          charts.Series<LinearSales, int>(
            id: 'Sales',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFFE41E31)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data1,
          ),
          charts.Series<LinearSales, int>(
            id: 'User',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFF13A331)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            // dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data2,
          ),
          charts.Series<LinearSales, int>(
            id: 'Dart',
            colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(0xFF6300A1)),
            domainFn: (LinearSales sales, _) => sales.year,
            measureFn: (LinearSales sales, _) => sales.sales,
            // dashPatternFn: (_, __) => [8, 2, 4, 2],
            data: data3,
          )..setAttribute(charts.rendererIdKey, 'customArea'),
        ];
    
        return charts.LineChart(seriesList, animate: true, customSeriesRenderers: [
          charts.LineRendererConfig(
            // RendererId
            customRendererId: 'customArea',
    
            // 圆点大小
            radiusPx: 5.0,
            stacked: false,
            // 线的宽度
            strokeWidthPx: 2.0,
            // 是否显示线
            includeLine: true,
            // 是否显示圆点
            includePoints: true,
            // 是否显示包含区域
            includeArea: true,
            // 区域颜色透明度 0.0-1.0
            areaOpacity: 0.2,
          ),
        ]);
    

    在这里插入图片描述

    柱状图

    import 'package:flutter/material.dart';
    import 'package:charts_flutter/flutter.dart' as charts;
    
    class ChartTestPage extends StatelessWidget {
      const ChartTestPage({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("chart_flutter")),
          body: Column(children: [Container(height: 240, child: _simpleBar())]),
        );
      }
    
      Widget _simpleBar() {
        var random = Random();
    
        var data = [
          OrdinalSales('2014', random.nextInt(100)),
          OrdinalSales('2015', random.nextInt(100)),
          OrdinalSales('2016', random.nextInt(100)),
          OrdinalSales('2017', random.nextInt(100)),
        ];
      
        var seriesList = [
          charts.Series<OrdinalSales, String>(
            id: 'Sales',
            colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
            domainFn: (OrdinalSales sales, _) => sales.year,
            measureFn: (OrdinalSales sales, _) => sales.sales,
            data: data,
          )
        ];
    
        return charts.BarChart(
          seriesList,
          animate: true,
        );
      }
    }
    
    class OrdinalSales {
      final String year;
      final int sales;
    
      OrdinalSales(this.year, this.sales);
    }  
    

    在这里插入图片描述

    柱状图-左右滑动

        return charts.BarChart(
          seriesList,
          animate: true,
          behaviors: [
            charts.SlidingViewport(),
            charts.PanAndZoomBehavior(),
          ],
          domainAxis: new charts.OrdinalAxisSpec(
              viewport: new charts.OrdinalViewport('2010', 6)),
        );
    

    在这里插入图片描述

    柱状图-多组横向展示

        var seriesList = [
          charts.Series<OrdinalSales, String>(
            id: 'Sales',
            colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
            domainFn: (OrdinalSales sales, _) => sales.year,
            measureFn: (OrdinalSales sales, _) => sales.sales,
            data: data1,
          ),
          charts.Series<OrdinalSales, String>(
            id: 'Sales',
            colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault,
            domainFn: (OrdinalSales sales, _) => sales.year,
            measureFn: (OrdinalSales sales, _) => sales.sales,
            data: data2,
          )
        ];
    

    在这里插入图片描述

    柱状图-多组上下展示barGroupingType

        return charts.BarChart(
          seriesList,
          animate: true,
          barGroupingType: charts.BarGroupingType.stacked,
          behaviors: [
            charts.SlidingViewport(),
            charts.PanAndZoomBehavior(),
          ],
          domainAxis: new charts.OrdinalAxisSpec(
              viewport: new charts.OrdinalViewport('2010', 6)),
        );
    

    在这里插入图片描述

    柱状图-横向展示vertical

        return charts.BarChart(
          seriesList,
          animate: true,
          barGroupingType: charts.BarGroupingType.stacked,
          vertical: false,
        );
    

    在这里插入图片描述

    柱状图-柱头文本显示

        return charts.BarChart(
          seriesList,
          animate: true,
          barGroupingType: charts.BarGroupingType.stacked,
          barRendererDecorator: charts.BarLabelDecorator<String>(),
        );
    

    在这里插入图片描述

    饼状图

    import 'package:flutter/material.dart';
    import 'package:charts_flutter/flutter.dart' as charts;
    
    class ChartTestPage extends StatelessWidget {
      const ChartTestPage({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("chart_flutter")),
          body: Column(children: [
            Container(height: 300, child: _simplePie()),
          ]),
        );
      }
    
      Widget _simplePie() {
        var random = Random();
    
        var data = [
          PieSales(0, random.nextInt(100)),
          PieSales(1, random.nextInt(100)),
          PieSales(2, random.nextInt(100)),
          PieSales(3, random.nextInt(100)),
        ];
    
        var seriesList = [
          charts.Series<PieSales, int>(
            id: 'Sales',
            domainFn: (PieSales sales, _) => sales.year,
            measureFn: (PieSales sales, _) => sales.sales,
            data: data,
          )
        ];
    
        return charts.PieChart(seriesList, animate: true);
      }
    }
    
    class PieSales {
      final int year;
      final int sales;
    
      PieSales(this.year, this.sales);
    }
    

    在这里插入图片描述

    饼状图-自定义颜色

    var data = [
          PieSales(0, random.nextInt(100), charts.ColorUtil.fromDartColor(Color(0xFF126610))),
          PieSales(1, random.nextInt(100), charts.ColorUtil.fromDartColor(Color(0xFF522210))),
          PieSales(2, random.nextInt(100), charts.ColorUtil.fromDartColor(Color(0xFF929910))),
          PieSales(3, random.nextInt(100), charts.ColorUtil.fromDartColor(Color(0xFFD26699))),
        ];
    
    class PieSales {
      final int year;
      final int sales;
      final charts.Color color;
    
      PieSales(this.year, this.sales, this.color);
    }
    

    在这里插入图片描述

    饼状图-文本显示

        var seriesList = [
          charts.Series<PieSales, int>(
            id: 'Sales',
            domainFn: (PieSales sales, _) => sales.year,
            measureFn: (PieSales sales, _) => sales.sales,
            colorFn: (PieSales sales, _) => sales.color,
            data: data,
            labelAccessorFn: (PieSales row, _) => '${row.year}: ${row.sales}',
          )
        ];
    
        return charts.PieChart(seriesList,
            animate: true,
            defaultRenderer: new charts.ArcRendererConfig(arcRendererDecorators: [
              new charts.ArcLabelDecorator(
                  labelPosition: charts.ArcLabelPosition.outside)
            ]));
    

    在这里插入图片描述

    饼状图-空心显示

        return charts.PieChart(seriesList,
            animate: true,
            defaultRenderer: new charts.ArcRendererConfig(
              arcWidth: 60,
              arcRendererDecorators: [
              new charts.ArcLabelDecorator(
                  labelPosition: charts.ArcLabelPosition.outside)
            ]));
    

    在这里插入图片描述

    展开全文
  • 学习flutter,想搞一下柱状图,发现Google粑粑有一个图形库charts_flutter,这个图形库也很强大,网上也有大佬写了,但我没找到可以左右滑动的,于是自己查了一下官网,做了一下,但是还是发现了一些问题,希望有...

            学习flutter,想搞一下柱状图,发现Google粑粑有一个图形库charts_flutter,这个图形库也很强大,网上也有大佬写了,但我没找到可以左右滑动的,于是自己查了一下官网,做了一下,但是还是发现了一些问题,希望有大佬可以解决一下;      

    1、首先先把图形库引入项目中:charts_flutter: ^0.9.0

    2、在界面中引用导包:import 'package:charts_flutter/flutter.dart' as charts;

    3、所有代码:

    import 'package:charts_flutter/flutter.dart' as charts;
    import 'package:flutter/material.dart';
    
    class TestScreen extends StatefulWidget {
      @override
      _TestScreenState createState() => _TestScreenState();
    }
    
    class _TestScreenState extends State<TestScreen> {
    
      final List<OrdinalSales> data =[];
    
      _getSeriesData() {
        List<charts.Series<OrdinalSales, String>> series = [
          charts.Series(
              id: '销售额',
              data: data,
              domainFn: (OrdinalSales series, _) => series.year.toString(),
              measureFn: (OrdinalSales series, _) => series.sales,
              labelAccessorFn: (OrdinalSales sales, _) => '\$${sales.sales.toString()}'
          )
        ];
        return series;
      }
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        data..add(OrdinalSales('2012',600))..add(OrdinalSales('2013',800))..add(OrdinalSales('2014',400))
          ..add(OrdinalSales('2015',500))..add(OrdinalSales('2016',850))..add(OrdinalSales('2017',450))
          ..add(OrdinalSales('2018',630))
          ..add(OrdinalSales('2019',720))..add(OrdinalSales('2020',990));
      }
    
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.white,
          child:Column(
            children: <Widget>[
              Expanded(
                  child: charts.BarChart(
                    _getSeriesData(),
                    animate: true,
                    barGroupingType: charts.BarGroupingType.grouped,
                    behaviors: [
                      // Add the sliding viewport behavior to have the viewport center on the
                      // domain that is currently selected.
                      new charts.SlidingViewport(),
                      // A pan and zoom behavior helps demonstrate the sliding viewport
                      // behavior by allowing the data visible in the viewport to be adjusted
                      // dynamically.
                      new charts.PanAndZoomBehavior(),
                      new charts.SeriesLegend(entryTextStyle: charts.TextStyleSpec(
                        fontSize: 12,
                        color: charts.Color.black,
                      )),
                    ],
                    barRendererDecorator: new charts.BarLabelDecorator<String>(),
                    // Set an initial viewport to demonstrate the sliding viewport behavior on
                    // initial chart load.
                    domainAxis: new charts.OrdinalAxisSpec(
                        viewport: new charts.OrdinalViewport('2016', 6)),
                  ),
              ),
    
              Expanded(
                child: charts.BarChart(
                  _getSeriesData(),
                  animate: true,
                  barGroupingType: charts.BarGroupingType.grouped,
                  behaviors: [
                    // Add the sliding viewport behavior to have the viewport center on the
                    // domain that is currently selected.
                    new charts.SlidingViewport(),
                    // A pan and zoom behavior helps demonstrate the sliding viewport
                    // behavior by allowing the data visible in the viewport to be adjusted
                    // dynamically.
                    new charts.PanAndZoomBehavior(),
                    new charts.SeriesLegend(entryTextStyle: charts.TextStyleSpec(
                      fontSize: 12,
                      color: charts.Color.black,
                    )),
                  ],
                  barRendererDecorator: new charts.BarLabelDecorator<String>(),
                  // Set an initial viewport to demonstrate the sliding viewport behavior on
                  // initial chart load.
                  domainAxis: new charts.OrdinalAxisSpec(
                      viewport: new charts.OrdinalViewport('2016', 6)),
                ),
              ),
    
            ],
          )
        );
      }
    }
    
    
    /// Sample ordinal data type.
    class OrdinalSales {
      final String year;
      final double sales;
      OrdinalSales(this.year, this.sales);
    }

    这里我个人的理解:

            a、他是平移缩放在behaviors数组中添加了new charts.PanAndZoomBehavior();

            b、X轴显示多少个柱子是domainAxis: new charts.OrdinalAxisSpec( viewport: new charts.OrdinalViewport('2016', 6)),来控                制,我这里是从2016开始,显示6个柱子;

           c、设置Legend字体大小和颜色是用new charts.SeriesLegend(entryTextStyle: charts.TextStyleSpec( fontSize: 12, color:                     charts.Color.black, )),

    大概先了解了这么多,后续再继续啃!

    对了,这里运行发现的问题是X轴的数据会超过线的边界;你们运行试试,看能不能让他隐藏起来;

    小白一个,勿喷

    展开全文
  • 老孟导读:Flutter中有这么一类组件,用于定位、装饰、控制子组件,比如 Container (定位、装饰)、Expanded (扩展)、SizedBox (固定尺寸)、AspectRatio (宽高比)、FractionallySizedBox (占父组件比例)。这些组件的...

    老孟导读:Flutter中有这么一类组件,用于定位、装饰、控制子组件,比如 Container (定位、装饰)、Expanded (扩展)、SizedBox (固定尺寸)、AspectRatio (宽高比)、FractionallySizedBox (占父组件比例)。这些组件的使用频率非常高,下面一一介绍,最后给出项目中实际案例熟悉其用法。 【Flutter实战】系列文章地址:http://laomengit.com/guide/introduction/mobile_system.html

    Container

    Container 是最常用的组件之一,它是单容器类组件,即仅能包含一个子组件,用于装饰和定位子组件,例如设置背景颜色、形状等。

    最简单的用法如下:

    Container(
        child: Text('老孟'),
     )

    子组件不会发生任何外观上的变化:

    设置背景颜色:

    Container(
        color: Colors.blue,
        child: Text('老孟'),
    )

    设置内边距( padding ) 和 外边距( margin )

    Container(
          color: Colors.blue,
          child: Container(
            margin: EdgeInsets.all(10),
            padding: EdgeInsets.all(20),
            color: Colors.red,
            child: Text('老孟'),
          ),
        )

    效果如下:

    decoration 属性设置子组件的背景颜色、形状等。设置背景为圆形,颜色为蓝色:

    Container(
      child: Text('老孟,专注分享Flutter技术及应用'),
      decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.blue),
    )

    默认情况下,圆形的直径等于 Container 窄边长度,相当于在矩形内绘制内切圆。

    上面的情况明显不是我们希望看到了,希望背景是圆角矩形:

    Container(
            child: Text('老孟,专注分享Flutter技术及应用'),
            padding: EdgeInsets.symmetric(horizontal: 10),
            decoration: BoxDecoration(
                shape: BoxShape.rectangle,
                borderRadius: BorderRadius.all(Radius.circular(20)),
                color: Colors.blue),
          )

    除了背景我们可以设置边框效果,代码如下:

    Container(
            child: Text('老孟,专注分享Flutter技术及应用'),
            padding: EdgeInsets.symmetric(horizontal: 10),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12),
              border: Border.all(
                color: Colors.blue,
                width: 2,
              ),
            ),
          )

    创建圆角图片和圆形图片:

    Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            image:  DecorationImage(
              image: NetworkImage(
                  'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
              fit: BoxFit.cover,
            ),
            border: Border.all(
              color: Colors.blue,
              width: 2,
            ),
            borderRadius: BorderRadius.circular(12),
          ),
        )

    修改其形状为圆形,代码如下:

    Container(
          height: 200,
          width: 200,
          decoration: BoxDecoration(
            image: DecorationImage(
              image: NetworkImage(
                  'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
              fit: BoxFit.cover,
            ),
            border: Border.all(
              color: Colors.blue,
              width: 2,
            ),
            shape: BoxShape.circle,
          ),
        )

    设置对齐方式为居中,背景色为蓝色,代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,一个有态度的程序员'),
            alignment: Alignment.center,
          )

    注意:设置对齐方式后,Container将会充满其父控件,相当于Android中 match_parent

    Alignment 已经封装了常用的位置,

    通过名字就知道其位置,这里要介绍一下其他的位置,比如在距离左上角1/4处:

    Container(
      alignment: Alignment(-.5,-.5),
      child: Text('老孟,专注分享Flutter技术及应用'),
    )

    所以这里有一个非常重要的坐标系,Alignment 坐标系如下:

    组件的中心为坐标原点。

    设置固定的宽高属性:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            height: 60,
            width: 250,
          )

    通过 constraints 属性设置最大/小宽、高来确定大小,如果不设置,默认最小宽高是0,最大宽高是无限大(double.infinity),约束width代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            constraints: BoxConstraints(
              maxHeight: 100,
              maxWidth: 300,
              minHeight: 100,
              minWidth: 100,
            ),
          )

    通过transform可以旋转、平移、缩放Container,旋转代码如下:

    Container(
            color: Colors.blue,
            child: Text('老孟,专注分享Flutter技术及应用'),
            alignment: Alignment.center,
            height: 60,
            width: 250,
            transform: Matrix4.rotationZ(0.5),
          )

    注意:Matrix4.rotationZ()参数的单位是弧度而不是角度

    SizedBox

    SizedBox 是具有固定宽高的组件,直接指定具体的宽高,用法如下:

    SizedBox(
            height: 60,
            width: 200,
            child: Container(
              color: Colors.blue,
              alignment: Alignment.center,
              child: Text('老孟,专注分享Flutter技术及应用'),
            ),
          )

    设置尺寸无限大,如下:

    SizedBox(
      height: double.infinity,
      width: double.infinity,
      ...
    )

    虽然设置了无限大,子控件是否会无限长呢?不,不会,子控件依然会受到父组件的约束,会扩展到父组件的尺寸,还有一个便捷的方式设置此方式:

    SizedBox.expand(
      child: Text('老孟,专注分享Flutter技术及应用'),
    )

    SizedBox 可以没有子组件,但仍然会占用空间,所以 SizedBox 非常适合控制2个组件之间的空隙,用法如下:

    Column(
              children: <Widget>[
                Container(height: 30,color: Colors.blue,),
                SizedBox(height: 30,),
                Container(height: 30,color: Colors.red,),
              ],
            )

    AspectRatio

    AspectRatio 是固定宽高比的组件,用法如下:

    Container(
            height: 300,
            width: 300,
            color: Colors.blue,
            alignment: Alignment.center,
            child: AspectRatio(
              aspectRatio: 2 / 1,
              child: Container(color: Colors.red,),
            ),
          )

    aspectRatio 是宽高比,可以直接写成分数的形式,也可以写成小数的形式,但建议写成分数的形式,可读性更高。效果如下:

    FractionallySizedBox

    FractionallySizedBox 是一个相对父组件尺寸的组件,比如占父组件的70%:

    Container(
      height: 200,
      width: 200,
      color: Colors.blue,
      child: FractionallySizedBox(
        widthFactor: .8,
        heightFactor: .3,
        child: Container(
          color: Colors.red,
        ),
      ),
    )

    通过 alignment 参数控制子组件显示的位置,默认为居中,用法如下:

    FractionallySizedBox(
      alignment: Alignment.center,
      ...
    )

    权重组件

    ExpandedFlexibleSpacer 都是具有权重属性的组件,可以控制 Row、Column、Flex 的子控件如何布局的组件。

    Flexible 组件可以控制 Row、Column、Flex 的子控件占满父组件,比如,Row 中有3个子组件,两边的宽是100,中间的占满剩余的空间,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )

    还是有3个子组件,第一个占1/6,第二个占2/6,第三个占3/6,代码如下:

    Column(
          children: <Widget>[
            Flexible(
              flex: 1,
              child: Container(
                color: Colors.blue,
                alignment: Alignment.center,
                child: Text('1 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
            Flexible(
              flex: 2,
              child: Container(
                color: Colors.red,
                alignment: Alignment.center,
                child: Text('2 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
            Flexible(
              flex: 3,
              child: Container(
                color: Colors.green,
                alignment: Alignment.center,
                child: Text('3 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
              ),
            ),
          ],
        )

    子组件占比 = 当前子控件 flex / 所有子组件 flex 之和。

    Flexible中 fit 参数表示填满剩余空间的方式,说明如下:

    • tight:必须(强制)填满剩余空间。
    • loose:尽可能大的填满剩余空间,但是可以不填满。

    这2个看上去不是很好理解啊,什么叫尽可能大的填满剩余空间?什么时候填满?看下面的例子:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                  child: Text('Container',style: TextStyle(color: Colors.white),),
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )

    这段代码是在最上面代码的基础上给中间的红色Container添加了Text子控件,此时红色Container就不在充满空间,再给Container添加对齐方式,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
                child: Container(
                  color: Colors.red,
                  height: 50,
                  alignment: Alignment.center,
                  child: Text('Container',style: TextStyle(color: Colors.white),),
                )
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )

    此时又填满剩余空间。

    大家是否还记得 Container 组件的大小是如何调整的吗?Container 默认是适配子控件大小的,但当设置对齐方式时 Container 将会填满父组件,因此是否填满剩余空间取决于子组件是否需要填满父组件。

    如果把 Flexible 中子组件由 Container 改为 OutlineButton,代码如下:

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Flexible(
              child: OutlineButton(
                child: Text('OutlineButton'),
              ),
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )

    OutlineButton 正常情况下是不充满父组件的,因此最终的效果应该是不填满剩余空间:

    下面再来介绍另一个权重组件 Expanded ,源代码如下:

    class Expanded extends Flexible {
      /// Creates a widget that expands a child of a [Row], [Column], or [Flex]
      /// so that the child fills the available space along the flex widget's
      /// main axis.
      const Expanded({
        Key key,
        int flex = 1,
        @required Widget child,
      }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
    }

    Expanded 继承字 Flexible,fit 参数固定为 FlexFit.tight,也就是说 Expanded 必须(强制)填满剩余空间。上面的 OutlineButton 想要充满剩余空间可以直接使用 Expanded :

    Row(
          children: <Widget>[
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
            Expanded(
              child: OutlineButton(
                child: Text('OutlineButton'),
              ),
            ),
            Container(
              color: Colors.blue,
              height: 50,
              width: 100,
            ),
          ],
        )

    Spacer 也是一个权重组件,源代码如下:

    @override
    Widget build(BuildContext context) {
      return Expanded(
        flex: flex,
        child: const SizedBox.shrink(),
      );
    }

    Spacer 的本质也是 Expanded 的实现的,和Expanded的区别是:Expanded 可以设置子控件,而 Spacer 的子控件尺寸是0,因此Spacer适用于撑开 Row、Column、Flex 的子控件的空隙,用法如下:

    Row(
      children: <Widget>[
        Container(width: 100,height: 50,color: Colors.green,),
        Spacer(flex: 2,),
        Container(width: 100,height: 50,color: Colors.blue,),
        Spacer(),
        Container(width: 100,height: 50,color: Colors.red,),
      ],
    )

    三个权重组建总结如下

    • Spacer 是通过 Expanded 实现的,Expanded继承自Flexible。
    • 填满剩余空间直接使用Expanded更方便。
    • Spacer 用于撑开 Row、Column、Flex 的子组件的空隙。

    仿 掘金-我 效果

    先看下效果:

    拿到效果图先不要慌 (取出手机拍照发个朋友圈😊),整个列表每一行的布局基本一样,所以先写出一行的效果:

    class _SettingItem extends StatelessWidget {
      const _SettingItem(
          {Key key, this.iconData, this.iconColor, this.title, this.suffix})
          : super(key: key);
    
      final IconData iconData;
      final Color iconColor;
      final String title;
      final Widget suffix;
    
      @override
      Widget build(BuildContext context) {
        return Container(
          height: 45,
          child: Row(
            children: <Widget>[
              SizedBox(
                width: 30,
              ),
              Icon(iconData,color: iconColor,),
              SizedBox(
                width: 30,
              ),
              Expanded(
                child: Text('$title'),
              ),
              suffix,
              SizedBox(
                width: 15,
              ),
            ],
          ),
        );
      }
    }

    消息中心和其他行最后的样式不一样,单独封装,带红色背景的组件:

    class _NotificationsText extends StatelessWidget {
      final String text;
    
      const _NotificationsText({Key key, this.text}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          padding: EdgeInsets.symmetric(horizontal: 10),
          decoration: BoxDecoration(
              shape: BoxShape.rectangle,
              borderRadius: BorderRadius.all(Radius.circular(50)),
              color: Colors.red),
          child: Text(
            '$text',
            style: TextStyle(color: Colors.white),
          ),
        );
      }
    }

    灰色后缀组件:

    class _Suffix extends StatelessWidget {
      final String text;
    
      const _Suffix({Key key, this.text}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Text(
          '$text',
          style: TextStyle(color: Colors.grey.withOpacity(.5)),
        );
      }
    }

    将这些封装好的组件组合起来:

    class SettingDemo extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            _SettingItem(
              iconData: Icons.notifications,
              iconColor: Colors.blue,
              title: '消息中心',
              suffix: _NotificationsText(
                text: '2',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.thumb_up,
              iconColor: Colors.green,
              title: '我赞过的',
              suffix: _Suffix(
                text: '121篇',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.grade,
              iconColor: Colors.yellow,
              title: '收藏集',
              suffix: _Suffix(
                text: '2个',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.shopping_basket,
              iconColor: Colors.yellow,
              title: '已购小册',
              suffix: _Suffix(
                text: '100个',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.account_balance_wallet,
              iconColor: Colors.blue,
              title: '我的钱包',
              suffix: _Suffix(
                text: '10万',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.location_on,
              iconColor: Colors.grey,
              title: '阅读过的文章',
              suffix: _Suffix(
                text: '1034篇',
              ),
            ),
            Divider(),
            _SettingItem(
              iconData: Icons.local_offer,
              iconColor: Colors.grey,
              title: '标签管理',
              suffix: _Suffix(
                text: '27个',
              ),
            ),
          ],
        );
      }
    }

    至此就结束了。

    柱状图

    先来看下效果:

    关于动画部分的内容会在后面的章节具体介绍。这个效果分为3大部分:

    1. 坐标轴,左边和底部黑色直线。
    2. 矩形柱状图。
    3. 动画控制部分。

    坐标轴的实现如下:

    class _Axis extends StatelessWidget {
      final Widget child;
    
      const _Axis({Key key, this.child}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            border: Border(
              left: BorderSide(color: Colors.black, width: 2),
              bottom: BorderSide(color: Colors.black, width: 2),
            ),
          ),
          child: child,
        );
      }
    }

    单个柱状图实现:

    class _Cylinder extends StatelessWidget {
      final double height;
      final double width;
      final Color color;
    
      const _Cylinder({Key key, this.height, this.width, this.color})
          : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return AnimatedContainer(
          duration: Duration(seconds: 1),
          height: height,
          width: width,
          color: color,
        );
      }
    }

    生成多个柱状图:

    final double _width = 20.0;
    List<double> _heightList = [60.0, 80.0, 100.0, 120.0, 140.0];
    
    Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: List.generate(_heightList.length, (index) {
          return _Cylinder(
            height: _heightList[index],
            width: _width,
            color: Colors.primaries[index % Colors.primaries.length],
          );
        }))

    将此合并,然后更改每一个柱状图的高度:

    class CylinderChart extends StatefulWidget {
      @override
      _CylinderChartState createState() => _CylinderChartState();
    }
    
    class _CylinderChartState extends State<CylinderChart> {
      final double _width = 20.0;
      List<double> _heightList = [60.0, 80.0, 100.0, 120.0, 140.0];
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Container(
            height: 200,
            width: 250,
            child: Stack(
              children: <Widget>[
                _Axis(),
                Positioned.fill(
                  left: 5,
                  right: 5,
                  child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: List.generate(_heightList.length, (index) {
                        return _Cylinder(
                          height: _heightList[index],
                          width: _width,
                          color: Colors.primaries[index % Colors.primaries.length],
                        );
                      })),
                ),
                Positioned(
                  top: 0,
                  left: 30,
                  child: OutlineButton(
                    child: Text('反转'),
                    onPressed: () {
                      setState(() {
                        _heightList = _heightList.reversed.toList();
                      });
                    },
                  ),
                )
              ],
            ),
          ),
        );
      }
    }

    搞定。

    交流

    老孟Flutter博客地址(330个控件用法):http://laomengit.com

    欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:

    展开全文
  • 之前写过一篇Android原生绘制曲线的博客,动画效果不要太丝滑,那么现在到了Flutter,该如何实现类似的效果呢?如果你熟悉android的Canvas,那么恭喜你, 你将很快上手Flutter的Canvas绘制各种图形,因为实现方式...

         之前写过一篇Android原生绘制曲线图的博客,动画效果不要太丝滑,那么现在到了Flutter,该如何实现类似的效果呢?如果你熟悉android的Canvas,那么恭喜你, 你将很快上手Flutter的Canvas绘制各种图形,因为实现方式基本上与android是一模一样

    先看下要实现的基本效果:

                 

        Flutter中如果想要自定义绘制,那么你需要用到 CustomPaint 和 CustomPainter ;  CustomPaint是Widget的子类,先来看下构造方法

    const CustomPaint({
        Key key,
        this.painter,
        this.foregroundPainter,
        this.size = Size.zero,
        this.isComplex = false,
        this.willChange = false,
        Widget child,
      }) :super(key: key, child: child);

          我们只需要关心三个参数,painterforegroundPainter child  , 这里需要说明一下,painter 是绘制的 backgroud 层,而child 是在backgroud之上绘制,foregroundPainter 是在 child 之上绘制,所以这里就有了个层级关系,这跟android里面的backgroud与foreground是一个意思,那这两个painter的应用场景是什么呢?假如你只是单纯的想绘制一个图形,只用painter就可以了,但是如果你想给绘制区域添加一个背景(颜色,图片,等等),这时候如果使用 painter是会有问题的,painter的绘制会被child 层覆盖掉,此时你只需要将painter替换成foregroundPainter,然会颜色或者图片传递给child即可。

          如果是Android绘制几何图形,应该是重写View的onLayout() 和 onDraw方法,但是Flutter实现绘制,必须继承CustomPainter并重写 paint(Canvas canvas, Size size)和 shouldRepaint (CustomPainter oldDelegate) 方法 ,第一个参数canvas就是我们绘制的画布了(跟Android一模一样),paint第二个参数Size就是上面CustomPaint构造方法传入的size, 决定绘制区域的宽高信息

           既然Size已经确定了,现在就定义下绘制区域的边界,一般我做类似的UI,都会定义一个最基本的padding, 一般取值为16 , 因为绘制的内容与坐标轴之间需要找到一个基准线,这样更容易绘制,而且调试边距也很灵活

    double startX, endX, startY, endY;//定义绘制区域的边界
    static const double basePadding = 16; //默认的边距
    double fixedHeight, fixedWidth; //去除padding后曲线的真实宽高
    bool isShowXyRuler; //是否显示xy刻度
    List<ChatBean> chatBeans;//数据源
    
    class ChatBean {
      String x;
      double y;
      int millisSeconds;
      Color color;
    
      ChatBean({@required this.x, @required this.y, this.millisSeconds, this.color});
    }

           然后在paint()方法中拿到Size,确定绘制区域的坐标

    ///计算边界
      void initBorder(Size size) {
        print('size - - > $size');
        this.size = size;
        startX = yNum > 0 ? basePadding * 2.5 : basePadding * 2; //预留出y轴刻度值所占的空间
        endX = size.width - basePadding * 2;
        startY = size.height - (isShowXyRuler ? basePadding * 3 : basePadding);
        endY = basePadding * 2;
        fixedHeight = startY - endY;
        fixedWidth = endX - startX;
        maxMin = calculateMaxMin(chatBeans);
      }

            maxMin是定义存储曲线中最大值和最小值的

    ///计算极值 最大值,最小值
      List<double> calculateMaxMin(List<ChatBean> chatBeans) {
        if (chatBeans == null || chatBeans.length == 0) return [0, 0];
        double max = 0.0, min = 0.0;
        for (ChatBean bean in chatBeans) {
          if (max < bean.y) {
            max = bean.y;
          }
          if (min > bean.y) {
            min = bean.y;
          }
        }
        return [max, min];
      }

           初始化画笔 .. 是dart中的独特语法,代表使用对象的返回值调用属性或方法

    var paint = Paint()
          ..isAntiAlias = true//抗锯齿
          ..strokeWidth = 2
          ..strokeCap = StrokeCap.round//折线连接处圆滑处理
          ..color = xyColor
          ..style = PaintingStyle.stroke;//描边

          绘制坐标轴,这里在确定好的边界基础上再次xy轴横向和纵向各自增加一倍的padding,不然显得太紧凑

    canvas.drawLine(Offset(startX, startY),Offset(endX + basePadding, startY), paint); //x轴
    canvas.drawLine(Offset(startX, startY),Offset(startX, endY - basePadding), paint); //y轴

           绘制 X 轴刻度,定义为最多绘制7组数据 ,rulerWidth就是刻度的长度定义为8

    int length = chatBeans.length > 7 ? 7 : chatBeans.length; //最多绘制7个
    double DW = fixedWidth / (length - 1); //两个点之间的x方向距离
    double DH = fixedHeight / (length - 1); //两个点之间的y方向距离
    for (int i = 0; i < length; i++) {
         ///绘制x轴文本
         TextPainter(
                textAlign: TextAlign.center,
                ellipsis: '.',
                text: TextSpan(
                    text: chatBeans[i].x,
                    style: TextStyle(color: fontColor, fontSize: fontSize)),
                textDirection: TextDirection.ltr)
              ..layout(minWidth: 40, maxWidth: 40)
              ..paint(canvas, Offset(startX + DW * i - 20, startY + basePadding));
    
          ///x轴刻度
          canvas.drawLine(Offset(startX + DW * i, startY),Offset(startX + DW * i, startY - rulerWidth), paint);
       }

        这里要说明一点,Flutter绘制文本,并不能像android那样调用canvas.drawText () ,  而是通过TextPainter来渲染的,

    构造TextPainter 你必须指定文字的方向 textDirection 和 宽度 layout ,最后调用paint方法,指定坐标进行绘制   

       绘制 Y 轴刻度,y轴的刻度数量并不需要跟随数据源的长度,只需要按照一定数量(yNum )平分y轴最大值即可

          int yLength = yNum + 1; //包含原点,所以 +1
          double dValue = maxMin[0] / yNum; //一段对应的值
          double dV = fixedHeight / yNum; //一段对应的高度
          for (int i = 0; i < yLength; i++) {
            ///绘制y轴文本,保留1位小数
            var yValue = (dValue * i).toStringAsFixed(isShowFloat ? 1 : 0);
            TextPainter(
                textAlign: TextAlign.center,
                ellipsis: '.',
                maxLines: 1,
                text: TextSpan(  
                    text: '$yValue',
                    style: TextStyle(color: fontColor, fontSize: fontSize)),
                textDirection: TextDirection.rtl)
              ..layout(minWidth: 40, maxWidth: 40)
              ..paint(canvas, Offset(startX - 40, startY - dV * i - fontSize / 2));
    
            ///y轴刻度
            canvas.drawLine(Offset(startX, startY - dV * (i)),Offset(startX + rulerWidth, startY - dV * (i)), paint);
          }

       现在坐标轴和刻度已经绘制完成了,基本上与原生一致,只是代码方式有些区别,接下来的曲线也是一模一样的,绘制贝塞尔曲线其实也不难,主要是找到起点和两个坐标之间的辅助点, 贝塞尔曲线的原理可以参考这里

    path.cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
    path = Path();
    double preX, preY, currentX, currentY;
    int length = chatBeans.length > 7 ? 7 : chatBeans.length;
    double W = fixedWidth / (length - 1); //两个点之间的x方向距离

      遍历数据源的第一个元素时,需要做个判断,index=0时,需要将path move到此处

    if (i == 0) {
         path.moveTo(startX, (startY - chatBeans[i].y / maxMin[0] * fixedHeight));
         continue;
       }

       添加后面的坐标时,需要找辅助点

    currentX = startX + W * i;
    preX = startX + W * (i - 1);
    
    preY = (startY - chatBeans[i - 1].y / maxMin[0] * fixedHeight);
    currentY = (startY - chatBeans[i].y / maxMin[0] * fixedHeight);
    
    path.cubicTo(
                  (preX + currentX) / 2, preY,
                  (preX + currentX) / 2, currentY,
                  currentX, currentY
                );

    如果是要画折线而非曲线,第一步还是path.moveTo  ,折线不需要找辅助点,所以后续可以直接添加坐标,path.lineTo  

    最后将path绘制出来

    canvas.drawPath(newPath, paint);

    虽然曲线已经成功绘制,但是这样显得很枯燥,如果可以看到绘制过程那就会更加有趣味性,这时候就需要通过动画来更新曲线的path的长度了,一般Android中我会用ValueAnimator.ofFloat(start ,end ) 来开启一个动画 ,在Flutter中,动画也是非常简单实用

    _controller = AnimationController(vsync: this, duration: widget.duration);
          Tween(begin: 0.0, end: widget.duration.inMilliseconds.toDouble())
              .animate(_controller)
                ..addStatusListener((status) {
                  if (status == AnimationStatus.completed) {
                    print('绘制完成');
                  }
                })
                ..addListener(() {
                  _value = _controller.value;//当前动画值
                  setState(() {});
                });
          _controller.forward();

     动画执行过程中,我们会及时获取到当前的动画进度 _value, 此时就需要一段完整的path跟随动画值 等比绘制了 ,之前在Android中我们可以用 PathMeasure 来测量path ,然后根据动画进度不断地截取,就实现了像贪吃蛇一样的效果, 但是在Flutter中,我并没有找到PathMeasure 这个类,相反的,PathMeasure 在Flutter竟然是个私有的类 _PathMeasure ,经过一通百度 和 google,也没有找到类似的案例。难道没有人给造轮子,就必须要停止我前进的步伐了嘛,不急,显然Path这个类里面有很多方法,就这样我走上了一条反复测试的不归路...

    幸运的是,在翻阅了google 官方Flutter api 后,终于找到了突破口

        哈哈,藏得还挺深呐,就是这个 PathMetrics 类,path.computeMetrics() 的返回值 ,是用来将path解析成矩阵的一个工具

    var pathMetrics = path.computeMetrics(forceClosed: false);

        有个参数 forceClosed , 表示是否要连接path的起始点 ,我们这里当然不要啦 ,computeMetrics方法返回的是PathMetrics对象,调用 toList () 可以获取到 多个path组成的 List<PathMetric> ; 集合中的每个元素代表一段path的矩阵 , 奇怪,为什么是多个path 呢 ???

        当时我也是懵着猜测的,历史总是惊人的相似,被我给猜对了,不晓得你们有没有发现,Path有个方法可以添加多个Path , 

    path.addPath(path, offset);

        当我每调用一次 addPath()或者 moveTo() ,lsit . length就增加1,所以上面提到的多个path的集合 就不难理解了 ,因为我们这里只有一个path, 所以我们的 list 中只有一个元素 , 元素中包含一段path, 现在我们获取到了描述path的矩阵PathMetric

      PathMetric . length 就是这段path的长度了,唉,为了找到你 ,我容易吗 !

    另外还有个关键的方法,可以将pathMetric按照给定的位置区间截取,最后返回这段path, 这就跟android中的PathMeasure.getSegment()是一样

    extractPath(double start, double end,{ bool startWithMoveTo:true }) → Path
    给定起始和停止距离,返回中间段。

    现在是时候将前面获取到的当前动画值 value 用起来了,找到当前path的length乘以value即是当前path的最新长度

    var pathMetrics = path.computeMetrics(forceClosed: true);
    var list = pathMetrics.toList();
    var length = value * list.length.toInt();
    Path newPath = new Path();
    for (int i = 0; i < length; i++) {
         var extractPath =list[i].extractPath(0, list[i].length * value, startWithMoveTo: true);
          newPath.addPath(extractPath, Offset(0, 0));
        }
     canvas.drawPath(newPath, paint);

    走到这里,好像跨过了山和大海,得了,困死了,睡了、睡了...

    现在曲线和折线都已经绘制完成了,不过刚开始的demo里还有个渐变色的部分没有完成,貌似有了渐变色以后,显得不那么单调了,其实,我们绘图所用到的Paint还有一个属性shader,可以绘制线条或区域的渐变色,LinearGradient可实现线性渐变的效果,默认为从左到右绘制,你可以通过beginend属性自定义绘制的方向,我们这里需要指定为从上至下,并且颜色类型为数组的形式,所以你可以传入多个颜色值来绘制

    var shader = LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  tileMode: TileMode.clamp,
                  colors: shaderColors)
              .createShader(Rect.fromLTRB(startX, endY, startX, startY));

     值得注意的是,通过 createShader的方式创建shader,你需要指定绘制区域的边界,我们这里要实现的是从上至下,所以就以y轴为基准,指定从上至下的绘制方向

    既然是绘制渐变色,所以画笔的样式必须设置为填充状态

    Paint shadowPaint = new Paint();
          shadowPaint
            ..shader = shader
            ..isAntiAlias = true
            ..style = PaintingStyle.fill;

    另外,渐变色的区域我们是通过path来指定上面的边界的,所以我们还需要指定path下面部分的起点和终点,这样形成一个闭环,才能确定出完整的区域

    ///从path的最后一个点连接起始点,形成一个闭环
          shadowPath
            ..lineTo(startX + fixedWidth * value, startY)
            ..lineTo(startX, startY)
            ..close();
          canvas..drawPath(shadowPath, shadowPaint);

    至此,即可实现带有渐变色的曲线或者折线,也许你有个疑问,画折线为什么也要用path呢,不是可以直接drawLine吗 ?机智如我,添加到path以后,可以更方便的绘制,添加动画也很方便

    另附上最终的实现效果,至于触摸操作就不打算阐述了,可以参考以下代码

    代码已发布到  Dart社区 https://pub.dev/flutter/packages?q=flutter_chart

    GitHub仓库链接 https://github.com/good-good-study/flutter_chart

    展开全文
  • Flutter中如果想要自定义绘制,那么你需要用到CustomPaint和CustomPainter ;CustomPaint是Widget的子类,先来看下构造方法 const CustomPaint({ Key key, this.painter, this....
  • 关于Flutter,之前写了两篇文章,第一篇Flutter如何和Native通信-Android视角简单说了一下如何使用Flutter和Native的通信通道:Platform Channels;第二篇Flutter插件(Plugin)开发 - Android视角讲了Flutter插件开发...
  • 绘图 根据数据源绘制展示在屏幕上的图标,实际上主要就是连线,柱状图,绘制一些文字,有时候会有些圆点或者不规则图形 比较少见。 图表操作,一般只有平移,缩放,点选三种。 指标操作,包含指标的参数调整,指标...
  • chart相关(多图表动态加载、渐变折线图、折线柱状图混合图表) JhTopTabBar(导航条分页切换) 小红点 二维码扫描与生成 轮播(全屏、缩放) Animation(标签云) 列表侧滑按钮 城市选择列表 富文本 屏幕适配 倒计时按钮 ...
  • flutter mpchart的使用

    2020-07-12 19:06:34
    主要使用了mpchart的柱状图,和折线图 在yaml中添加依赖 charts_flutter ^0.5.0 然后 package get 成功之后就可以使用 截图: github地址https://github.com/1296695625/flutterdemo.git 此项目是在...
  • 接到一个需求,需要展示一个环状图和一个柱状图,去pub上逛了一圈,选择了功能比较全的MPFlutterChart 库。引入项目时,发现MPFlutterChart 和国际化包flutter_localizations依赖不同的intl版本。为了能使用...

空空如也

1 2
收藏数 26
精华内容 10
关键字:

flutter柱状图