當前位置:首頁 » 安卓系統 » android輸入事件

android輸入事件

發布時間: 2023-01-09 05:05:00

❶ 深入理解android2-WMS,控制項

title: '深入理解android2-WMS,控制項-圖床版'
date: 2020-03-08 16:22:42
tags:
typora-root-url: ./深入理解android2-WMS-控制項
typora--images-to: ./深入理解android2-WMS-控制項

WMS主要負責兩個功能, 一是負責窗口的管理,如窗口的增加刪除,層級.二是負責全局事件的派發.如觸摸點擊事件.

先簡單介紹幾個重要的類

IWindowSession. 進程唯一的.是一個匿名binder.通過他向WMS請求窗口操作

surface 繪畫時,canvas會把內容繪制到surface里.surface是有surfaceFlinger提供給客戶端的.

WindowManager.LayoutParams 集成自ViewGroup.LayoutParams.用來指明client端的窗口的一些屬性.最重要的是type. 根據這屬性來對多個窗口進程ZOrder的排序.

windowToken.向WMS添加的窗口令牌.每個窗口都要有一個令牌.

IWindow. 是client提供給WMS的.繼承自binder.WMS通過IWindow對象來主動發起client端的事件.

窗口的本周就是進行繪制所使用的surface,客戶端向WMS添加窗口的過程,就是WMS為客戶端分配surface的過程.

ui框架層就是使用surface上繪制ui元素.及響應輸入事件

WMS負責surface的分配.窗口的層級順序

surfaceFlinger負責將多個Surface混合並輸出.

WMS有SystemServer 進程啟動.他和AMS其實是運行於一個進程中的.只是分別有各自的線程.

上邊傳入了兩個handler.這里就使用windowManager的handler來創建WMS.也就是在一個handerThread線程中創建

用來管理每個窗口的事件輸入.也就是把輸入事件轉發到正確的窗口

能獲取顯示系統的同步信號.用來驅動動畫的渲染

所有窗口動畫的總管,在mChoreographer的驅動下渲染所有動畫

只有PhoneWindowManager一個實現.定義了很多窗口相關的策略.是最重要的成員,比如負責窗口的zorder順序.

zorder就是各個窗口在z軸的值.越大越在屏幕上層.窗口就是根據zorder值一層一層堆在一起.

可以繪制的屏幕列表.默認是只有1個.

管理所以窗口的顯示令牌token,每個窗口都要屬於一個token.這里的IBinder 是

表示所有Activity的token. AppWindowToken是WindowToken的子類,這個list的順序和AMS中對mHistory列表中activity的順序是一樣的 .反應了系統中activity的疊加順序.也就是說.所有窗口都有WindowToken.而Activity對應的窗口則多了AppWindowToken.

每個窗口都對應一個WindowState.存儲改窗口的狀態信息(這就和AMS中對每個activity抽象成ActivityRecord一樣)

這里的iBinder 是IWIndow類.

Session 是WMS提供給客戶端來與WMS進行交互的,這是匿名binder.為了減輕WMS的負擔.客戶端通過IWindowManager.openSession 拿到他的代理.然後通過代理與WMS交互.每個進程唯一.

客戶端通過IWindowSession.add 來添加窗口. iWindowSession 是同aidl形成的.最終到了WMS.addWindow.

這里總的來說就是確立了客戶窗口的WindowToken.WindowState.和DisplayContent. 並都保存了起來.同時根據layoutparams.type進行了些窗口等級的判斷.

WindowToken將同一個應用組件的窗口安排在一起.一個應用組件可以是Activity,InputMethod.

WindowToken使應用組件在變更窗口時必須與自己的WindowToken匹配.

這里主要是為了處理窗口的層級關系而設立的.

只要是一個binder對象.都可以作為token向wms聲明.wms會把這個binder對應起一個WindowToken.其實就是把客戶端的binder和wms里的一個WindowToken對象進行了綁定.

因為Activity比較復雜,因此WMS為Activity實現了WindowToken的子類 appwindowtoken.同時在AMS啟動Activity的ActivityStack.startActivityLocked里聲明token.

然後在activityStack.realStartActivityLocked里在發給用戶進程,然後用戶在通過這個binder和WMS交互時帶過來.

activity則在 activityStack 線程的handleResumeActivity 里把Activity 對應的窗口,加入到wMS中

取消token 也是在AMS中 ,也就是說, AMS負責avtivity的token向WMS的添加和刪除.

當然.Activity的 r.appToken 是 IApplicationToken.Stub ,他里邊有一系列的窗口相關的通知回調.

這里總結下. AMS在創建Activity的ActivityRecord時,創建了他的appToken,有把appToken傳送給WMS.WMS對應匹配為APPWindowToken,最後還把這個appToken發送給activity.因此AMS就通過ActivityRecord就可有直接操作WMS對該窗口的繪制.如圖.

每個window在WMS里都抽象成了WindowState.他包含一個窗口的所有屬性.WindowState在客戶端對應的則是iWidow.stub類.iWidow.stub有很多窗口通知的回調.

WindowState被保存在mWindowMap里.這是整個系統所有窗口的一個全集.

HashMap<IBinder, WindowToken> mTokenMap .這里是 IApplicationToken(客戶端)和WindowToken的映射

HashMap<IBinder, WindowState> mWindowMap 這里是IWidow(客戶端)和WindowState的映射,並且WMS通過這個IWindow 來回調客戶端的方法.

上圖可以看出.每個activity 只有一個ActivityRecord.也只有一個AppToken,也就只有一個WindowToken.而一個acitvity可能有多個窗口.每個窗口對應一個WindowState.

WindowToken用來和AMS交換. 而WindowState對應的iWindow則是WMS來與客戶端交互的.

窗口顯示次序就是窗口在Z軸的排了.因為窗口是疊加在一起的.因此就需要知道哪些顯示在上邊,哪些在下邊.這個由WindowState構造時確定

可見.分配規則是由WindowManagerPolicy mPolicy來決定的.產生 mBaseLayer和mSubLayer. mBaseLayer決定該窗口和他的子窗口在所有窗口的顯示位置. mSubLayer決定子窗口在同級的兄弟窗口的顯示位置.值越高.顯示約靠上.

WindowState 產生了他自己這個窗口的layer值後.在添加窗口的時候就會把所有窗口按layer排序插入mWindows列表中,在通過 adjustWallpaperWindowsLocked();進行層級調整.

當客戶端通過IWindowsession.add後,客戶端還沒有獲得Surface.只有在執行IWindowsession.relayout後.客戶端才獲得了一塊Surface. IWindowsession.relayout根據客戶端提供的參數,為客戶端提供surface.具體實現是WMS.relayoutWindow

總的來說就是根據用戶傳入的參數,更新WindowState.然後遍歷所有窗口布局.在設置合適的Surface尺寸,在返回給用戶端

會循環調用6次.里邊的邏輯大概如下

這里主要下,因為之前加了鎖.requestTraversalLocked他又會重復執行();因此會重復循環執行布局.

布局這部分就記個原理吧

布局完成後.客戶端的尺寸和surface都得到了.就可以繪制 了.WMS會通知客戶端布局發送變化

總結,WMS 負責管理所有的窗口.包括系統窗口和APP窗口,而窗口必須有一個WindowToken所為標識符.同時WMS為每個窗口創建一個WindowState類,這是窗口在服務端的抽象.WindowState則綁定了一個客戶端的IWindow類,WMS通過這個IWindow 向APP發送消息.

AMS在啟動Activity的時候.把ActivityRecord.token 通過wms.addtoken 注冊到WMS.又把這個token發送到APP端.因此三方可以通過這個token正確找到對應的數據.

WMS負責給所以窗口按ZOrder排序,確定窗口的尺寸,提供繪畫用的surface.

Activity的窗口是先wms.addtoken 建立windowToken關系 . wms.addWindow. 添加串口, WMS.relayout獲取surface. 完成 .

一個windowToken對應一個Activity. 但是可能對應多個windowSatate.也就是對應多個窗口.

是view樹的根實現類是viewRootImpl.但是他不是view.他是用來和WMS進行交流的管理者.viewRootImpl內部有個IWindowSession,是WMS提供的匿名binder,同時還有個iWindow子類,用來讓WMS給viewr發消息. view通過ViewRoot向WMS發消息.WMS在通過IWIndow 向APP發消息. 每個View樹只有一個ViewRoot,每個Activity也只有一個ViewRoot. UI繪制,事件傳遞.都是通過ViewRoot.

.實現類是PhoneWindow . Activity和View的溝通就是通過Window.Activity實現window的各種回調.一個Activity也對應一個PhoneWindow.也對應一個View樹.

Docerview 就是View樹的根.這是一個View. 他由PhoneWindow管理. 下文的WindowManager也由phoneWindow管理.

他還管理window的屬性 WindowManager.layoutparams.

他是一個代理類.他集成自ViewManager.他的實現是WindowManagerImpl.這是每個Activity都有一個.但是他只是把工作委託給了 WindowManagerGlobal來實現. 他負責添加刪除窗口,更新窗口.並控制窗口的補件屬性WindowManager.Layoutparams.

是進程唯一的.負責這個進程的窗口管理.他里邊有三個集合.保存這個進程所有窗口的數據.這里的每個數據根據index得到的是同一個Activity屬性.所有的WindowManager的操作都轉到他這里來.

private final ArrayList<View> mViews 每個view是個跟節點

private final ArrayList<ViewRootImpl> mRoots view對應的viewRoot

private final ArrayList<WindowManager.LayoutParams> mParams 窗口的layoutparams屬性.每個窗口一個

對於一個acitivity對象永遠對應一個PhoneWindow,一個WindowManagerImpl,一個WMS端的APPWindowToken,一個AMS里的ActivityRecord(但是如果一個activity在棧里有多個對象,就有多個ActivityRecord和AppWindowToken),acitvity 的默認窗口的view樹是DocerView.

一個窗口 對應一個ViewRoot,一個View樹.一個WindowManager.LayoutParams,一IWindow(WMS回調app).一個WSM端的WindowSatate.

但是一個Activity可以有多個窗口,因此對應WMS里可能有多個WindowSatate.這些WindowState都對應一個AppWindowToken.

一個Activity可能被載入多次.因此在AMS中可能有多個ActivityRecord對應這個activit的多個對象.

但是一個進程則對應一個WindowManagerGlobal.一個ActivityThread(主線程).一個ApplicationThread(AMS調用app).一個iWindowSession(viewroot向WMS發消息)

這里的區別就是 app與AMS 的交互是以進程之間進行通信.而App與WMS的交互.則是以窗口作為通信基礎.

當Activity由AMS啟動時.ActivityThread 通過handleResumeActivity執行resume相關的操作.這個函數首先是執行activity.resume, 此時activity 對應的view樹已經建立完成(oncreate中建立,PhoneWindow也創建了).需要把activity的窗口添加到WMS中去管理.

這里的wm是WindowManager.是每個activity一個.他內部會調用WindowManagerGlobal.addView

WindowManagerGlobal.addView

這里會為窗口創建ViewRootImpl. 並把view.ViewRootImpl.WindowMa.LayoutParams都保存在WindowManagerGlobal中, 並通過ViewRootImpl向WMS添加窗口

如果這個窗口是子窗口(wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW)

就把子窗口的token設為父窗口的token否則就是所屬activity的token.

在來個圖

在這里我們看到.我們通過mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 拿到的並不是遠程的WMS.而是本地的WindowManagerImpl. 他又把請求轉發給WindowManagerGlobal ,而WindowManagerGlobal作為進程單實例.又是吧請求轉給對應窗口的ViewRootImpl.ViewRootImpl通過WMS的IWindowSession 把數據發給WMS.

ViewRootImpl用來溝通View和WMS.並接受WMS的消息.這是雙向的binder通信.作為整個空間樹的根部,控制項的測量,布局,繪制,輸入時間的派發都由ViewRootImpl來觸發.

ViewRootImpl由WindowManagerGlobal創建,是在activityThread.handleResumeActivity時,先執行activity.resume.在調用wm.addView. 就會執行WindowManagerGlobal.addView里.創建ViewRootImpl,此時是在ui線程中.

ViewRootImpl里的mView屬性.host屬性,就是view樹

添加窗口時通過requestLayout();向ui線程發送消息.最後回調到ViewRootImpl.performTraversals.他是整個ui控制項樹,measure.layout.draw的集合.

這里分為五個階段.

預測量階段.進行第一次測量,獲得view.getMeasuredWitdh/Height,此時是控制項樹期望的尺寸.會執行View的onMeasure

布局階段,根據預測量的結果,通過IWindowSession.relayout向WMS請求調整窗口的尺寸這會使WMS對窗口重新布局,並把結果返回給ViewRootImpl.

最終測量階段, 預測量的結果是view樹期望的結果.WMS可能會進行調整,在這里WMS已經把結果通知了ViewRootImpl.因此這里會窗口實際尺寸performTraversals進行布局.view及子類的onMeasure會被回調

布局階段. 測量完成後獲得空間的尺寸,布局要確定控制項的位置,View及子類的onLayout會被回調.

繪制階段,使用WMS提供的surface.進行繪制,View及子類的onDraw會被回調.

通常我們看到的都是 先measure,在layout在draw. 這里看到.其實measure先得到期望值,在和WMS溝通.WMS在調整後,返回確定值,在根據確定值進行mesure.

measureHierarchy里會通過三次協商.執行performMeasure 來確認合適的尺寸.

performMeasure 會調用view 的measure 優會調用onMeasure. 我們可重寫onMeasure來實現測量.而measure 方法是final的.onMeasure 的結果通過setMeasuredDimension方法保存.

對於view. onMeasure.比較容易. 對於ViewGroup.則還要遍歷調用他所以子view的measure. 並且需要考慮padding和子view 的margin. padding是控制項外內邊距. margin 是控制項外邊距.

ViewGroup需要先測量完子view.在根據子view的測量值得到自己的寬高.舉例,如果只有一個子view.那麼ViewGroup的寬= 子view的寬+子view的margin+viewg的padding. 至少是這個值.

繼續回到performTraversals

這里就是提前測量了一下.得到控制項樹希望的尺寸大小,

通過relayoutWindow來布局窗口. ViewRootImpl 通過IWindowSession 來通知WMS進行窗口布局.

這里主要下. 調用WMS後.WMS會調整窗口的尺寸. 同時會生成surface返回給ViewRootImpl. 因此後續的繪畫就有了畫布了.可以看到最後的參數是mSurface.這是本地的surface. 這里會和wms的進行綁定.

接下來繼續performTraversals,綁定WMS返回的surface.然後更新尺寸.

最後進行最終測量. 上邊過程太亂了. 了解下就行.還是看常見的控制項繪制流程.

繪制由viewRootImpl.performTraversals觸發, 抽取出來後,就是這樣

就是直接調用view樹的根的measure方法. 傳入到View

該方法是final .意味著無法重寫.這里又會調用onMeasure.

因此.對於view.在onMeasure中調整好高度,通過setMeasuredDimension設置好自己的測量寬高就可以了.

對應ViewGroup.則在onMeasure中,先要遍歷子view.調用他們的measure(注意一定是調用子類的measure,measure又會調用onMeasure), 子view寬高都知道後,在根據子view的寬高來設置自己.也就是ViewGroup的寬高受子view影響.

可以看到view的measure又調用了onMeasure, 如果是view 則可以直接重新onMeasure來設定大小.而對於ViewGroup, 則需要重寫onMeasure來先遍歷子view.設定大小.然後再設定viewGroup的大小. ViewGroup並沒有重寫onMeasure.因為每個ViewGroup要實現的效果不同,需要自己完成.但ViewGroup提供了幾個方法供ViewGroup的繼承類來遍歷子view.

view的寬高由自己的layoutParams和父view提供的 widthMeasureSpec|heightMeasureSpec共同決定.

View 自己的寬高,是保存在LayoutParams中對,以寬舉例 LayoutParams.width 有三種情況,精確值(就是指定大小),MATCH_PARENT. WRAP_CONTENT,模式則有fuview提供.有 unspecified,exactly,at_most三種.

匹配如下.

其實這個很好理解. 如果子view自己指定了寬高.就用他的值就可以.如果子view是match_parent.那就使用父view提供的寬高. 如果子view是wrap_content,那就不能超過父view的值.

看下ViewGroup為子view繪制而提供的方法,可以看到.ViewGroup會減去padding和margin,來提供子view的寬高.

上步measure過程未完成後,整個view書的 測量寬高都得到了.也就是view.getMeasuredWidth()和getMeasuredHeight()

performLayout中會調用mView.layout. 這樣就把事件從ViewRootImpl傳遞到了view.而layout中又會調用onLayout.ViewGroup需要重寫onLayout為子view進行布局,遍歷調用子view的layout.因此就完成整個view樹的laylut過程.

豎向的實現, 豎向的就行把view從上到下一次排開

這里注意區分.measure過程是先得到子view的測量值,在設定父ViewGroup的值.而layout過程則是先傳入父view的左上右下值,來計運算元view的左上右下的位置值.這里應該具有普遍性.但不知道是否絕對.

performDraw 中的調用draw.又調用mView.draw.然後就進入view樹的繪制了.

view的draw 又會調用onDraw ,viewGroup又調用dispatchDraw()把draw分發到子view里 繪制的畫布就是canvas. 這是從surface.lockCanvas中獲得的一個區域.

而在ViewGroup.dispatchDraw中.重要的一點是getChildDrawingOrder 表示子view的繪制順序.默認是與ziview的添加順序一樣.我們也可以改變他.最後繪制的會顯示在最上邊,而這也影響view的事件傳遞順序.

view.draw. 就是一層一層的畫內容.先畫北京,在onDraw.在畫裝飾什麼的.

canvas.translate(100,300)通過平移坐標系.使之後的內容可以直接在新坐標系中繪制.

這就是ViewGroup在向子view傳遞canvas的時候.方便多了. 會之前先對其ziview的左上角.那麼子view就可以直接從自己坐標軸的(0,0)開始繪制, 繪制完成後ViewGroup在還原原有坐標系.

canvas.save. canvas.restore 用來保存還原坐標系.

view.invalidate.

當某個view發送變化需要重繪時,通過view.invalidate向上通知到ViewRootImpl.從這個view到ViewRootImpl的節點都標記為藏區域.dirty area. ViewRootimpl再次從上到下重繪時,只繪制這些臟區域.效率高.

本來安卓兼容使用鍵盤,也支持,觸摸.二者的輸入事件派發不一樣.使用鍵盤時會有個控制項處於獲得焦點狀態.處於觸摸模式則由用戶決定. 因此控制項分為兩類.任何情況下都能獲得焦點.如輸入文本框.只有在鍵盤操作時才能獲得焦點.如菜單,按鈕.

安卓里有觸摸模式.當發送任意觸摸時進入觸摸模式.當發送方向鍵和鍵盤或者執行View.requestRocusFromTouch時,退出觸摸模式.

獲取焦點. view.request.

先檢查是否能獲取焦點,

然後設置獲取簡單的標記,

上傳遞到ViewRootimpl.保證只能有一個控制項獲取焦點.

通知焦點變化的監聽者.

更新view的drawable狀態,

requestChildFocus會把焦點事件層層上報取消原來有焦點的控制項.最後的效果就是從viewrootimpl中.到最終有焦點的view.構成一條 mFoucued 標識的鏈條.來個圖就明白了.每個view的mFocused總是指向他的直接下級.

獲取focus的傳遞是從底層view到頂層的ViewRootImpl.而取消focus測試從頂層的ViewRootimpl到底層原來那個獲得焦點的view.

而如果是ViewGroup請求獲取焦點,會根據FLAG_MASK_FOCUSABILITY特性來做不同方式,分別有先讓自己獲取焦點,或者安卓view的索引遞增或者遞減來匹配view.

ViewRootImpl 中的.WindowInputEventReceiver接受輸入事件.他會把事件包裝成一個QueuedInputEvent.然後追加到一個單鏈表的末尾.接著重頭到尾的處理輸入事件,並通過deliverInputEvent完成分發.這里會把單鏈表所有事件都處理完.

deliverInput中又會把觸摸事件執行到通過 ViewPreImeInputStage.processKeyEvent. 轉入mView.dispatchPointerEvent(event).這里又進入 dispatchTouchEvent

MotionEvent是觸摸事件的封裝.getAction可以拿到動作的類型和觸控點索引號.

getX(),getY().拿到動作的位置信息.通過getPointID拿到觸控點的id. 動作以down 開頭.跟多個move.最後是up.

,當事件返回true.表示事件被消費掉了.

❷ Android 系統運行機制 【Looper】【Choreographer】篇

目錄:
1 MessageQueue next()
2 Vsync
3 Choreographer doFrame
4 input

系統是一個無限循環的模型, Android也不例外,進程被創建後就陷入了無限循環的狀態

系統運行最重要的兩個概念:輸入,輸出。

Android 中輸入 輸出 的往復循環都是在 looper 中消息機制驅動下完成的

looper 的循環中, messageQueue next 取消息進行處理, 處理輸入事件, 進行輸出, 完成和用戶交互

應用生命周期內會不斷 產生 message 到 messageQueue 中, 有: java層 也有 native層

其中最核心的方法就是 messageQueue 的 next 方法, 其中會先處理 java 層消息, 當 java 層沒有消息時候, 會執行 nativePollOnce 來處理 native 的消息 以及監聽 fd 各種事件

從硬體來看, 屏幕不會一直刷新, 屏幕的刷新只需要符合人眼的視覺停留機制

24Hz , 連續刷新每一幀, 人眼就會認為畫面是流暢的

所以我們只需要配合上這個頻率, 在需要更新 UI 的時候執行繪制操作

如何以這個頻率進行繪制每一幀: Android 的方案是 Vsync 信號驅動。

Vsync 信號的頻率就是 24Hz , 也就是每隔 16.6667 ms 發送一次 Vsync 信號提示系統合成一幀。

監聽屏幕刷新來發送 Vsync 信號的能力,應用層 是做不到的, 系統是通過 jni 回調到 Choreographer 中的 Vsync 監聽, 將這個重要信號從 native 傳遞到 java 層。

總體來說 輸入事件獲取 Vsync信號獲取 都是先由 native 捕獲事件 然後 jni 到 java 層實現業務邏輯

執行的是 messageQueue 中的關鍵方法: next

next 主要的邏輯分為: java 部分 和 native 部分

java 上主要是取java層的 messageQueue msg 執行, 無 msg 就 idleHandler

java層 無 msg 會執行 native 的 pollOnce@Looper

native looper 中 fd 監聽封裝為 requestQueue, epoll_wait 將 fd 中的事件和對應 request 封裝為 response 處理, 處理的時候會調用 fd 對應的 callback 的 handleEvent

native 層 pollOnce 主要做的事情是:

vsync 信號,輸入事件, 都是通過這樣的機制完成的。

epoll_wait 機制 拿到的 event , 都在 response pollOnce pollInner 處理了

這里的 dispatchVsync 從 native 回到 java 層

native:

java:

收到 Vsync 信號後, Choreographer 執行 doFrame

應用層重要的工作幾乎都在 doFrame 中

首先看下 doFrame 執行了什麼:

UI 線程的核心工作就在這幾個方法中:

上述執行 callback 的過程就對應了圖片中 依次處理 input animation traversal 這幾個關鍵過程

執行的周期是 16.6ms, 實際可能因為一些 delay 造成一些延遲、丟幀

input 事件的整體邏輯和 vsync 類似

native handleEvent ,在 NativeInputEventReceiver 中處理事件, 區分不同事件會通過 JNI

走到 java 層,WindowInputEventReceiver 然後進行分發消費

native :

java:

input事件的處理流程:

輸入event deliverInputEvent

deliver的 input 事件會來到 InputStage

InputStage 是一個責任鏈, 會分發消費這些 InputEvent

下面以滑動一下 recyclerView 為例子, 整體邏輯如下:

vsync 信號到來, 執行 doFrame,執行到 input 階段

touchEvent 消費, recyclerView layout 一些 ViewHolder

scroll 中 fill 結束,會執行 一個 recyclerView viewProperty 變化, 觸發了invalidate

invalidate 會走硬體加速, 一直到達 ViewRootImpl , 從而將 Traversal 的 callback post choreographer執行到 traversal 階段就會執行

ViewRootImpl 執行 performTraversal , 會根據目前是否需要重新layout , 然後執行layout, draw 等流程

整個 input 到 traversal 結束,硬體繪制後, sync 任務到 GPU , 然後合成一幀。

交給 SurfaceFlinger 來顯示。

SurfaceFlinger 是系統進程, 每一個應用進程是一個 client 端, 通過 IPC 機制,client 將圖像顯示工作交給 SurfaceFlinger

launch 一個 app:

❸ Android EditText控制項如何獲取軟鍵盤輸入事件呢

物理鍵盤和虛擬鍵盤都是一樣的通過onKeyDown監聽的吧。

熱點內容
安卓在哪裡找游戲 發布:2025-07-04 22:15:25 瀏覽:242
路由器訪問光貓 發布:2025-07-04 22:07:47 瀏覽:897
資料庫顯示語句 發布:2025-07-04 22:04:30 瀏覽:740
編程課道具 發布:2025-07-04 22:04:02 瀏覽:844
華為手機不是安卓什麼時候可以更新米加小鎮 發布:2025-07-04 22:01:37 瀏覽:785
飢荒伺服器搭建視頻 發布:2025-07-04 21:48:38 瀏覽:523
github上傳文件夾 發布:2025-07-04 21:29:22 瀏覽:1003
php課程學習中心 發布:2025-07-04 21:29:16 瀏覽:298
win7加密文件夾如何解密 發布:2025-07-04 21:25:24 瀏覽:555
為啥系統緩存的垃圾多呢 發布:2025-07-04 21:15:45 瀏覽:952