You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 18:33:10