Flutter学习-阶段案例

Posted GY-93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习-阶段案例相关的知识,希望对你有一定的参考价值。

学习完列表渲染后,我打算做一个综合一点的练习小项目:豆瓣Top电影排行列表;效果图如下:

这个项目主要是为了锻炼Widget的布局,也设计到一些其他的知识,评分展示、分割线、底部工具栏

在进行豆瓣评分模仿时,有两个部件实现起来比较困难

  • 评分展示:我们需要根据不同的评分显示不用的星级展示,这里封装一个start_rating的widget来实现
  • 分割线:flutter中好像并没有边框虚线,因此我们需要封装一个DashedLine的Widget来实现

1. start_rating(评分)widget

1.1 最终效果展示

目的:实现功能的同时,实现高度的定制效果

  • rating:必传参数,当前的评分
  • maxRating:可选参数,最高评分,根据她来计算一个比例,默认值为10
  • count:可选参数,表示星星的个数,默认值为5
  • ratingSize:可选参数,表示星星的带下,默认值:30
  • normalColor:可选参数,表示星星的原始颜色,默认grey.
  • selectedColor: 可选参数,表示星星选中的颜色, 默认orange
  • unselectedWidget:可选参数, 未选中时的widget,默认参数通过初始化列表初始化一个Icon
  • selectedWidget:可选参数,选中时展示的widget,默认参数通过初始化列表初始化一个Icon

暂时实现以上的目标,后期如果有需求可以添加新的功能点

1.2 实现思路分析

  • 未选中start展示:根据个数和传入的unselectedWidget创建对应个数的widget即可
  • 选中start
    • 计算出满start的个数,创建对应的wiget
    • 计算剩余比例的评分,对最后一个widget进行裁剪

问题一:选择StatelessWidget还是StatefulWidget?

  • 考虑到后面可能会做用户点击进行评分或者用户手指滑动评分的效果,所以这里选择StatefulWidget
  • 但是目前我还没有学习到事件的监听,所以暂时不添加这个功能

问题二:如何让选中的star和未选中的star重叠显示?

  • 其实使用我们前面学习stack这个widget即可
child: Stack(
        children:[
          Row(mainAxisSize: MainAxisSize.min, children: buildNormalSatrt()),
          Row(mainAxisSize: MainAxisSize.min, children: buildSelectedSart())
        ],
      ),

问题三:如何对最好选中的start进行裁剪?

  • 可以使用ClipRect定制CustomClipper进行裁剪
  • 定义CustomClipper裁剪规则
/* 裁剪我们使用ClipRect, 该Widget有一个属性是clipper: 类型是CustomClipper<Rect>
*  我们查看文档可以知道,该类的实例类子类中只有一个设置边框的裁剪类:ShapeBorderClipper
* 其他的子类都是私有类,没有符合我们要求的类,那么我们需要自己自定义一个子类来继承这个抽象类,然后实现抽象方法
* */
class GYStarClipper extends CustomClipper<Rect> {
  final double width;//需要裁剪的宽度

  GYStarClipper(this.width);

  //裁剪一个size大小的矩形,然后返回一个rect
  @override
  Rect getClip(Size size) {
    // TODO: implement getClip
    return Rect.fromLTRB(0, 0, width, size.height);
  }

  /*表示什么情况下重新裁剪*/
  @override
  bool shouldReclip(GYStarClipper oldClipper) {
    // TODO: implement shouldReclip
    return oldClipper.width != width;  //旧的裁剪宽度和现在要裁剪的宽度不相等的时候,我们需要重新裁剪
  }
}
  • 然后使用GYStarClipper进行裁剪:
final clipStar = ClipRect(
      child: star,
      clipper: GYStarClipper(leftValue * this.widget.ratingSize),
    );

1.3 最终实现代码

import 'package:flutter/material.dart';

class GYStartRating extends StatefulWidget {
  /*当前需要显示的分数*/
  final double rating;
  /*最大分数*/
  final double maxRating;
  /*要显示几个星星*/
  final int count;
  /*每个星星的大小*/
  final double ratingSize;
  final Color normalColor;
  final Color selectedColor;

  //传入需要显示的Widget
  final Widget unselectedWidget;
  final Widget selectedWidget;

  GYStartRating({
    Key? key,
    required this.rating,
    this.maxRating = 10,
    this.count = 5,
    this.ratingSize = 30,
    this.normalColor = Colors.grey,
    this.selectedColor = Colors.orange,
    Widget? unselectedWidget,
    Widget? selectedWidget
  }) : unselectedWidget = unselectedWidget ?? Icon(Icons.star_border, size: ratingSize,color: normalColor,),
        selectedWidget = selectedWidget ?? Icon(Icons.star, size: ratingSize, color: selectedColor,),
        super(key: key);

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

class _GYStartRatingState extends State<GYStartRating> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Stack(
        children:[
          Row(mainAxisSize: MainAxisSize.min, children: buildNormalSatrt()),
          Row(mainAxisSize: MainAxisSize.min, children: buildSelectedSart())
        ],
      ),
    );
  }

  /* 得到未选中的rating*/
  List<Widget> buildNormalSatrt(){
    return List.generate(this.widget.count, (index) {
      return this.widget.unselectedWidget;
    });
  }

  List<Widget> buildSelectedSart() {
    List<Widget> starts = [];
    final star = this.widget.selectedWidget;

    //计算完整的start有几颗
    double oneValue = this.widget.maxRating / this.widget.count;
    int entireCount = (this.widget.rating / oneValue).floor();
    //for循环
    for(var i = 0; i < entireCount; i++) {
      starts.add(star);
    }

    // 计算没有小数位的star
    double leftValue = (this.widget.rating / oneValue) - entireCount;
    final clipStar = ClipRect(
      child: star,
      clipper: GYStarClipper(leftValue * this.widget.ratingSize),
    );
    starts.add(clipStar);

    return starts;
  }
}

/* 裁剪我们使用ClipRect, 该Widget有一个属性是clipper: 类型是CustomClipper<Rect>
*  我们查看文档可以知道,该类的实例类子类中只有一个设置边框的裁剪类:ShapeBorderClipper
* 其他的子类都是私有类,没有符合我们要求的类,那么我们需要自己自定义一个子类来继承这个抽象类,然后实现抽象方法
* */
class GYStarClipper extends CustomClipper<Rect> {
  final double width;//需要裁剪的宽度

  GYStarClipper(this.width);

  //裁剪一个size大小的矩形,然后返回一个rect
  @override
  Rect getClip(Size size) {
    // TODO: implement getClip
    return Rect.fromLTRB(0, 0, width, size.height);
  }

  /*表示什么情况下重新裁剪*/
  @override
  bool shouldReclip(GYStarClipper oldClipper) {
    // TODO: implement shouldReclip
    return oldClipper.width != width;  //旧的裁剪宽度和现在要裁剪的宽度不相等的时候,我们需要重新裁剪
  }

}

2. DashedLine(虚线)Widget

2.1 实现思路和代码

  • 参数介绍:

    • axis:确定虚线的方向
    • dashedWidth:根据虚线的方向来设置虚线的宽度
    • dashedHeight:根据虚线的方向来设置虚线的高度
    • color:虚线的颜色
    • dashedSpaceWitdh:虚线的间隔
  • 思路分析:

    • 虚线的个数是根据容器的宽度, 和传入的虚线宽高,虚线的间隔 来计算虚线的高度的
    • 这里是根据方向,获取父容器的宽度和高度来决定的
    • 通过LayoutBuilderwidget可以获取到父widget的宽度和高度
  • 最终代码实现:

import 'package:flutter/material.dart';

class GYDashedLine extends StatelessWidget {
  //虚线的方向
  final Axis axis;
  // 虚线的宽度
  final double dashedWidth;
  // 虚线的长度
  final double dashedHeight;
  //虚线的颜色
  final Color color;
  //虚线分割宽度
  final double dashedSpaceWitdh;

  GYDashedLine({this.axis = Axis.horizontal,
    this.dashedWidth = 1,
    this.dashedSpaceWitdh = 5,
    this.dashedHeight = 1,
    this.color = Colors.orange});


  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints){
        //根据父容器的宽度 和虚线的宽度 和间隔 来计算虚线的个数
        //constraints.biggest.width 获取组件在父组件中设置的最大宽度
        //constraints.biggest.height 获取组件在父组件中设置的最大高度
        int count = 0;
        var direction = this.axis == Axis.horizontal ? true : false;
        if (direction) {
          count = (constraints.biggest.width / (this.dashedWidth + this.dashedSpaceWitdh)).floor();
        } else {
          count = (constraints.biggest.height / (this.dashedHeight + this.dashedSpaceWitdh)).floor();
        }
        return Flex(
          direction: axis,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: List.generate(count, (index) {
            return SizedBox(
              width: this.dashedWidth,
              height: this.dashedHeight,
              child: DecoratedBox(
                decoration: BoxDecoration(
                    color: this.color
                ),
              ),
            );
          }),
        );
      },
    );
  }
}

2.2 LayoutBuilder(获取父组件的大小)

const LayoutBuilder({
    Key? key,
    required LayoutWidgetBuilder builder,
  }) : assert(builder != null),
       super(key: key, builder: builder);

/// The signature of the [LayoutBuilder] builder function.
typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints);


LayoutBuilder(
    builder: (context,constraints){
      	context为父级上下文
      	constraints.biggest.height;  获取组件在父组件所能设置的最大高度
      	contraints.maxWidth;  获取父组件宽度,高度同理
      	
        return 组件
     } 
  )

3. 实现底部TabBar

3.1 tabBar的实现说明

Flutter中,我们会使用Scaffold来搭建页面的基本结构,实际上它里面有一个属性就可以实现底部TabBar功能:bottomNavigationBar

bottomNavigationBar对应的类型是BottomNavigationBar,我们来看一下它有什么属性:

BottomNavigationBar({
    Key? key,
    required this.items,//底部展示items
    this.onTap,//监听点击方法
    this.currentIndex = 0,//当前选中的item的下标
    this.elevation,
    this.type,
    Color? fixedColor,
    this.backgroundColor,
    this.iconSize = 24.0,
    Color? selectedItemColor,
    this.unselectedItemColor,
    this.selectedIconTheme,
    this.unselectedIconTheme,
    this.selectedFontSize = 14.0,
    this.unselectedFontSize = 12.0,
    this.selectedLabelStyle,
    this.unselectedLabelStyle,
    this.showSelectedLabels,
    this.showUnselectedLabels,
    this.mouseCursor,
    this.enableFeedback,
  })

当我们实现底部Tabbar之后我们需要监听它的点击来切换不同的页面,这个时候我们可以使用IndexStackwidget来管理多个页面的切换

class GYMainPage extends StatefulWidget {
  const GYMainPage({Key? key}) : super(key: key);

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

class _GYMainPageState extends State<GYMainPage> {
  var _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        children: pages,
        index: _currentIndex,
      ),
      bottomNavigationBar: BottomNavigationBar(
        //当你的item大于等于4个后,如果不设置该属性的值为fixed的话,那么tababr会自动隐藏你的文本
        type: BottomNavigationBarType.fixed,
        //bottomNavigationBar:自带点击文字有缩放效果,如果不需要则,把选中和未选中状态的文字都设置成一个大小这样文字就没有缩放效果了
        selectedFontSize: 14,
        unselectedFontSize: 14,
        currentIndex: _currentIndex,
        items: items,
        onTap: (index){
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}

//初始化item
List<GYBottomBarItem> items = [
  GYBottomBarItem("home", "首页"),
  GYBottomBarItem("subject", "书影音"),
  GYBottomBarItem("group", "小组"),
  GYBottomBarItem("mall", "市集"),
  GYBottomBarItem("profile", "我的"),
];

  • 注意:
    • 当你的item大于等于4个后,tababr会自动隐藏你的文本,这个时候我们需要设置bottomNavigationBar中的type属性值为BottomNavigationBarType.fixed ,这个时候文字才能显现出来

3.2 最终代码实现

import 'package:flutter/material.dart';
import 'widget/start_rating.dart';
import 'widget/dashedline.dart';
import 'pages/main/initialize_times.dart';


//main函数作为程序的入口
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(
            primarySwatch: Colors.green,//设置主题颜色为
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent),
        home: GYMainPage());
  }
}

class GYMainPage extends StatefulWidget {
  const GYMainPage({Key? key}) : super(key: key);

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

class _GYMainPageState extends State<GYMainPage> {
  var _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        children: pages,
        index: _currentIndex,
      ),
      bottomNavigationBar: BottomNavigationBar(
        //当你的item超过4个后,如果不设置该属性的值为fixed的话,那么tababr会自动隐藏你的文本
        type: BottomNavigationBarType.fixed,
        //bottomNavigationBar:自带点击文字有缩放效果,如果不需要则,把选中和未选中状态的文字都设置成一个大小这样文字就没有缩放效果了
        selectedFontSize: 14,
        unselectedFontSize: 14,
        currentIndex: _currentIndex,
        items: items,
        onTap: (index){
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}

import 'package:flutter/material.dart';
import '../home/home.dart';
import '../subject/subject.dart';
import '../group/group.dart';
import '../mall/mall.dart';
import '../profile/profile.dart';
import 'bottom_bar_item.dart';

//初始化item
List<GYBottomBarItem> items = [
  GYBottomBarItem("home", "首页"),
  GYBottomBarItem("subject", "书影音"),
  GYBottomBarItem("group", "小组"),
  GYBottomBarItem("mall", "市集"),
  GYBottomBarItem("profile", "我的"),
];

//初始化所有page
List<Widget> pages = [
  GYHomePage(),
  GYSubjectPage(),
  GYGropuPage(),
  GYMallPage(),
  GYProfilePage()
];

4. 首页数据请求

目前我并没有学习到详细的网络请求相关知识,所以这里网络请求是基于前面学习到的Dio库的一个简单的使用

本来是采用豆瓣数据的接口来请求数据,但是现在发现豆瓣的API都无法使用了,所以这里我们直接把相关电影的JSON数据放到本地的文件中,然后解析数据

4.1 模型数据的封装

json数据格式如下

class GYDataHandleTools {

  static Future<List<GYMovieItem>> loadJsonData() async {
    List<GYMovieItem> movieList = <GYMovieItem>[];
    //1.把json文件读取成一个字符串
    String jsonString = await rootBundle.loadString("assets/tempData.json");
    //拿到json字符串我们需要将其转换成,我们可以通过dart:convert包中的json.decode方法将其进行转化
    final jsonResult = json.decode(jsonString);
    //判断jsonresult是否是map类型
    if (jsonResult is Map) {
      var data = jsonResult["data"];
      var subject = data["subject"];

      for (Map<String, dynamic> map in subject) {
        movieList.add(GYMovieItem.forMap(map));
      }
    }
    return movieList;
  }
}


//这里我们对json数据的解析采用是手动解析
class GYMovieItem {
  String movied_id = "";
  String movied_img = "";
  String movied_title_china = "";
  String movied_title_english = "";
  String movied_title_hk = "";
  String movied_average = "";
  String movied_evaluation = "";
  String movied_director = "";
  String movied_plot = "";
  String movied_quote = "";

  GYMovieItem.forMap(Map<String, dynamic> json) {
    this.movied_id = json["id"];
    this.movied_img = json["img"];
    this.movied_title_china = json["title_china"];
    this.movied_title_english = json["title_english"];
    this.movied_title_hk = json["title_hk"];
    this.movied_average = json["average"];
    this.movied_evaluation = json["evaluation"];
    this.movied_director = json["director"];
    this.movied_plot = json["plot"];
    this.movied_quote = json以上是关于Flutter学习-阶段案例的主要内容,如果未能解决你的问题,请参考以下文章

flutter解决 dart:html 只支持 flutter_web 其他平台编译报错 Avoid using web-only libraries outside Flutter web(代码片段

Flutterflutter doctor 报错Android license status unknown. Run `flutter doctor --android-licenses‘(代码片段

Flutter学习-flutter开发初体验

Flutter -- 实例案例二:加深布局的熟练度

错误记录Flutter 混合开发获取 BinaryMessenger 报错 ( FlutterActivityAndFragmentDelegate.getFlutterEngine() )(代码片段

在 webview_flutter 中启用捏合和缩放,在哪里添加代码片段 [this.webView.getSettings().setBuiltInZoomControls(true);]