IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Flutter学习-阶段案例 -> 正文阅读

[移动开发]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["quote"];
  }
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-17 15:30:31  更:2021-08-17 15:31:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:49:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码