欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net

本文参考Flutter鸿蒙开发指南(七):轮播图搜索框和导航栏-CSDN博客

本文记录操作过程以及遇到的问题

在上一节中,我们已完成电商App首页布局的基础开发,成功实现了轮播图的基础搭建和自动播放功能。这一节,我们将聚焦轮播图的细节优化,重点实现「轮播图顶部搜索框」和「底部导航指示灯」两个核心交互组件,让轮播图既美观又具备实用价值,提升整体用户体验。

一、实现轮播图顶部搜索框

搜索框是电商App首页的核心交互组件之一,我们将其叠加在轮播图顶部,采用半透明样式,既不遮挡轮播图内容,又能清晰展示搜索功能。实现思路:利用Stack布局,将搜索框组件叠加在轮播图上方,通过Positioned组件控制搜索框的位置,配合样式优化实现美观效果。

核心操作:修改上一节编写的lib/components/Home/HmSlider.dart文件,在原有轮播图代码基础上,新增搜索框相关代码,完整修改后代码如下:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';
 
class HmSlider extends StatefulWidget {
  //父传子
  final List<BannerItem> bannerList;
 
  HmSlider({Key? key, required this.bannerList}) : super(key: key);
 
  @override
  State<HmSlider> createState() => _HmSliderState();
}
 
class _HmSliderState extends State<HmSlider> {
  Widget _getSlider() {
    //在Flutter中获取屏幕宽度的方法:媒体查询对象: MediaQuery
    final double screenWidth = MediaQuery.of(context).size.width; //屏幕宽度
 
    //返回轮播图插件
    //根据数据渲染的不同的轮播选项
    return CarouselSlider(
        items: List.generate(widget.bannerList.length, (int index) {
          return Image.network(
            widget.bannerList[index].imgUrl,
            fit: BoxFit.cover,
            width: screenWidth,
          );
        }),
        options: CarouselOptions(
            //CarouselOptions中有一个视口占比:  viewportFraction:
            //高度默认300
            height: 300,
            //调整播放间距
            autoPlayInterval: Duration(seconds: 5),
            viewportFraction: 1,
            autoPlay: true));
  }
 
 //搜索框代码
  Widget _getSearch() {
    return Positioned(
      top: 10,
      left: 0,
      right: 0,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.symmetric(horizontal: 40),
          height: 50,
          decoration: BoxDecoration(
              color: const Color.fromRGBO(0, 0, 0, 0.4),
              borderRadius: BorderRadius.circular(25)
          ),
          child: Text(
            "搜索...",
            style: TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  } 
//搜索框代码
 

  @override
  Widget build(BuildContext context) {
    //Stack里面放轮播图 再盖上搜索框 指示灯导航
    return Stack(
      children: [
        //第一个放轮播图
        _getSlider(),
        _getSearch()
      ],
    );
    // return Container(
    //     color: Colors.blue,
    //     height: 300,
    //     alignment: Alignment.center,
    //     child:
    //         Text('轮播图', style: TextStyle(color: Colors.white, fontSize: 20)));
  }
}

搜索框实现效果如下:

二、实现轮播图底部导航指示灯效果

导航指示灯用于提示用户当前轮播图的位置(共几张、当前显示第几张),同时支持点击指示灯切换对应轮播图,提升交互体验。整体实现分为4步:实现指示灯基础UI —— 实现点击指示灯切换图片 —— 完善指示灯选中/未选中样式 —— 添加指示灯切换动画,逐步优化效果。

2.1 实现图片指示灯基础UI

核心思路:继续使用Stack布局,将指示灯组件叠加在轮播图最底层(轮播图之上、搜索框之下),通过Positioned控制指示灯位于轮播图底部,利用Row布局横向排列指示灯,根据轮播图数量动态生成指示灯个数。

修改lib/components/Home/HmSlider.dart文件,在原有搜索框、轮播图代码基础上,新增指示灯UI代码,完整代码如下:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';
 
class HmSlider extends StatefulWidget {
  //父传子
  final List<BannerItem> bannerList;
 
  HmSlider({Key? key, required this.bannerList}) : super(key: key);
 
  @override
  State<HmSlider> createState() => _HmSliderState();
}
 
class _HmSliderState extends State<HmSlider> {
  Widget _getSlider() {
    //在Flutter中获取屏幕宽度的方法:媒体查询对象: MediaQuery
    final double screenWidth = MediaQuery.of(context).size.width; //屏幕宽度
 
    //返回轮播图插件
    //根据数据渲染的不同的轮播选项
    return CarouselSlider(
        items: List.generate(widget.bannerList.length, (int index) {
          return Image.network(
            widget.bannerList[index].imgUrl,
            fit: BoxFit.cover,
            width: screenWidth,
          );
        }),
        options: CarouselOptions(
            //CarouselOptions中有一个视口占比:  viewportFraction:
            //高度默认300
            height: 300,
            //调整播放间距
            autoPlayInterval: Duration(seconds: 5),
            viewportFraction: 1,
            autoPlay: true));
  }
 
  Widget _getSearch() {
    return Positioned(
      top: 10,
      left: 0,
      right: 0,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.symmetric(horizontal: 40),
          height: 50,
          decoration: BoxDecoration(
              color: const Color.fromRGBO(0, 0, 0, 0.4),
              borderRadius: BorderRadius.circular(25)),
          child: Text(
            "搜索...",
            style: TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  }
 
  //返回指示灯
  Widget _getDots() {
    return Positioned(
        left: 0,
        right: 0,
        bottom: 10,
        child: SizedBox(
          height: 40,
          width: double.infinity,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center, //主轴居中
            children: List.generate(widget.bannerList.length, (int index) {
              return Container(
                height: 6,
                width: 40,
                margin: EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(3),
                ),
              );
            }),
          ),
        ));
  }
 
  @override
  Widget build(BuildContext context) {
    //Stack里面放轮播图 再盖上搜索框 指示灯导航
    return Stack(
      children: [
        //第一个放轮播图
        _getSlider(),
        _getSearch(),
        _getDots(),
      ],
    );
    // return Container(
    //     color: Colors.blue,
    //     height: 300,
    //     alignment: Alignment.center,
    //     child:
    //         Text('轮播图', style: TextStyle(color: Colors.white, fontSize: 20)));
  }
}

指示灯基础UI效果如下:

2.2 实现点击指示灯切换对应图片

核心思路:使用carousel_slider插件提供的CarouselSliderController(轮播图控制器),绑定到轮播图组件,通过控制器的jumpToPage(index)方法,实现点击指示灯跳转至对应轮播图页面。

修改lib/components/Home/HmSlider.dart文件,新增控制器、绑定控制器并实现点击交互,完整代码如下:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmSlider extends StatefulWidget {
  //父传子
  final List<BannerItem> bannerList;

  HmSlider({Key? key, required this.bannerList}) : super(key: key);

  @override
  State<HmSlider> createState() => _HmSliderState();
}

class _HmSliderState extends State<HmSlider> {
  CarouselSliderController _controller = CarouselSliderController();

  Widget _getSlider() {
    //在Flutter中获取屏幕宽度的方法:媒体查询对象: MediaQuery
    final double screenWidth = MediaQuery.of(context).size.width; //屏幕宽度

    //返回轮播图插件
    //根据数据渲染的不同的轮播选项
    return CarouselSlider(
      carouselController: _controller, //绑定controller对象
      items: List.generate(widget.bannerList.length, (int index) {
        return Image.network(
          widget.bannerList[index].imgUrl,
          fit: BoxFit.cover,
          width: screenWidth,
        );
      }),
      options: CarouselOptions(
        //CarouselOptions中有一个视口占比:  viewportFraction:
        //高度默认300
        height: 300,
        //调整播放间距
        //autoPlayInterval: Duration(seconds: 5),
        viewportFraction: 1,
        autoPlay: false,
      ),
    );
  }

  Widget _getSearch() {
    return Positioned(
      top: 10,
      left: 0,
      right: 0,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.symmetric(horizontal: 40),
          height: 50,
          decoration: BoxDecoration(
            color: const Color.fromRGBO(0, 0, 0, 0.4),
            borderRadius: BorderRadius.circular(25),
          ),
          child: Text(
            "搜索...",
            style: TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  }

  //返回指示灯
  Widget _getDots() {
    return Positioned(
      left: 0,
      right: 0,
      bottom: 10,
      child: SizedBox(
        height: 40,
        width: double.infinity,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center, //主轴居中
          children: List.generate(widget.bannerList.length, (int index) {
            return GestureDetector(
              onTap: () {
                _controller.jumpToPage(index);
              },
              child: Container(
                height: 6,
                width: 40,
                margin: EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(3),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    //Stack里面放轮播图 再盖上搜索框 指示灯导航
    return Stack(
      children: [
        //第一个放轮播图
        _getSlider(),
        _getSearch(),
        _getDots(),
      ],
    );
    // return Container(
    //     color: Colors.blue,
    //     height: 300,
    //     alignment: Alignment.center,
    //     child:
    //         Text('轮播图', style: TextStyle(color: Colors.white, fontSize: 20)));
  }
}

点击交互效果测试

重新运行项目,点击不同的指示灯,可实现以下效果:​

  • 点击第一个指示灯,轮播图立即跳转至第一张图片;​
  • 点击第二个指示灯,轮播图立即跳转至第二张图片,以此类推;​
  • 点击交互流畅,无卡顿,跳转后指示灯暂未区分选中/未选中状态(后续完善);

2.3 完善指示灯切换效果(区分选中/未选中状态)

核心思路:新增_currentIndex变量,用于记录当前轮播图的索引;通过CarouselOptions的onPageChanged回调,监听轮播图页面切换,实时更新_currentIndex;使用三元表达式,根据_currentIndex动态修改指示灯的宽度和颜色,区分选中与未选中状态。

修改lib/components/Home/HmSlider.dart文件,完善指示灯样式,完整代码如下:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmSlider extends StatefulWidget {
  //父传子
  final List<BannerItem> bannerList;

  HmSlider({Key? key, required this.bannerList}) : super(key: key);

  @override
  State<HmSlider> createState() => _HmSliderState();
}

class _HmSliderState extends State<HmSlider> {
  CarouselSliderController _controller =
  CarouselSliderController(); //控制 轮播图跳转的控制器
  int _currentIndex = 0;

  Widget _getSlider() {
    //在Flutter中获取屏幕宽度的方法:媒体查询对象: MediaQuery
    final double screenWidth = MediaQuery.of(context).size.width; //屏幕宽度

    //返回轮播图插件
    //根据数据渲染的不同的轮播选项
    return CarouselSlider(
      carouselController: _controller, //绑定controller对象
      items: List.generate(widget.bannerList.length, (int index) {
        return Image.network(
          widget.bannerList[index].imgUrl,
          fit: BoxFit.cover,
          width: screenWidth,
        );
      }),
      options: CarouselOptions(
        //CarouselOptions中有一个视口占比:  viewportFraction:
        //高度默认300
        height: 300,
        //调整播放间距
        //autoPlayInterval: Duration(seconds: 5),
        viewportFraction: 1,
        autoPlay: false,
        onPageChanged: (int index, reason) {
          _currentIndex = index;
          setState(() {});
        },
      ),
    );
  }

  Widget _getSearch() {
    return Positioned(
      top: 10,
      left: 0,
      right: 0,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.symmetric(horizontal: 40),
          height: 50,
          decoration: BoxDecoration(
            color: const Color.fromRGBO(0, 0, 0, 0.4),
            borderRadius: BorderRadius.circular(25),
          ),
          child: Text(
            "搜索...",
            style: TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  }

  //返回指示灯
  Widget _getDots() {
    return Positioned(
      left: 0,
      right: 0,
      bottom: 10,
      child: SizedBox(
        height: 40,
        width: double.infinity,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center, //主轴居中
          children: List.generate(widget.bannerList.length, (int index) {
            return GestureDetector(
              onTap: () {
                _controller.jumpToPage(index);
              },
              child: Container(
                height: 6,
                width: index == _currentIndex ? 40 : 20,
                margin: EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  color: index == _currentIndex ? Colors.white : Color.fromRGBO(0, 0, 0, 0.3),
                  borderRadius: BorderRadius.circular(3),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    //Stack里面放轮播图 再盖上搜索框 指示灯导航
    return Stack(
      children: [
        //第一个放轮播图
        _getSlider(),
        _getSearch(),
        _getDots(),
      ],
    );
    // return Container(
    //     color: Colors.blue,
    //     height: 300,
    //     alignment: Alignment.center,
    //     child:
    //         Text('轮播图', style: TextStyle(color: Colors.white, fontSize: 20)));
  }
}

核心修改点说明:

  1. 新增int _currentIndex = 0;:记录当前显示的轮播图索引,默认选中第一张图片;​
  2. 新增onPageChanged回调:监听轮播图页面切换(手动滑动、点击指示灯均可触发),实时更新_currentIndex,并通过setState(() {})刷新页面;​
  3. 指示灯宽度优化:width: index == _currentIndex ? 40 : 20,选中的指示灯更宽,视觉上更突出;​
  4. 指示灯颜色优化:color: index == _currentIndex ? Colors.white : Color.fromRGBO(0, 0, 0, 0.3),选中时为纯白色,未选中时为半透明黑色,对比清晰,贴合轮播图背景。
//用三元表达式改变指示灯切换效果UI的样式。
width: index == _currentIndex ? 40 : 20,


//使用三元表达式,当未切换时是黑色,切换时是白色,0.3为透明度
color: index == _currentIndex ? Colors.white : Color.fromRGBO(0, 0, 0, 0.3),

完善后指示灯效果如下:

2.4 实现指示灯切换动画效果

核心思路:使用Flutter自带的AnimatedContainer组件(动画容器)替换原来的Container,通过duration参数设置动画时长,实现指示灯选中/未选中状态切换时的平滑过渡动画,提升交互体验。

修改lib/components/Home/HmSlider.dart文件,仅修改指示灯组件部分,完整代码如下:

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:xiuhu_mall/viewmodels/home.dart';

class HmSlider extends StatefulWidget {
  //父传子
  final List<BannerItem> bannerList;

  HmSlider({Key? key, required this.bannerList}) : super(key: key);

  @override
  State<HmSlider> createState() => _HmSliderState();
}

class _HmSliderState extends State<HmSlider> {
  CarouselSliderController _controller =
  CarouselSliderController(); //控制 轮播图跳转的控制器
  int _currentIndex = 0;

  Widget _getSlider() {
    //在Flutter中获取屏幕宽度的方法:媒体查询对象: MediaQuery
    final double screenWidth = MediaQuery.of(context).size.width; //屏幕宽度

    //返回轮播图插件
    //根据数据渲染的不同的轮播选项
    return CarouselSlider(
      carouselController: _controller, //绑定controller对象
      items: List.generate(widget.bannerList.length, (int index) {
        return Image.network(
          widget.bannerList[index].imgUrl,
          fit: BoxFit.cover,
          width: screenWidth,
        );
      }),
      options: CarouselOptions(
        //CarouselOptions中有一个视口占比:  viewportFraction:
        //高度默认300
        height: 300,
        //调整播放间距
        autoPlayInterval: Duration(seconds: 5),
        viewportFraction: 1,
        autoPlay: true,
        onPageChanged: (int index, reason) {
          _currentIndex = index;
          setState(() {});
        },
      ),
    );
  }

  Widget _getSearch() {
    return Positioned(
      top: 10,
      left: 0,
      right: 0,
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Container(
          alignment: Alignment.centerLeft,
          padding: EdgeInsets.symmetric(horizontal: 40),
          height: 50,
          decoration: BoxDecoration(
            color: const Color.fromRGBO(0, 0, 0, 0.4),
            borderRadius: BorderRadius.circular(25),
          ),
          child: Text(
            "搜索...",
            style: TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  }

  //返回指示灯
  Widget _getDots() {
    return Positioned(
      left: 0,
      right: 0,
      bottom: 10,
      child: SizedBox(
        height: 40,
        width: double.infinity,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center, //主轴居中
          children: List.generate(widget.bannerList.length, (int index) {
            return GestureDetector(
              onTap: () {
                _controller.jumpToPage(index);
              },
              child: AnimatedContainer(
                duration: Duration(milliseconds: 300),
                height: 6,
                width: index == _currentIndex ? 40 : 20,
                margin: EdgeInsets.symmetric(horizontal: 4),
                decoration: BoxDecoration(
                  color: index == _currentIndex ? Colors.white : Color.fromRGBO(0, 0, 0, 0.3),
                  borderRadius: BorderRadius.circular(3),
                ),
              ),
            );
          }),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    //Stack里面放轮播图 再盖上搜索框 指示灯导航
    return Stack(
      children: [
        //第一个放轮播图
        _getSlider(),
        _getSearch(),
        _getDots(),
      ],
    );
    // return Container(
    //     color: Colors.blue,
    //     height: 300,
    //     alignment: Alignment.center,
    //     child:
    //         Text('轮播图', style: TextStyle(color: Colors.white, fontSize: 20)));
  }
}

动画效果说明

修改完成后,重新运行项目,指示灯切换时将呈现以下动画效果:

  • 轮播图自动切换、手动滑动或点击指示灯时,指示灯的宽度和颜色将在300毫秒内平滑过渡,无生硬跳转;​
  • 动画过渡流畅,与轮播图切换节奏呼应,提升整体交互的细腻度;​
  • 多端适配:动画效果在鸿蒙、Android端均能正常显示,无差异;​
  • 可根据需求调整duration: Duration(milliseconds: 300)中的毫秒数,数值越小动画越快,数值越大动画越平缓。

三、上传代码及总结

3.1 上传代码

git add .
git commit -m "完成轮播图搜索框和指示灯"
git push

3.2 总结

本文衔接上一节轮播图基础功能,实现了电商App首页轮播图的两个核心细节组件——搜索框和导航指示灯,从基础UI到交互优化、动画效果,逐步完善,核心总结如下:

  1. 搜索框实现:利用Stack+Positioned布局,将半透明圆角搜索框叠加在轮播图顶部,既美观又不遮挡轮播图内容,适配多端屏幕;​
  2. 指示灯实现:分4步逐步优化,从基础UI → 点击交互 → 选中状态区分 → 切换动画,核心依赖CarouselSliderController控制器和AnimatedContainer动画组件;​
  3. 核心知识点:Stack层叠布局的使用、CarouselSliderController的绑定与使用、AnimatedContainer平滑动画的实现、三元表达式动态修改组件样式;​
  4. 避坑重点:控制器必须与轮播图绑定才能实现跳转;onPageChanged回调需配合setState刷新页面;指示灯动画需使用AnimatedContainer替换普通Container;​
  5. 功能延伸:本文实现的搜索框为基础样式,后续可扩展为可输入搜索框(添加TextField组件);指示灯可扩展为圆形样式、添加更多动画效果,进一步完善用户体验。

至此,轮播图的基础功能(基础渲染、自动播放、搜索框、导航指示灯)已全部实现。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐