Android Kotlin:集成AutofillService并让自动填充建议显示于键盘上方
Android Autofill:将填充建议移至键盘上方垂直栏(Android 11+)
问题
我正在学习Android平台的Autofill机制,已开发一款存储登录凭据并提供自动填充服务的应用,目前自动填充建议会显示在输入框旁的弹窗中,点击后可填充账号密码。但我希望将该建议改为显示在键盘上方的垂直栏(该特性自Android 11起可用)。查阅官方文档未找到解决方案,现咨询:该功能应在继承AutofillService的类中实现,还是需要新建继承InputMethodService(IME)的类?
测试设备使用Gboard作为输入法。现有代码如下:
class CustomAutoFillServiceIME : AutofillService() { @SuppressLint("Recycle", "RemoteViewLayout") override fun onFillRequest( request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback ) { try{ val structure = request.fillContexts[request.fillContexts.size - 1].structure val emailFields: MutableList<ViewNode?> = ArrayList() val passFields: MutableList<ViewNode?> = ArrayList() val appName = structure.activityComponent.packageName identifyEmailFields(structure.getWindowNodeAt(0).rootViewNode, emailFields) identifyPassFields(structure.getWindowNodeAt(0).rootViewNode, passFields) val credentialsList = listOf( AppCredentials("com.example.abc", "x@x.com", "xx","ABC test"), AppCredentials("com.example.filltests", "login", "password","Filltests sample"), AppCredentials("com.windyty.android", "Adam xTest", "xxx","Windy Adam"), AppCredentials("pl.mapa_turystyczna.app", "LoginTest", "PassTest","Mapa Turystyczna sample"), AppCredentials("com.kfc_polska", "LoginTestKFC", "PassTestKFC","KFC sample"), AppCredentials("com.example.aaa", "login", "password","AAA sample") ) val datasets = mutableListOf<Dataset>() for (credential in credentialsList.filter { it.appName == appName }) { // Make sure you have found both email and password fields if (emailFields.isNotEmpty() && passFields.isNotEmpty()) { val emailField = emailFields[0] val passField = passFields[0] val remoteView = RemoteViews(packageName, R.layout.autofill_suggestion) remoteView.setTextViewText(R.id.suggestion_item, credential.name) val dataSetBuilder = Dataset.Builder(remoteView) emailField?.autofillId?.let { autofillId -> dataSetBuilder.setValue(autofillId, AutofillValue.forText(credential.login)) } passField?.autofillId?.let { autofillId -> dataSetBuilder.setValue(autofillId, AutofillValue.forText(credential.password)) } datasets.add(dataSetBuilder.build()) } } val responseBuilder = FillResponse.Builder() datasets.forEach { dataset -> responseBuilder.addDataset(dataset) } callback.onSuccess(responseBuilder.build()) } catch (e: Exception) { Log.e("XD", "Error during onFillRequest", e) callback.onFailure(e.localizedMessage) } } private fun identifyEmailFields( node: ViewNode, emailFields: MutableList<ViewNode?> ) { //check by autofillHints if exists: if (node.autofillHints != null) { Log.e("XD", node.autofillHints.toString()) if (node.autofillHints!!.contains("email") || node.autofillHints!!.contains("username") || node.autofillHints!!.contains("login") || node.autofillHints!!.contains("user") || node.autofillHints!!.contains("usr") || node.autofillHints!!.contains("user name") || node.autofillHints!!.contains("identifier") || node.autofillHints!!.contains("id") ) { Log.e("XD", "identifyEmailField detected autofillhints") emailFields.add(node) return } } //check by id, if autofillHints don't exist: if (node.className != null && node.className!!.contains("EditText")) { Log.e("XD", "identifyEmailField didn't detect autofillhints") val viewId = node.idEntry if (viewId != null && (viewId.contains("email") || viewId.contains("username") || viewId.contains("login") || viewId.contains("user") || viewId.contains("usr") || viewId.contains("user name") || viewId.contains("identifier") || viewId.contains("id")) ) { emailFields.add(node) return } } for (i in 0 until node.childCount) { identifyEmailFields(node.getChildAt(i), emailFields) } } private fun identifyPassFields( node: ViewNode, passFields: MutableList<ViewNode?> ) { if (node.autofillHints != null) { Log.e("XD", node.autofillHints.toString()) if (node.autofillHints!!.contains("password") || node.autofillHints!!.contains("pass") ) { passFields.add(node) return } } if (node.className != null && node.className!!.contains("EditText")) { val viewId = node.idEntry if (viewId != null && (viewId.contains("password") || viewId.contains("pass")) ) { passFields.add(node) return } } for (i in 0 until node.childCount) { identifyPassFields(node.getChildAt(i), passFields) } } override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { } }
解答
无需新建InputMethodService子类,所有逻辑都可以在你现有的AutofillService实现中完成。Android 11+的键盘上方垂直栏(IME自动填充建议栏)是Autofill框架与输入法协作的产物,只需在构建填充数据集时添加特定标记即可触发该展示方式。
核心修改
在构建Dataset时,调用setPresentationStyle方法指定Dataset.PRESENTATION_STYLE_IME,该标记会告诉系统将填充建议展示在输入法的顶部建议栏中。同时建议添加版本判断,避免低版本系统报错。
修改后的数据集构建代码片段:
val dataSetBuilder = Dataset.Builder(remoteView) // 关键:指定在IME建议栏展示 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { dataSetBuilder.setPresentationStyle(Dataset.PRESENTATION_STYLE_IME) } emailField?.autofillId?.let { autofillId -> dataSetBuilder.setValue(autofillId, AutofillValue.forText(credential.login)) } passField?.autofillId?.let { autofillId -> dataSetBuilder.setValue(autofillId, AutofillValue.forText(credential.password)) } datasets.add(dataSetBuilder.build())
注意事项
- 该特性仅在Android 11(API 30)及以上版本生效,低版本会忽略此标记,仍显示输入框旁的弹窗。
- 并非所有第三方输入法都支持该特性,Gboard在Android 11+环境下完全兼容。
- 如果需要同时保留输入框弹窗和IME栏建议,可移除
setPresentationStyle调用,系统会根据输入法能力自动适配展示方式;若强制仅在IME栏显示,则保留该标记。
内容的提问来源于stack exchange,提问作者Adam
相关产品推荐
相关产品推荐

