7、搜索功能实现:SearchFragment集成、语音搜索与搜索建议

搜索功能,说白了就是用户在大屏上找内容的入口。TV端的搜索和手机端差别很大——用户没有触摸屏,只能靠遥控器上下左右移动焦点。我刚开始做TV搜索时,直接照搬了手机端的搜索框,结果用户反馈“太难用了”。嗯,今天我们就来聊聊,怎么在Android TV上做一个真正好用的搜索功能。

7.1 SearchFragment:Google给你的现成方案

Android TV官方其实提供了一个搜索组件——SearchFragment。我个人习惯直接用它,而不是自己从头写一个。为什么呢?因为它已经处理好了焦点移动、软键盘弹出、搜索历史这些麻烦事。

集成方式很简单,在你的布局里放一个SearchView

<androidx.leanback.widget.SearchView
    android:id="@+id/search_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:searchBackgroundColor="@color/search_bg"
    app:searchHintText="搜索电影或电视剧"
    app:searchIconColor="@color/white" />

然后在Fragment中这样使用:

public class MySearchFragment extends SearchFragment
        implements SearchFragment.SearchResultProvider {

    private ArrayObjectAdapter mRowsAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
        setSearchResultProvider(this);
    }

    @Override
    public boolean onQueryTextChange(String newQuery) {
        // 用户每输入一个字符都会触发这里
        loadResults(newQuery);
        return true;
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        // 用户按下确认键提交搜索
        loadResults(query);
        return true;
    }

    private void loadResults(String query) {
        // 这里调用你的搜索API或本地数据库
        // 把结果塞进mRowsAdapter
    }
}

小提示:onQueryTextChange会在每次按键时触发,如果你的搜索是网络请求,记得做防抖处理。我一般用RxJava的debounce操作符,或者简单点用Handler延迟300ms。

7.2 语音搜索:大屏上最自然的交互方式

你想想看,用遥控器在虚拟键盘上一个字母一个字母地选,多痛苦。语音搜索才是TV端的王道。Android TV系统级就支持语音输入,你只需要做一件事——集成SpeechRecognitionCallback

具体做法:

public class MySearchFragment extends SearchFragment {

    private SpeechRecognitionCallback mSpeechRecognitionCallback;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mSpeechRecognitionCallback = new SpeechRecognitionCallback() {
            @Override
            public void recognizeSpeech() {
                // 启动系统语音识别
                Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                        RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "说出你要搜索的内容");
                startActivityForResult(intent, REQUEST_SPEECH);
            }
        };
        setSpeechRecognitionCallback(mSpeechRecognitionCallback);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_SPEECH && resultCode == Activity.RESULT_OK) {
            ArrayList<String> results =
                    data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            if (results != null && results.size() > 0) {
                String spokenText = results.get(0);
                // 把语音识别结果设置到搜索框
                setSearchQuery(spokenText, true);
            }
        }
    }
}

注意:语音识别需要网络连接。我曾经遇到过用户离线状态下点击语音按钮,应用直接崩溃的情况。记得检查网络状态,并给出友好提示。

7.3 搜索建议:让用户少按几次遥控器

搜索建议,说白了就是用户还没输完,你就猜他想搜什么。TV端尤其需要这个功能——少按一次遥控器,体验就好一分。

实现搜索建议需要两步:

  1. 提供建议数据源
  2. 在界面上展示建议列表

SearchFragment自带了一个建议展示机制,你只需要实现SearchFragment.SearchResultProvider接口:

@Override
public boolean onQueryTextChange(String newQuery) {
    if (newQuery.length() > 2) {  // 至少输入3个字符才开始建议
        List<String> suggestions = getSuggestionsFromLocal(newQuery);
        // 把建议展示在搜索结果区域
        mRowsAdapter.clear();
        for (String suggestion : suggestions) {
            mRowsAdapter.add(new SuggestionItem(suggestion));
        }
    }
    return true;
}

建议的来源可以是:

  • 本地缓存的热门搜索词
  • 用户的历史搜索记录
  • 服务端返回的联想词

我个人习惯把热门搜索词存在本地SQLite里,这样即使断网也能给出建议。用户选择建议后,直接触发搜索,省去输入完整关键词的麻烦。

7.4 搜索结果的展示与交互

搜索结果展示,我推荐用RowsFragment或者VerticalGridFragment。为什么?因为TV端内容展示的核心就是“行”和“网格”。

举个例子,搜索结果可以按类型分几行:

  • 第一行:电影结果
  • 第二行:电视剧结果
  • 第三行:演员结果

每一行内部用HorizontalGridView横向滚动,用户左右选择,上下切换行。这种交互模式在TV上非常成熟,用户一看就懂。

// 创建搜索结果的行
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());

// 电影行
ArrayObjectAdapter movieAdapter = new ArrayObjectAdapter(new CardPresenter());
movieAdapter.add(new Movie("流浪地球"));
movieAdapter.add(new Movie("哪吒"));
rowsAdapter.add(new ListRow(new HeaderItem("电影"), movieAdapter));

// 电视剧行
ArrayObjectAdapter tvAdapter = new ArrayObjectAdapter(new CardPresenter());
tvAdapter.add(new TVShow("三体"));
tvAdapter.add(new TVShow("狂飙"));
rowsAdapter.add(new ListRow(new HeaderItem("电视剧"), tvAdapter));

核心要点:搜索结果为空时,不要直接显示空白页面。我一般会展示一个“没有找到相关内容”的提示,同时推荐一些热门内容。这样用户不会感到失落,还能继续浏览。

7.5 知识体系总览

下面这张图总结了搜索功能的整体架构,从用户输入到结果展示的完整流程:

TV搜索功能架构图 用户输入 遥控器 / 语音 SearchFragment 查询处理 / 防抖 数据源 本地 / 网络 搜索建议 结果展示 语音搜索通过 SpeechRecognitionCallback 集成,搜索建议在 onQueryTextChange 中实时提供

7.6 避坑指南

做TV搜索,有几个坑我踩过,分享给你:

  • 焦点丢失问题:搜索结果更新时,如果当前焦点所在的item被移除了,焦点会飞到奇怪的地方。我一般会在更新数据前,先记录当前焦点的位置,更新后手动恢复。
  • 软键盘适配:不同品牌的TV,软键盘样式不一样。有些遥控器没有“完成”键,用户不知道怎么提交搜索。我建议在搜索框旁边放一个“搜索”按钮,作为备选方案。
  • 语音权限:Android 6.0以上需要动态申请录音权限。我曾经忘记处理权限拒绝的情况,结果语音按钮点了没反应,用户以为坏了。

小技巧:搜索历史可以存在SharedPreferences里,用Gson转成JSON数组。每次搜索时,把新词插到最前面,最多保留20条。这样实现简单,而且不需要引入数据库。

好了,搜索功能的核心实现就这些。说白了就是三件事:集成SearchFragment、接入语音识别、提供搜索建议。把这三件事做好,你的TV搜索体验就不会差。


公众号:蓝海资料掘金营,微信deep3321