Flutter项目之底部搜索功能实现

1、实现效果图

在这里插入图片描述

2、工具栏顶部搜索

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import 'dart:convert';

import 'package:city_pickers/city_pickers.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_haoke/config.dart';
import 'package:flutter_haoke/pages/home/tab_index/index_recommond_item.dart';
import 'package:flutter_haoke/scopoed_model/city.dart';
import 'package:flutter_haoke/utils/common_toast.dart';
import 'package:flutter_haoke/utils/model/general_type.dart';
import 'package:flutter_haoke/utils/scopoed_mode_helper.dart';
import 'package:flutter_haoke/utils/store.dart';

class SearchBar extends StatefulWidget {
  final bool? showLocation; //是否显示位置
  final void Function()? goBackCallback; //回退
  final String? inputValue; //搜索框值
  final String? defaultInputValue; //默认显示值
  final void Function()? onCancel; //取消按钮
  final bool? showMap; //是否显示地图按钮
  final void Function()? onSearch; //点击搜索框触发
  // final Function? onSearch;
  final ValueChanged<String>? onSearchSubmit; //点击按键回车触发

  const SearchBar(
      {Key? key,
      this.showLocation,
      this.goBackCallback,
      this.inputValue = '',
      this.defaultInputValue = '请输入搜索词',
      this.onCancel,
      this.showMap,
      this.onSearch,
      this.onSearchSubmit})
      : super(key: key);

  
  _SearchBarState createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  String _searchWord = '';
  late TextEditingController _controller;
  late FocusNode _focus;
  _onClean() {
    _controller.clear();
    setState(() {
      _searchWord = '';
    });
  }

  _onChangeLocation() async {
    //打开第三方的选择页面
    var resultCity = await CityPickers.showCitiesSelector(
        context: context, theme: ThemeData(primarySwatch: Colors.green));
    //选择之后返回选择的数据
    String? cityName = resultCity!.cityName;
    if (cityName == null) return;

    //检测选中的城市是否在 四个城市中
    //查找数组中是否有这个名字 有就返回这个城市model 没有就返回空的类
    var city = Config.availableCitys
        .firstWhere((city) => cityName.startsWith(city.name), orElse: () {
      CommontToast.showToast("该城市暂未开通!");
      return GeneralType('', "");
    });

    //保存选中的城市
    _saveCity(city);
  }

  _saveCity(GeneralType city) async {
    if (city.name == null ||city.name =="") return;
    //保存到全局
    ScopoedModelHelper.getModel<CityModel>(context).city = city;
    //保存到本地
    var store = await Store.getInstance();
    //转化成json字符串格式
    var cityString = json.encode(city.toJson());
    store.setString(StoreKeys.city, cityString);
  }

 
  
  void initState() {
    _focus = FocusNode();
    _controller = TextEditingController(text: widget.inputValue);
    super.initState();
  }

  
  Widget build(BuildContext context) {
    //从全局里拿到city
    var city = ScopoedModelHelper.getModel<CityModel>(context).city;
    if (city.name == null || city.name == "") {
      city = Config.availableCitys.first;
      //存储下此时的city
      _saveCity(city);
    }
    return Container(
        child: Row(
      children: [
        if (widget.showLocation != null)
          Padding(
            padding: EdgeInsets.only(right: 10),
            child: GestureDetector(
              onTap: () {
                _onChangeLocation();
              },
              child: Row(
                children: [
                  Icon(
                    Icons.room,
                    color: Colors.green,
                    size: 15,
                  ),
                  Text(
                    city.name,
                    style: TextStyle(color: Colors.black, fontSize: 14),
                  )
                ],
              ),
            ),
          ),
        if (widget.goBackCallback != null)
          Padding(
            padding: EdgeInsets.only(right: 10),
            child: GestureDetector(
                onTap: widget.goBackCallback,
                child: Icon(
                  Icons.chevron_left,
                  color: Colors.black,
                )),
          ),
        Expanded(
            child: Container(
          height: 34,
          decoration: BoxDecoration(
              color: Colors.grey.shade100,
              borderRadius: BorderRadius.circular(17)),
          margin: EdgeInsets.only(right: 10),
          child: TextField(
            focusNode: _focus,
            controller: _controller,
            style: TextStyle(fontSize: 14),
            onChanged: (value) {
              setState(() {
                _searchWord = value;
              });
            },
            onTap: () {
              //判断没有使用搜索功能则失去焦点
              if (null == widget.onSearchSubmit) {
                _focus.unfocus();
              }
              if(widget.onSearch !=null)widget.onSearch!();
            },
            onSubmitted: widget.onSearchSubmit,
            textInputAction: TextInputAction.search,
            decoration: InputDecoration(
                hintText: widget.defaultInputValue,
                hintStyle: TextStyle(color: Colors.black, fontSize: 14),
                contentPadding: EdgeInsets.only(top: 2, left: -10),
                border: InputBorder.none,
                icon: Padding(
                  padding: EdgeInsets.only(top: 4, left: 8),
                  child: Icon(
                    Icons.search,
                    size: 18,
                    color: Colors.grey,
                  ),
                ),
                suffixIcon: GestureDetector(
                  onTap: _onClean,
                  child: Icon(
                    Icons.clear,
                    size: 18,
                    color:
                        _searchWord == '' ? Colors.grey.shade100 : Colors.grey,
                  ),
                )),
          ),
        )),
        if (widget.onCancel != null)
          Padding(
            padding: EdgeInsets.only(right: 10),
            child: GestureDetector(
              onTap: widget.onCancel,
              child: Text(
                "取消",
                style: TextStyle(color: Colors.black, fontSize: 14),
              ),
            ),
          ),
        if (widget.showMap != null)
          Image.asset("static/icons/widget_search_bar_map.png")
      ],
    ));
  }
}

3、工具栏底部搜索

在这里插入图片描述

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_haoke/pages/home/tab_search/datalist.dart';
import 'package:flutter_haoke/pages/home/tab_search/filter_bar/data.dart';
import 'package:flutter_haoke/pages/home/tab_search/filter_bar/item.dart';
import 'package:flutter_haoke/scopoed_model/room_filter.dart';
import 'package:flutter_haoke/utils/common_picker/index.dart';
import 'package:flutter_haoke/utils/dio_http.dart';
import 'package:flutter_haoke/utils/model/general_type.dart';
import 'package:flutter_haoke/utils/scopoed_mode_helper.dart';

//上次选择的城市ID 用于切换不同数据请求
var lastCityId;

class FilterBar extends StatefulWidget {
  final ValueChanged<FilterBarResult>? onChange;

  const FilterBar({Key? key, this.onChange}) : super(key: key);

  
  _FilterBarState createState() => _FilterBarState();
}

class _FilterBarState extends State<FilterBar> {
  List<GeneralType> areaList = [];
  List<GeneralType> priceList = [];
  List<GeneralType> rentTypeList = [];
  List<GeneralType> roomTypeList = [];
  List<GeneralType> orientedList = [];
  List<GeneralType> floorList = [];

  bool isAreaActive = false;
  bool isRentTypeActive = false;
  bool isPriceActive = false;
  bool isFilterActive = false;

  String areaId = "";
  String rentTypeId = "";
  String priceId = "";
  List<String> moreIds = [];

//选择区域方法
  _onChangeArea(context) {
    setState(() {
      isAreaActive = true;
    });
    var result = CommonPicker.showPicker(
        context: context,
        options: areaList.map((item) => item.name).toList(),
        value: 0);
    result!.then((index) {
      setState(() {
        areaId = areaList[index].id;
      });
      _onChange();
    }).whenComplete(() {
      setState(() {
        //动作结束的时候调用
        isAreaActive = false;
      });
    });
  }

//选择租金方法
  _onChangeRentType(context) {
    setState(() {
      isRentTypeActive = true;
    });
    var result = CommonPicker.showPicker(
        context: context,
        options: rentTypeList.map((item) => item.name).toList(),
        value: 0);
    result!.then((index) {
      setState(() {
        rentTypeId = rentTypeList[index].id;
      });
      _onChange();
    }).whenComplete(() {
      setState(() {
        //动作结束的时候调用
        isRentTypeActive = false;
      });
    });
  }

  //选择价格方法
  _onChangePrice(context) {
    setState(() {
      isPriceActive = true;
    });
    var result = CommonPicker.showPicker(
        context: context,
        options: priceList.map((item) => item.name).toList(),
        value: 0);
    result!.then((index) {
      setState(() {
        priceId = priceList[index].id;
      });
      _onChange();
    }).whenComplete(() {
      setState(() {
        //动作结束的时候调用
        isPriceActive = false;
      });
    });
  }

  _onChangeFliter(context) {
    Scaffold.of(context).openEndDrawer(); //打开右侧的抽屉
  }

  _onChange() {
    //获取选中的数据
    var selectIds = ScopoedModelHelper.getModel<FilterBarModel>(context)
        .selectedList
        .toList();
    if (widget.onChange != null) {
      widget.onChange!(FilterBarResult(
          areaId: areaId,
          priceId: priceId,
          rentTypeId: rentTypeId,
          moreIds: selectIds));
    }
  }

  _getData() async {
//调用网络获取数据
    var cityId = ScopoedModelHelper.getAreaId(context);
    //给本次选择的数据赋值
    lastCityId = cityId;
    var url = "/houses/condition?id=$cityId";
    var result = await DioHttp.of(context).get(url);
    print(result);
    if (!this.mounted) {
      //判断本页面是否存在 如果已销毁 则不用进行以下赋值操作
      return;
    }
    //接口不通 伪造数据赋值情况如下----------假数据-------
    List<GeneralType> areaList = [
      GeneralType('区域1', '11'),
      GeneralType('区域2', '22'),
    ];
    List<GeneralType> priceList = [
      GeneralType('价格1', 'bb'),
      GeneralType('价格2', 'aa'),
    ];
    List<GeneralType> rentTypeList = [
      GeneralType('出租类型1', 'bb'),
      GeneralType('出租类型2', '22'),
    ];
    List<GeneralType> roomTypeList = [
      GeneralType('房屋类型1', '11'),
      GeneralType('房屋类型2', '22'),
    ];
    List<GeneralType> orientedList = [
      GeneralType('方向1', '99'),
      GeneralType('方向2', 'cc'),
    ];
    List<GeneralType> floorList = [
      GeneralType('楼层1', 'aa'),
      GeneralType('楼层2', 'bb'),
    ];
    setState(() {
      this.areaList = areaList;
      this.priceList = priceList;
      this.rentTypeList = rentTypeList;
      this.roomTypeList = roomTypeList;
      this.orientedList = orientedList;
      this.floorList = floorList;
    });

    //接口不通 伪造数据赋值情况如上----------假数据-------

    Map<String, List<GeneralType>> datalist = Map<String, List<GeneralType>>();
    datalist["roomTypeList"] = roomTypeList;
    datalist["orientedList"] = orientedList;
    datalist["floorList"] = floorList;

    //传入所有要展示的数据
    ScopoedModelHelper.getModel<FilterBarModel>(context).dataList = datalist;
  }

  
  void initState() {
    //这个方法在这里调用只走一次
    Timer.run(_getData);
    super.initState();
  }

  
  void didChangeDependencies() {
    //每次加载都会走这个方法
    //判断上次的城市选择和本次是否相同
    if (lastCityId != "" &&
        lastCityId != ScopoedModelHelper.getAreaId(context)) {
      //刷新数据
      _getData();
    }
    _onChange();
    super.didChangeDependencies();
  }

  
  Widget build(BuildContext context) {
    return Container(
      height: 41,
      decoration: BoxDecoration(
          border: Border(bottom: BorderSide(width: 1, color: Colors.grey))),
      child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
        Item(
          title: "区域",
          isActive: isAreaActive,
          onTap: _onChangeArea,
        ),
        Item(
          title: "方式",
          isActive: isRentTypeActive,
          onTap: _onChangeRentType,
        ),
        Item(
          title: "租金",
          isActive: isPriceActive,
          onTap: _onChangePrice,
        ),
        Item(
          title: "筛选",
          isActive: isFilterActive,
          onTap: _onChangeFliter,
        ),
      ]),
    );
  }
}

3.1、主体结构

在这里插入图片描述

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_haoke/pages/home/tab_search/datalist.dart';
import 'package:flutter_haoke/pages/home/tab_search/filter_bar/data.dart';
import 'package:flutter_haoke/pages/home/tab_search/filter_bar/filter_drawer.dart';
import 'package:flutter_haoke/pages/home/tab_search/filter_bar/index.dart';
import 'package:flutter_haoke/scopoed_model/room_filter.dart';
import 'package:flutter_haoke/utils/dio_http.dart';
import 'package:flutter_haoke/utils/model/room_list_item_data.dart';
import 'package:flutter_haoke/utils/scopoed_mode_helper.dart';
import 'package:flutter_haoke/widget/room_list_item_widget.dart';
import 'package:flutter_haoke/widget/search_bar/index.dart';

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

  
  _TabSearchState createState() => _TabSearchState();
}

class _TabSearchState extends State<TabSearch> {
  //存储网络获取的数据
  List<RoomListItemData> list = [];
  _onfilterBarChange(FilterBarResult data) async {
    //获取所有选中的数据
    var cityId =
        Uri.decodeQueryComponent(ScopoedModelHelper.getAreaId(context));
    var area = Uri.decodeQueryComponent(data.areaId!);
    var mode = Uri.decodeQueryComponent(data.rentTypeId!);
    var price = Uri.decodeQueryComponent(data.priceId!);
    var more = Uri.decodeQueryComponent(data.moreIds!.join(","));
    //组装URL参数
    String url =
        '/houses?cityId=$cityId&area=$area&mode=$mode&price=$price&more=$more';
    var result = await DioHttp.of(context).get(url);
    // 网络数据返回----真实数据----------
    // var resultMap = json.decode(result.toString());
    // List<RoomListItemData> datalist = resultMap["data"]["list"]
    //     .map((json) => RoomListItemData.fromJson(json))
    //     .toList();
    // setState(() {
    //   list = datalist;
    // });
    // --------真实数据----------
    //以上请求不通 --------模拟数据---------------
    const List<RoomListItemData> datas = [
      const RoomListItemData(
          '0',
          '朝阳门南大街 2室1厅 8300元',
          "二室/114/东|北/朝阳门南大街",
          "https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu9t1kxj30lo0c7796.jpg",
          ["近地铁", "集中供暖", "新上", "随时看房"],
          1200),
      const RoomListItemData(
        '144',
        '整租 · CBD总部公寓二期 临近国贸 精装修 随时拎包入住',
        "一室/110/西/CBD总部公寓二期",
        "https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu5s7gcj30lo0c7myq.jpg",
        ["近地铁", "随时看房"],
        6000,
      ),
      const RoomListItemData(
        '155',
        '朝阳门南大街 2室1厅 8300元',
        "二室/114/东|北/朝阳门南大街",
        "https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu5s7gcj30lo0c7myq.jpg",
        ["近地铁", "集中供暖", "新上", "随时看房"],
        1200,
      ),
      const RoomListItemData(
        '166',
        '整租 · CBD总部公寓二期 临近国贸 精装修 随时拎包入住',
        "一室/110/西/CBD总部公寓二期",
        "https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu9t1kxj30lo0c7796.jpg",
        ["近地铁", "随时看房"],
        6000,
      ),
    ];
    setState(() {
      //刷新数据了-----
      print('刷新数据了');
      list = datas;
    });
    //--------模拟数据---------------
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      endDrawer: FilterDrawer(),
      appBar: AppBar(
        actions: [Container()],
        elevation: 0,
        title: SearchBar(
          showLocation: true,
          showMap: true,
          onSearch: () {
            Navigator.of(context).pushNamed("search");
          },
        ),
        backgroundColor: Colors.white,
      ),
      body: Column(
        children: [
          Container(
            height: 41,
            child: FilterBar(
              onChange: _onfilterBarChange,
            ),
          ),
          Expanded(
            child: ListView(
              children: list.map((e) {
                return RoomListItemWidget(e);
              }).toList(),
            ),
          )
        ],
      ),
    );
  }
}

3.2、item部分

在这里插入图片描述

import 'package:flutter/material.dart';
import 'package:flutter_haoke/config.dart';
import 'package:flutter_haoke/pages/home/tab_search/datalist.dart';
import 'package:flutter_haoke/utils/model/room_list_item_data.dart';
import 'package:flutter_haoke/widget/common_tag.dart';

class RoomListItemWidget extends StatelessWidget {
  final RoomListItemData data;

  const RoomListItemWidget(
    this.data, {
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    //判断返回的图片地址是否包含http
    var imageUri = data.imageUrl.startsWith('http')
        ? data.imageUrl
        : Config.BaseUrl + data.imageUrl;
    return GestureDetector(
      onTap: () {
        Navigator.of(context).pushNamed("roomDetail/${data.id}");
      },
      child: Container(
        padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
        child: Row(
          children: [
            Image.network(
              imageUri,
              width: 130,
              height: 100,
            ),
            Padding(padding: EdgeInsets.only(left: 10)),
            Expanded(
                child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  data.title,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
                ),
                Text(
                  data.subTitle,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                Wrap(
                //CommonTag(item)标签代码部分的使用
                  children: data.tags.map((item) => CommonTag(item)).toList(),
                ),
                Text(
                  "${data.price}元/月",
                  style: TextStyle(
                      color: Colors.orange, fontWeight: FontWeight.w600),
                )
              ],
            ))
          ],
        ),
      ),
    );
  }
}

3.3、标签tag部分

在这里插入图片描述

import 'package:flutter/material.dart';

class CommonTag extends StatelessWidget {
  final String title; //这是外部传递过来的
  final Color color; //自己的
  final Color backgroundColor;
  const CommonTag.origin(
      this.title,
      {Key? key,
      this.color = Colors.black,
      this.backgroundColor = Colors.grey})
      : super(key: key); //注意外部和自己的写法

  // 工厂函数
  factory CommonTag(String title) {
    switch (title) {
      case '近地铁':
        return CommonTag.origin(
          title, //title子不变,其它值都改变
          color: Colors.red,
          backgroundColor: Colors.red.shade100,
        );
      case '集中供暖':
        return CommonTag.origin(
           title, //title子不变,其它值都改变
          color: Colors.blue,
          backgroundColor: Colors.blue.withOpacity(.5),
        );
      case '新上':
        return CommonTag.origin(
       title, //title子不变,其它值都改变
          color: Colors.green,
          backgroundColor: Colors.green.shade100,
        );
      case '随时看房':
        return CommonTag.origin(
          title, //title子不变,其它值都改变
          color: Colors.orange,
          backgroundColor: Colors.orange.shade100,
        );

      default:
        return CommonTag.origin( title);
    }
  }

  
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(right: 4.0),
      padding: EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0, bottom: 2.0),
      decoration: BoxDecoration(
          color: backgroundColor, //工厂里自定义的背景
          borderRadius: BorderRadius.circular(8.0)),
      child: Text(
        title, //工厂里自定义的文字
        style: TextStyle(fontSize: 10.0, color: color //工厂里自定义的字体颜色
            ),
      ),
    );
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bst@微胖子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值