- 使用無障礙服務
無障礙服務即AccessibilityService,是一種,可提供界面增強功能,來協助殘障使用者或可能暫時無法與裝置進行全面互動的使用者完成操作的應用。
Google是這樣描述的:
無障礙服務是一種應用,可提供界面增強功能,來協助殘障使用者或可能暫時無法與裝置進行全面互動的使用者完成操作。
從 Android 1.6(API 級别 4)開始,您就可以建構和部署無障礙服務,并且這些服務在 Android 4.0(API 級别 14)中得到了顯著改進。Android 支援庫也随着 Android 4.0 的釋出得到更新,為這些增強的無障礙功能(自 Android 1.6 起)提供支援。如果開發者的目标是打造廣泛相容的無障礙服務,建議他們使用該支援庫,并讓開發的應用支援 Android 4.0 中引入的更進階的無障礙功能。
class ScannerService : AccessibilityService() {
override fun onAccessibilityEvent(p0: AccessibilityEvent) {
}
override fun onInterrupt() {
}
override fun onKeyEvent(event: KeyEvent): Boolean {
LogUtils.d(event)
if (Scanner.dispatchKeyEvent(event)) {
return true
}
return super.onKeyEvent(event)
}
}
AndroidManifest.xml檔案中注冊ScannerService
<service
android:name=".ScannerService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/keyservice_config" />
</service>
keyservice_config.xml這個檔案用來聲明無障礙服務,最終最後系統設定頁面的【無障礙】設定頁面中看到相關的開關。
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRequestFilterKeyEvents"
android:canRetrieveWindowContent="true"
android:notificationTimeout="100"
android:canRequestFilterKeyEvents="true"
android:description="@string/accessibility_description"
android:packageNames="cn.xxx.demo" />
- KeyEvent處理
這裡我通過Scanner工具類來處理捕獲的KeyEvent,并且在識别到KeyEvent.ACTION_DOWN按鍵之後将識别内容抛給注冊進來的回調函數。最終由回調函數得到了識别到的掃描内容。
另外這裡KeyEvent到掃描内容的轉換,使用了TextKeyListener。避免了,手動的字元轉換,也盡量避免一些相容性的問題(例如回車換行的識别等)
參考實作如下(主要代碼):
object Scanner : IScanner {
//掃描結果回調
private val callbacks = Stack<IScanCallback>()
private var isStart = false
//整體掃描結果
private var mScanResultList: ArrayList<String> = ArrayList()
private var handler: Handler? = null
private val tkl: KeyListener =
TextKeyListener.getInstance(false, TextKeyListener.Capitalize.NONE)
private val et = Editable.Factory.getInstance().newEditable("")
private val key = View.OnKeyListener { v, keyCode, event ->
var returnResult = false
if (event.action == KeyEvent.ACTION_DOWN) {
val callback = callbacks.peek()
if (mScanResultList.size == 0 && callback != null && !isStart) {
isStart = true
et.clear()
callback.scanStart()
}
returnResult = tkl.onKeyDown(null, et, keyCode, event)
} else if (event.action == KeyEvent.ACTION_UP) {
LogUtils.d(
"Scanner onKeyUp keyCode=${keyCode} UnicodeChar= ${event.unicodeChar}"
)
val isEnter = event.keyCode == KeyEvent.KEYCODE_ENTER
if (isEnter) {
LogUtils.d("Scanner newapi:$et")
val mScanResult = et.toString()
et.clear()
mScanResultList.add(mScanResult)
handler?.let {
it.removeCallbacks(resultRunnable)
it.postDelayed(resultRunnable, 300)
}
} else {
handler?.let {
it.removeCallbacks(resultRunnable)
}
}
returnResult = tkl.onKeyUp(null, et, keyCode, event)
} else {
returnResult = tkl.onKeyOther(
null,
et,
event
) //NOTE: My devices never used KeyEvent.ACTION_MULTIPLE so I don't know if it should get fired here or with the key down event.
}
returnResult
}
override fun initScanner(mContext: Context) {
LogUtils.d("initScanner")
if (handler == null) {
handler = Handler()
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return key.onKey(null, event.keyCode, event)
}
override fun registerScanCallback(callback: IScanCallback) {
callbacks.add(callback)
}
override fun unregisterScanCallback(callback: IScanCallback) {
callbacks.remove(callback)
}
override fun destroyScanner() {
handler?.let {
it.removeCallbacks(resultRunnable)
}
mScanResultList?.clear()
callbacks.clear()
}
private val resultRunnable = Runnable {
val callback = callbacks.peek()
if (callback != null) {
val result: List<String> = ArrayList(mScanResultList)
LogUtils.d("Scan result=${GsonUtils.toJson(result)}")
if (result.isNotEmpty()) {
callback.scanSuccess(result[0])
}
callback.scanSuccess(result)
mScanResultList.clear()
isStart = false
}
}
}
- 掃碼回調處理
通過監聽頁面的生命周期,自動的将實作過IScanCallback的頁面注冊到Scanner工具類中,并且在頁面銷毀的時候自助登出監聽,進而實作了監聽掃描結果的自動管理。
參考實作如下(主要代碼):
class AppLifecycleHandler : Application.ActivityLifecycleCallbacks, ComponentCallbacks2 {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
val isAccessibilitySettingsOn = isAccessibilitySettingsOn(activity.applicationContext)
LogUtils.d("AppLifecycleHandler isAccessibilitySettingsOn=$isAccessibilitySettingsOn")
if (!isAccessibilitySettingsOn) {
Toast.makeText(
activity.applicationContext,
"請在系統【設定】中【無障礙】頁面開啟的demo【識别掃碼輸入】開關",
Toast.LENGTH_LONG
).show()
openAccessibilitySetting(activity.applicationContext)
}
if (activity is IScanCallback) {
LogUtils.d("AppLifecycleHandler onActivityCreated 掃描監聽注冊 $activity")
registerScanCallback((activity as IScanCallback))
}
}
override fun onActivityDestroyed(activity: Activity) {
if (activity is IScanCallback) {
LogUtils.d("AppLifecycleHandler onActivityDestroyed 登出掃描監聽注冊 $activity")
unregisterScanCallback((activity as IScanCallback))
}
}
}
- 總結
通過android.accessibilityservice.AccessibilityService的onKeyEvent捕獲掃碼輸入,将KeyEvent傳遞給Scanner處理,Scanner将KeyEvent傳遞給android.text.method.TextKeyListener,捕獲回車按鍵(或者其他自定義按鍵的時候)将識别結果抛到回調中,由掃碼回調處理掃碼結果。