将语音搜索集成到Google Now中
原文标题:Use Voice Search to integrate with Google Now
原文链接:http://antonioleiva.com/voice_search_google_now/
原文作者:Antonio Leiva(http://antonioleiva.com/about/)
原文发布:2015-10-14
Android最棒的能力之一就是可以将我们的APP以不同的方式集成到它的生态链中。APP相互之间可以进行“交流”,这给予我们极大的灵活性来创建独特的应用体验。
集成Google应用就是一个典型的例子。有很多不同的特性可以帮助我们提升APP的知名度,如APP索引或一组强大的语音功能。
语音搜索
尽管,在APP中实现语音搜索的过程与任何其他语音功能类似,但是,在这篇文章中,我还是会聚焦在怎样在APP中实现语音搜索功能。你也可以在Play Music中尝试这个例子:
- 好,Google在Play Music中,搜索Beatles
这句指令将在Play Music APP中开启搜索Beatles。
怎样实现语音搜索
当语音搜索启动后,我们的APP将收到一个查询文字的Intent,我们必须捕捉和分析这段文字。所以,这第一部分是指定哪个Activity接收这条Intent:
1 <activity android:name=".MainActivity" android:launchMode="singleTask" > 2 <intent-filter> 3 <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/> 4 <category android:name="android.intent.category.DEFAULT"/> 5 </intent-filter> 6 </activity>
这个动作(action)称为com.google.android.gms.actions.SEARCH_ACTION,所以我们说MainActivity是处理这类Intent。另外,我用singleTask启动模式,这样MainActivity就仅仅创建一次。否则,每次收到这条Intent就要创建一个新的Activity实例。
接下来的一步是要在MainActivity内进行处理它。当我们用singleTask模式时,在Activity的两个不同位置上可以接收Intent:首先是用getIntent()创建Activity,接着是onNewIntent方法中。这样就要创建一个处理方法在需要它的时候调用它:
1 private static final String ACTION_VOICE_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION"; 2 ... 3 private void handleVoiceSearch(Intent intent) { 4 if (intent != null && ACTION_VOICE_SEARCH.equals(intent.getAction())) { 5 String query = intent.getStringExtra(SearchManager.QUERY); 6 setSearchViewVisible(true); 7 searchView.setQuery(query, true); 8 } 9 }
这个方法检查Intent是否为空(null)或者是否是在收到查询文字前要检测的动作,它是Intent内部的额外动作。查询的额外动作关键字是SearchManager.QUERY。
在searchView后,设置查询,提交执行查询。其方法是onNewIntent:
1 @Override protected void onNewIntent(Intent intent) { 2 super.onNewIntent(intent); 3 handleVoiceSearch(intent); 4 }
UI也准备好(在我们的例子中,当菜单弹出时,我们访问SearchView),你稍后将看到。
在我的例子中,UI是基于工具栏(Toolbar)内部的SearchView。你可以在前面的文章中看到怎样实现SearchView,不过我还是稍作解释怎样做。首先,产生菜单动作(menu action):
1 <menu xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:app="http://schemas.android.com/apk/res-auto"> 3 <item 4 android:id="@+id/action_search" 5 android:title="@string/action_search" 6 android:icon="@drawable/ic_search" 7 app:actionViewClass="android.support.v7.widget.SearchView" 8 app:showAsAction="ifRoom" /> 9 </menu>
然后,在菜单弹出时,你请求SearchView:
1 @Override public boolean onCreateOptionsMenu(Menu menu) { 2 getMenuInflater().inflate(R.menu.main, menu); 3 4 MenuItem searchItem = menu.findItem(R.id.action_search); 5 searchView = (SearchView) MenuItemCompat.getActionView(searchItem); 6 7 searchView.setOnSearchClickListener(new View.OnClickListener() { 8 @Override public void onClick(View v) { 9 setSearchViewVisible(true); 10 } 11 }); 12 13 searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 14 @Override public boolean onQueryTextSubmit(String query) { 15 Toast.makeText(MainActivity.this, query, Toast.LENGTH_LONG).show(); 16 searchView.clearFocus(); 17 return true; 18 } 19 20 @Override public boolean onQueryTextChange(String newText) { 21 return false; 22 } 23 }); 24 25 handleVoiceSearch(getIntent()); 26 27 return true; 28 } 29 30 private void setSearchViewVisible(boolean visible) { 31 32 if (searchView.isIconified() == visible) { 33 searchView.setIconified(!visible); 34 } 35 36 if (getSupportActionBar() != null) { 37 getSupportActionBar().setDisplayHomeAsUpEnabled(visible); 38 } 39 }
怎样尝试
由于APP需要发布到Play商店中,Google Now才能检测到APP,所以我们还不能直接从Google Now尝试这个例子。但是,我们可以用ADB来调试它。这条命令是:
- adb shell am start -a com.google.android.gms.actions.SEARCH_ACTION -e query searchquery app_package
对于从我的代码库下载的例子,就可以这样做:
- adb shell am start -a com.google.android.gms.actions.SEARCH_ACTION -e query VoiceSearch com.antonioleiva.googlenowsearch
在APP完全关闭和启动时,都可以尝试本例了。这样,就可以测试两种可能的途径了。
附加说明:Kotlin语言的实现
你可能知道,因为我认为Kotlin语言可以非常好替代Java语言,Kotlin可以使我们的代码更简洁、可读性更好,所以这些天讨论了许多Kotlin语言的特性。作为例子,我将简化onCreateOptionsMenu,你可以在同一个代码库中找到完整的代码。
实现扩展函数的能力可以帮助我们减少冗长代码。例如,可以为Menu创建一个扩展函数,基于action id找到ActionView,返回层级视图:
1 inline fun <reified T : View?> Menu.findCompatActionView(actionRes: Int): T { 2 val searchItem = findItem(actionRes) 3 return MenuItemCompat.getActionView(searchItem) as T 4 }
现在可以这样做:
1 searchView = menu.findCompatActionView(R.id.action_search)
另一个扩展函数可以以清晰的方式帮助我们编写OueryTextListener:
1 fun SearchView.onQueryText(submit: (String) -> Boolean = { false }, textChange: (String) -> Boolean = { false }) { 2 3 this.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 4 5 override fun onQueryTextSubmit(query: String): Boolean = submit(query) 6 7 override fun onQueryTextChange(newText: String): Boolean = textChange(newText) 8 9 }) 10 }
这个函数接收一对函数,一个用于侦听器中的每个方法,并给出它们的默认值。这样我们仅需要定义我们要用的。如果我们仅仅要第一个函数(对于第二个我们用默认的),现在我就可以这样做:
1 searchView.onQueryText ({ 2 longToast(it) 3 searchView.clearFocus() 4 true 5 })
最终,这个函数就是这样的:
1 override fun onCreateOptionsMenu(menu: Menu): Boolean { 2 menuInflater.inflate(R.menu.main, menu) 3 4 searchView = menu.findCompatActionView(R.id.action_search) 5 searchView.setOnSearchClickListener { setSearchViewVisible(true) } 6 7 searchView.onQueryText ({ 8 longToast(it) 9 searchView.clearFocus() 10 true 11 }) 12 13 intent?.let { handleVoiceSearch(it) } 14 15 return true 16 }
如你所见,如果在这个位置上(例如:在一个常规activity创建上),Intent可以为null,就必须在使用它之前检查它是否为null。用let函数,可以避免if条件的创建,在对象调用时不为null,就可只进入对象内部。
如果你对Kotlin有兴趣,可以搜索Kotlin文章,或购买我编写的《Android开发者的Kotlin》一书。