ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Flutter 布局实战 仿携程网格卡片布局

2021-07-12 20:33:24  阅读:204  来源: 互联网

标签:title url items 布局 Flutter item child 携程网 icon


参考:https://coding.imooc.com/class/321.html

最终效果:
在这里插入图片描述


上源码:如有纰漏,敬请指出~

import 'package:doucan_flutter/model/navi/home_grid_nav_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_color_plugin/flutter_color_plugin.dart';

const DEF_ICON = '....png';

/// 仿携程网格导航模块
/// UI分析:
/// 1、该模块四处有圆角:PhysicalModel
/// 2、由三组横向排列的模块纵向排列构成:column包裹3个row
/// 3、横向模块,有三个子模块等分排列:子模块均用Expanded-flex=1包裹
/// 4、左边子模块:背景图片+文字在上层,Stack
/// 5、中间子模块:两个文字上下排列,且均有左边框,上门的文字有底边框:borderSide
/// 6、右边子模块,和中间子模块一样
class GridNav extends StatefulWidget {
  final List<ModelsData> models;

  const GridNav({Key? key, required this.models}) : super(key: key);

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

class _GridNavState extends State<GridNav> {
  @override
  Widget build(BuildContext context) {
    //使用PhysicalModel对最终的布局Widget进行圆角裁剪
    return PhysicalModel(
      color: Colors.transparent,
      borderRadius: BorderRadius.circular(10),
      clipBehavior: Clip.antiAlias,
      //纵向排列的,直接使用column。一般都不需要加出child外的属性(长度如何计算?自动根据子布局撑开?)
      child: Column(
        children: _rowItems(widget.models),
      ),
    );
  }

  List<Widget> _rowItems(List<ModelsData> models) {
    List<Widget> rowItems = [];
    if (models == null || models.isEmpty) {
      return rowItems;
    }
    models.forEach((model) {
      rowItems.add(_rowItem(model, models.indexOf(model) == models.length - 1));
    });
    return rowItems;
  }

  /// isEnd -- 判断当前items是否为最后一个,若是,则不再添加底部的margin,防止PhysicalModel裁剪不正确
  Widget _rowItem(ModelsData model, isEnd) {
    List<Widget> items = [];
    items.add(_leftItem(model.items[0]));
    items.add(_middleItem(model.items[1], model.items[2]));
    items.add(_rightItem(model.items[3], model.items[4]));
    List<Widget> expendItems = [];
    items.forEach((element) {
      //因为横向布局的三个子布局宽度均分,所以使用Expanded包裹每一个子布局
      expendItems.add(Expanded(
        child: element,
        // flex: 1,
      ));
    });
    //一般横向布局使用container包裹,便于设置宽高、内外边距等
    return Container(
      //横向布局间有间隙,container中添加底部margin即可
      margin: isEnd ? EdgeInsets.only(bottom: 0) : EdgeInsets.only(bottom: 2),
      //横向布局的高度需要确定,根据UI设计稿来设置,不然图片大小不好处理
      height: 88,
      //添加线性渐变的背景色
      decoration: BoxDecoration(
          gradient: LinearGradient(colors: [
        ColorUtil.color(model.startColor),
        ColorUtil.color(model.endColor)
      ])),
      child: Row(
        children: expendItems,
      ),
    );
  }

  Widget _leftItem(ItemsData item) {
    return _wrapGesture(
        item,
        //该布局为底部是图片,顶部是文字,层叠而成
        Stack(
          //文字是顶部居中显示的
          alignment: Alignment.topCenter,
          children: [
            Image.network(
              item.icon == '' ? DEF_ICON : item.icon,
              //宽高是根据设计稿设置的,设计稿里面是一副(88*120)(单位点)的透明背景图片
              height: 88,
              width: 121,
              //图片需要完全显示出来
              fit: BoxFit.contain,
              //实际图片可能没有设置的图片的宽高,设置该属性可以避免图片没有贴底显示
              alignment: AlignmentDirectional.bottomEnd,
            ),
            //文字层叠在图片上面
            Container(
              margin: EdgeInsets.only(top: 11),
              child: Text(
                item.title,
                style: TextStyle(fontSize: 14, color: Colors.white),
              ),
            )
          ],
        ));
  }

  Widget _middleItem(ItemsData item1, ItemsData item2) {
    ///中间的布局是两个等高的文字竖排
    ///使用Column竖向排列,高度如何计算的?
    ///使用Expanded包裹每一个子布局,平分Column竖向的空间
    return Column(
      children: [
        Expanded(child: _item(item1, true)),
        Expanded(child: _item(item2, false)),
      ],
    );
  }

  Widget _rightItem(ItemsData item1, ItemsData item2) {
    return Column(
      children: [
        Expanded(child: _item(item1, true)),
        Expanded(child: _item(item2, false)),
      ],
    );
  }

  //布局中有多个需要点击的部分,且点击逻辑一致,故进行封装
  Widget _wrapGesture(ItemsData item, child) {
    return GestureDetector(
      onTap: () {
        print(item.title);
      },
      child: child,
    );
  }

  ///文字item布局
  _item(ItemsData item, bool isTop) {
    BorderSide borderSide = BorderSide(width: 0.8, color: Colors.white);
    //文字item布局的父布局高度已经撑好了(宽度应该也撑好了吧?)
    //但Container是自适应子元素的,而子元素的Text宽度是自适应文字的
    //故,要使用FractionallySizedBox把Container的宽度撑开-撑满
    return FractionallySizedBox(
      widthFactor: 1,
      child: Container(
        //子元素居中(当我们要把子元素居于哪个位置时,使用该属性)
        alignment: Alignment.center,
        decoration: BoxDecoration(
            //添加边框,营造表格感:控制好添加那条边的逻辑
            border: Border(
          left: borderSide,
          bottom: isTop ? borderSide : BorderSide.none,
        )),
        child: _wrapGesture(
            item,
            Text(
              item.title,
              //父布局的Container已经设置了Alignment,就由父布局来控制文字的位置好了
              //若父布局的Container没设置了Alignment,文字只能水平居中显示,达不到垂直水平居中效果
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 14, color: Colors.white),
            )),
      ),
    );
  }
}


导航菜单json:

{
	"models": [{
		"startColor": "5EFCE8",
		"endColor": "736EFE",
		"items": [{
			"title": "酒店",
			"icon": "",
			"url": ""
		}, {
			"title": "海外",
			"icon": "",
			"url": ""
		}, {
			"title": "特价",
			"icon": "",
			"url": ""
		}, {
			"title": "团购",
			"icon": "",
			"url": ""
		}, {
			"title": "民宿",
			"icon": "",
			"url": ""
		}]
	}, {
		"startColor": "43CBFF",
		"endColor": "9708CC",
		"items": [{
			"title": "机票",
			"icon": "",
			"url": ""
		}, {
			"title": "火车票·高铁票",
			"icon": "",
			"url": ""
		}, {
			"title": "特价",
			"icon": "",
			"url": ""
		}, {
			"title": "专车快车",
			"icon": "",
			"url": ""
		}, {
			"title": "船票",
			"icon": "",
			"url": ""
		}]
	}, {
		"startColor": "EA5455",
		"endColor": "FEB692",
		"items": [{
			"title": "旅游",
			"icon": "",
			"url": ""
		}, {
			"title": "门票",
			"icon": "",
			"url": ""
		}, {
			"title": "攻略",
			"icon": "",
			"url": ""
		}, {
			"title": "游轮",
			"icon": "",
			"url": ""
		}, {
			"title": "定制",
			"icon": "",
			"url": ""
		}]
	}]
}
import 'package:json_annotation/json_annotation.dart';

part 'home_grid_nav_model.g.dart';

@JsonSerializable(explicitToJson: true)
class Homegridnavmodel {
  HomegridnavmodelData data;
  int code;
  String message;

  Homegridnavmodel(this.data, this.code, this.message);

  factory Homegridnavmodel.fromJson(Map<String, dynamic> json) =>
      _$HomegridnavmodelFromJson(json);

  Map<String, dynamic> toJson() => _$HomegridnavmodelToJson(this);
}

@JsonSerializable(explicitToJson: true)
class HomegridnavmodelData {
  List<ModelsData> models;

  HomegridnavmodelData(
    this.models,
  );

  factory HomegridnavmodelData.fromJson(Map<String, dynamic> json) =>
      _$HomegridnavmodelDataFromJson(json);

  Map<String, dynamic> toJson() => _$HomegridnavmodelDataToJson(this);
}

@JsonSerializable(explicitToJson: true)
class ModelsData {
  String startColor;
  String endColor;

  List<ItemsData> items;

  ModelsData(this.items, this.startColor, this.endColor);

  factory ModelsData.fromJson(Map<String, dynamic> json) =>
      _$ModelsDataFromJson(json);

  Map<String, dynamic> toJson() => _$ModelsDataToJson(this);
}

@JsonSerializable(explicitToJson: true)
class ItemsData {
  String title;
  String icon;
  String url;

  ItemsData(
    this.title,
    this.icon,
    this.url,
  );

  factory ItemsData.fromJson(Map<String, dynamic> json) =>
      _$ItemsDataFromJson(json);

  Map<String, dynamic> toJson() => _$ItemsDataToJson(this);
}

json解析部分的代码就省略了,不是重点。

标签:title,url,items,布局,Flutter,item,child,携程网,icon
来源: https://blog.csdn.net/campchan/article/details/118683018

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有