在这里插入图片描述

搜索功能是音乐播放器中使用频率最高的功能之一。用户可以通过搜索快速找到想听的歌曲、歌手或专辑。本篇文章将详细介绍如何实现一个功能完善的搜索页面,包括搜索建议、热门搜索、搜索历史以及多类型搜索结果展示。

页面基础结构

搜索页面使用StatefulWidget,因为需要管理搜索框内容、搜索状态等多个状态变量。

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  
  State<SearchPage> createState() => _SearchPageState();
}

页面继承自StatefulWidget,使用GetX进行路由管理。搜索页面的交互比较复杂,需要响应用户的输入和点击操作。

状态变量定义

搜索页面需要管理多个状态,包括输入控制器、Tab控制器和搜索状态标志。

class _SearchPageState extends State<SearchPage> with SingleTickerProviderStateMixin {
  final TextEditingController _controller = TextEditingController();
  late TabController _tabController;
  bool _showResult = false;

_controller用于控制搜索输入框的内容,_tabController用于控制搜索结果的Tab切换,_showResult标志决定显示搜索建议还是搜索结果。混入SingleTickerProviderStateMixin是使用TabController的必要条件。

生命周期管理

在initState中初始化TabController,在dispose中释放所有控制器资源。

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 5, vsync: this);
  }

  
  void dispose() {
    _controller.dispose();
    _tabController.dispose();
    super.dispose();
  }

TabController的length设置为5,对应单曲、歌手、专辑、歌单、MV五个搜索类型。dispose方法中同时释放输入控制器和Tab控制器,避免内存泄漏。

AppBar搜索框设计

搜索框直接放在AppBar的title位置,这是音乐类App常见的设计模式。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField(
          controller: _controller,
          autofocus: true,
          style: const TextStyle(color: Colors.white),
          decoration: const InputDecoration(
            hintText: '搜索歌曲、歌手、专辑',
            hintStyle: TextStyle(color: Colors.white54),
            border: InputBorder.none,
          ),

TextField设置了autofocus为true,进入页面时自动弹出键盘。输入文字使用白色,提示文字使用半透明白色,与深色主题协调。border设置为none,让输入框与AppBar融为一体。

搜索提交处理

用户按下键盘确认键或点击搜索按钮时触发搜索。

          onSubmitted: (v) => setState(() => _showResult = v.isNotEmpty),
        ),
        actions: [
          TextButton(
            onPressed: () => setState(() => _showResult = _controller.text.isNotEmpty),
            child: const Text('搜索', style: TextStyle(color: Color(0xFFE91E63))),
          ),
        ],
      ),

onSubmitted回调在用户按下键盘确认键时触发,actions区域放置搜索按钮。两种方式都会检查输入内容是否为空,不为空时切换到搜索结果视图。搜索按钮使用主题色,突出显示。

页面内容切换

根据_showResult状态决定显示搜索建议还是搜索结果。

      body: _showResult ? _buildSearchResult() : _buildSearchSuggestion(),
    );
  }

这种条件渲染的方式简洁明了。当用户还没有进行搜索时显示热门搜索和搜索历史,搜索后显示搜索结果列表。

搜索建议页面

搜索建议页面包含热门搜索和搜索历史两个部分。

  Widget _buildSearchSuggestion() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '热门搜索',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),

使用SingleChildScrollView包裹整个内容,当内容超出屏幕时可以滚动。Column采用左对齐,标题使用粗体突出显示。

热门搜索标签

热门搜索使用Wrap组件实现流式布局的标签展示。

          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: List.generate(
              12,
              (i) => GestureDetector(
                onTap: () {
                  _controller.text = '热门 ${i + 1}';
                  setState(() => _showResult = true);
                },
                child: Chip(
                  label: Text('热门 ${i + 1}'),
                  backgroundColor: const Color(0xFF1E1E1E),
                ),
              ),
            ),
          ),

Wrap组件会自动换行,spacing和runSpacing分别设置水平和垂直间距。点击标签时会将标签内容填入搜索框并触发搜索。Chip组件使用深色背景,与整体主题一致。

搜索历史区域

搜索历史使用列表形式展示,方便用户快速选择之前搜索过的内容。

          const SizedBox(height: 24),
          const Text(
            '搜索历史',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 12),
          ListView.builder(
            shrinkWrap: true,
            physics: const NeverScrollableScrollPhysics(),
            itemCount: 5,
            itemBuilder: (context, i) => ListTile(
              leading: const Icon(Icons.history, color: Colors.grey),
              title: Text('历史搜索 ${i + 1}'),
              trailing: const Icon(Icons.north_west, color: Colors.grey, size: 16),
            ),
          ),
        ],
      ),
    );
  }

ListView.builder设置shrinkWrap为true,让列表高度自适应内容。physics设置为NeverScrollableScrollPhysics,禁用列表自身的滚动,由外层SingleChildScrollView统一处理。每个历史记录前面有时钟图标,尾部的箭头图标表示点击可以填入搜索框。

搜索结果页面结构

搜索结果页面使用TabBar和TabBarView实现多类型结果切换。

  Widget _buildSearchResult() {
    return Column(
      children: [
        TabBar(
          controller: _tabController,
          isScrollable: true,
          labelColor: const Color(0xFFE91E63),
          unselectedLabelColor: Colors.grey,
          indicatorColor: const Color(0xFFE91E63),
          tabs: const [
            Tab(text: '单曲'),
            Tab(text: '歌手'),
            Tab(text: '专辑'),
            Tab(text: '歌单'),
            Tab(text: 'MV'),
          ],
        ),

TabBar设置isScrollable为true,当Tab数量较多时可以横向滚动。选中的Tab使用主题色,未选中使用灰色。五个Tab分别对应不同类型的搜索结果。

TabBarView内容区域

TabBarView包含五个不同类型的搜索结果列表。

        Expanded(
          child: TabBarView(
            controller: _tabController,
            children: [
              _buildSongList(),
              _buildArtistList(),
              _buildAlbumList(),
              _buildPlaylistList(),
              _buildMVGrid(),
            ],
          ),
        ),
      ],
    );
  }

使用Expanded让TabBarView占据剩余空间。每个Tab对应一个构建方法,分别构建不同类型的结果列表。

单曲搜索结果

单曲列表展示搜索到的歌曲。

  Widget _buildSongList() {
    return ListView.builder(
      itemCount: 20,
      itemBuilder: (context, i) => ListTile(
        leading: Text(
          '${i + 1}',
          style: const TextStyle(color: Colors.grey),
        ),
        title: Text('搜索结果 ${i + 1}'),
        subtitle: const Text('歌手名'),
        trailing: const Icon(
          Icons.play_circle_outline,
          color: Color(0xFFE91E63),
        ),
      ),
    );
  }

每首歌曲前面显示序号,中间显示歌曲名和歌手名,尾部是播放按钮。播放按钮使用主题色,点击可以直接播放歌曲。

歌手搜索结果

歌手列表使用圆形头像展示。

  Widget _buildArtistList() {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, i) => ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
          child: const Icon(Icons.person, color: Colors.white70),
        ),
        title: Text('歌手 ${i + 1}'),
        subtitle: Text('${(i + 1) * 100} 首歌曲'),
      ),
    );
  }

CircleAvatar用于显示歌手头像,背景色根据索引变化。副标题显示歌手的歌曲数量,帮助用户了解歌手的作品规模。

专辑搜索结果

专辑列表使用方形封面展示。

  Widget _buildAlbumList() {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, i) => ListTile(
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(8),
            color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
          ),
          child: const Icon(Icons.album, color: Colors.white70),
        ),
        title: Text('专辑 ${i + 1}'),
        subtitle: const Text('歌手'),
      ),
    );
  }

专辑封面使用圆角矩形,与歌手的圆形头像形成区分。Container设置固定宽高,保证封面比例一致。

歌单搜索结果

歌单列表的样式与专辑类似,但图标不同。

  Widget _buildPlaylistList() {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, i) => ListTile(
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(8),
            color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
          ),
          child: const Icon(Icons.queue_music, color: Colors.white70),
        ),
        title: Text('歌单 ${i + 1}'),
        subtitle: Text('${(i + 1) * 50} 首'),
      ),
    );
  }

歌单使用queue_music图标,副标题显示歌单包含的歌曲数量。这种统一的列表样式让用户容易理解和操作。

MV搜索结果

MV使用网格布局展示,更适合视频类内容。

  Widget _buildMVGrid() {
    return GridView.builder(
      padding: const EdgeInsets.all(16),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 1.5,
        crossAxisSpacing: 12,
        mainAxisSpacing: 12,
      ),
      itemCount: 10,
      itemBuilder: (context, i) => Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(12),
          color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
        ),
        child: const Center(
          child: Icon(Icons.play_circle_filled, size: 40, color: Colors.white70),
        ),
      ),
    );
  }

GridView使用SliverGridDelegateWithFixedCrossAxisCount设置每行2个,childAspectRatio设置为1.5,让MV封面呈现横向矩形。每个MV卡片中间显示播放图标,点击可以播放MV。

搜索防抖处理

为了避免频繁请求接口,可以添加搜索防抖功能。

  Timer? _debounceTimer;
  
  void _onSearchChanged(String value) {
    _debounceTimer?.cancel();
    _debounceTimer = Timer(const Duration(milliseconds: 500), () {
      if (value.isNotEmpty) {
        _performSearch(value);
      }
    });
  }
  
  void _performSearch(String keyword) {
    setState(() => _showResult = true);
    // 调用搜索接口
  }

使用Timer实现防抖,用户停止输入500毫秒后才执行搜索。每次输入变化时先取消之前的定时器,避免重复请求。

清空搜索框

搜索框右侧可以添加清空按钮。

  Widget _buildClearButton() {
    if (_controller.text.isEmpty) return const SizedBox.shrink();
    return IconButton(
      icon: const Icon(Icons.clear, color: Colors.grey),
      onPressed: () {
        _controller.clear();
        setState(() => _showResult = false);
      },
    );
  }

只有当搜索框有内容时才显示清空按钮。点击后清空输入内容并返回搜索建议页面。SizedBox.shrink()返回一个零尺寸的Widget,不占用任何空间。

搜索历史管理

搜索历史可以使用SharedPreferences进行本地存储。

  Future<void> _saveSearchHistory(String keyword) async {
    final prefs = await SharedPreferences.getInstance();
    List<String> history = prefs.getStringList('search_history') ?? [];
    history.remove(keyword);
    history.insert(0, keyword);
    if (history.length > 20) {
      history = history.sublist(0, 20);
    }
    await prefs.setStringList('search_history', history);
  }

新的搜索词插入到列表开头,如果已存在则先删除再插入,保证最新搜索的在最前面。限制历史记录最多20条,避免占用过多存储空间。

总结

搜索功能的实现涉及到多个Flutter组件的综合运用:TextField实现搜索输入、Wrap实现流式标签布局、TabBar和TabBarView实现多类型结果切换、ListView和GridView实现不同形式的列表展示。通过合理的状态管理和UI设计,为用户提供了流畅的搜索体验。在实际项目中,还需要对接后端搜索接口,实现真正的搜索功能。

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐