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

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

最终效果:
Flutter 布局实战 仿携程网格卡片布局


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

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解析部分的代码就省略了,不是重点。

上一篇:2021-07-28


下一篇:vue中is的用法