進階觸控筆功能

Android 和 ChromeOS 提供多種 API,可協助您建構提供 絕佳的觸控筆體驗 MotionEvent 類別會公開 觸控筆與螢幕互動的相關資訊,包括觸控筆壓力 方向、傾斜度、懸停和手掌偵測。低延遲圖像和動態 預測程式庫能強化觸控筆在螢幕上算繪的功能 就像使用紙筆一樣自然

MotionEvent

MotionEvent 類別代表使用者輸入內容的互動情形,例如位置 以及畫面上的觸控指標移動如為觸控筆輸入,請MotionEvent 也會提供壓力、方向、傾斜度和懸停資料。

事件資料

如要存取 MotionEvent 資料,請在元件中新增 pointerInput 修飾符:

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

MotionEvent 物件提供與 UI 以下方面相關的資料 事件:

  • 動作:對裝置進行的實體互動操作,例如輕觸螢幕、 在螢幕表面上移動指標,將指標懸停在畫面上 途徑
  • 指標:與螢幕互動的物件 ID,包括手指、 觸控筆, 滑鼠
  • 軸:資料類型,包括 x 和 y 座標、壓力、傾斜度、方向、 懸停 (距離)

動作

如要實作觸控筆支援功能,您需要瞭解使用者的動作 成效。

MotionEvent 提供多種 ACTION 常數,可用於定義動作 事件。觸控筆最重要的動作包括:

動作 說明
ACTION_DOWN
ACTION_POINTER_DOWN
指標已與螢幕接觸。
ACTION_MOVE 指標在畫面上移動。
ACTION_UP
ACTION_POINTER_UP
指標不再與螢幕接觸
ACTION_CANCEL 之前或當下的動作集應取消的時機。

您的應用程式可以在 ACTION_DOWN時執行新的筆觸等工作 觸發時,請使用 ACTION_MOVE, 繪製筆觸,在出現時則完成筆觸 ACTION_UP 已觸發。

針對特定指定,從 ACTION_DOWNACTION_UP 的一組 MotionEvent 動作 指標稱為動作集

指標

大部分的螢幕為多點觸控功能:系統會為手指指派指標, 觸控筆、滑鼠或其他指標物件與螢幕互動。指標 索引可讓您取得特定指標的軸資訊,例如 第一隻手指觸碰螢幕或第二隻手指的位置。

指標索引範圍從 0 到傳回的指標數。 MotionEvent#pointerCount()敬上 減號 1。

指標的軸值可使用 getAxisValue(axis, pointerIndex) 方法存取。 省略指標索引時,系統會傳回第一個 指標,指標 0。

MotionEvent 物件包含使用中指標類型的相關資訊。個人中心 可以藉由疊代指標索引並呼叫 這個 getToolType(pointerIndex)敬上 方法。

如要進一步瞭解指標,請參閱「處理多點觸控 手勢

觸控筆輸入

你可以使用下列條件篩選觸控筆輸入內容 TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

您也可以使用觸控筆做為橡皮擦, TOOL_TYPE_ERASER:

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

觸控筆的軸資料

ACTION_DOWNACTION_MOVE 提供觸控筆的軸資料,包括 x 和 y 座標、壓力、方向、傾斜度和懸停距離。

如要啟用這類資料的存取權,MotionEvent API 提供了 getAxisValue(int), 參數是下列任一軸的 ID:

Axis getAxisValue() 的傳回值
AXIS_X 動作事件的 X 座標。
AXIS_Y 動作事件的 Y 座標。
AXIS_PRESSURE 如果是觸控螢幕或觸控板,則為以手指、觸控筆或其他指標施加的壓力。如果是滑鼠或軌跡球,按下主按鈕的值為 1,否則為 0。
AXIS_ORIENTATION 如果是觸控螢幕或觸控板,則為手指、觸控筆或其他指標相對於裝置垂直面的方向。
AXIS_TILT 觸控筆的傾斜度,以弧度為單位。
AXIS_DISTANCE 觸控筆與螢幕間的距離。

舉例來說,MotionEvent.getAxisValue(AXIS_X) 會傳回 這是第一個指標

另請參閱「處理多點觸控 手勢

位置

您可以使用下列呼叫擷取指標的 x 和 y 座標:

畫面上的觸控筆繪圖與對應的 x 和 y 座標。
圖 1. 觸控筆指標的 x 和 y 螢幕座標。

氣壓

您可以使用 MotionEvent#getAxisValue(AXIS_PRESSURE) 或針對第一個指標 MotionEvent#getPressure()

觸控螢幕或觸控板的壓力值介於 0 (否) 壓力) 和 1 之間,但可能會傳回更高的值 (視螢幕而定) 校正。

代表從低到高持續施壓的觸控筆觸。左側筆觸較窄且顏色較淡,表示筆觸壓力較低。筆觸由左到右變寬且顏色變深,至畫面最右側達到最寬且顏色最深的筆觸,表示壓力最高。
圖 2. 壓力表示法:左側為低壓,右側為高壓。
,瞭解如何調查及移除這項存取權。

方向

方向是指觸控筆指向的方向。

您可以使用 getAxisValue(AXIS_ORIENTATION)getOrientation() (用於第一個指標)。

針對觸控筆,系統會以弧度值傳回 0 到 pi (π) 的弧度值 順時針或逆時針方向,或 0 到 -pi。

您可以透過方向實作逼真的筆刷體驗。舉例來說, 觸控筆代表平坦筆刷,筆刷的寬度則取決於 觸控筆方向

圖 3. 觸控筆指向左邊約負 0.57 度。

垂直運鏡

傾斜度指的是觸控筆相對於螢幕的傾斜度。

傾斜度會傳回觸控筆的正角 (以弧度為單位),其中 0 是 垂直方向與 π/2 垂直。

您可以使用 getAxisValue(AXIS_TILT) 擷取傾斜度 ( 第一個指標)。

傾斜度可用來盡可能重現真實工具,例如 使用傾斜的鉛筆來模擬陰影。

觸控筆從螢幕表面傾斜約 40 度。
圖 4. 觸控筆傾斜了約 0.785 弧度,或沿垂直方向傾斜 45 度。
,瞭解如何調查及移除這項存取權。

懸停

可透過以下方式取得觸控筆與螢幕間的距離: getAxisValue(AXIS_DISTANCE)。此方法會傳回 0.0 (與 螢幕),並在觸控筆離開螢幕時調出更高的值。懸停 螢幕和觸控筆的六邊形 (point) 之間的距離取決於 同時具備螢幕和觸控筆的製造商資訊由於導入方式可能 不同,請勿仰賴精確的值來提供應用程式的重要功能。

觸控筆懸停功能可用來預覽筆刷大小,或表示 按鈕。

圖 5. 觸控筆懸停在螢幕上。即使觸控筆未觸碰螢幕表面,應用程式也會做出回應。

注意:Compose 提供的修飾符會影響 UI 元素的互動狀態:

  • hoverable:將元件設為可透過指標的進入/離開事件,成為可懸停的元件。
  • indication:在發生互動時為此元件繪製視覺效果。

防止誤觸、導覽及不必要的輸入

多點觸控螢幕有時可能會註冊不必要的觸控動作,例如 使用者在手寫時自然會把手放在螢幕上,以便尋求支援。 防止誤觸機制可偵測這種行為,並通知您 最後一個 MotionEvent 集應取消

因此,您必須保留使用者輸入內容的記錄,以免不必要的觸控行為 就可以從畫面上移除正確的使用者輸入內容 重新轉譯。

ACTION_CANCEL 和 FLAG_CANCELED

ACTION_CANCELFLAG_CANCELED 是 這兩種 ID 都會用來通知您,先前的 MotionEvent 組合應該 取消了上一個 ACTION_DOWN,因此您可以復原這項操作 特定指標的繪圖應用程式的筆觸。

ACTION_CANCEL

已在 Android 1.0 中新增 (API 級別 1)

ACTION_CANCEL 表示應取消上一組動作事件。

如果偵測到以下任一項目,就會觸發 ACTION_CANCEL

  • 導覽手勢
  • 防止誤觸

觸發 ACTION_CANCEL 時,建議您使用 getPointerId(getActionIndex())。接著,從輸入記錄中移除使用該指標建立的筆觸,然後重新轉譯場景。

FLAG_CANCELED

已在 Android 13 (API 級別 33) 中新增

FLAG_CANCELED敬上 表示指標上移是使用者無意輕觸。旗標是 通常是在使用者不小心觸碰螢幕時設定,例如按住 或將手掌放在螢幕上。

您可以透過下列方式存取標記值:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

如果已設定此標記,您需要從最後 MotionEvent 的最後一個集上, 從這個指標選取 ACTION_DOWN

ACTION_CANCEL 一樣,您可以透過 getPointerId(actionIndex) 找到指標。

圖 6. 觸控筆和手掌觸控時建立了 MotionEvent 集。系統會取消手掌觸控動作,並重新轉譯螢幕畫面。

全螢幕、無邊框設計和導覽手勢

如果應用程式為全螢幕模式且在邊緣附近設有可操作元素 (例如 現在可以從繪圖或記事應用程式的畫布 顯示導覽畫面或將應用程式移至背景都可能造成 在這幅畫布上做出不必要的觸控動作

圖 7. 滑動手勢即可將應用程式移至背景。

如要防止手勢在應用程式中觸發不必要的觸控動作,您可以採取下列做法: 利用插邊ACTION_CANCEL

另請參閱「防止誤觸、導覽及不必要的輸入」。 專區。

使用 setSystemBarsBehavior()敬上 方法和 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE。 / WindowInsetsController 如何避免導覽手勢造成不必要的觸控事件:

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

如要進一步瞭解插邊和手勢管理,請參閱:

低延遲

延遲時間是指硬體、系統和應用程式所需的處理時間 以及轉譯使用者輸入內容

延遲時間 = 硬體和 OS 的輸入處理 + 應用程式處理 + 系統組合

  • 硬體轉譯
,瞭解如何調查及移除這項存取權。
延遲會導致轉譯的筆觸落後於觸控筆的位置。轉譯後的筆觸與觸控筆位置之間的時間差即為延遲時間。
圖 8.延遲會導致轉譯的筆觸落後於觸控筆的位置。

延遲的來源

  • 正在透過觸控螢幕 (硬體) 註冊觸控筆:初始無線連線 觸控筆和 OS 通訊完成註冊及同步時。
  • 觸控取樣率 (硬體):觸控螢幕的每秒次數 檢查指標是否觸碰表面,範圍從 60 到 1000Hz。
  • 輸入處理 (應用程式):套用色彩、圖形效果及轉換 使用者輸入內容時
  • 圖形轉譯 (OS + 硬體):緩衝區互換、硬體處理。

低延遲圖形

Jetpack 低延遲圖形程式庫 減少使用者輸入內容與畫面算繪作業之間的處理時間。

程式庫會避免多緩衝區轉譯作業,並 採用前端緩衝區轉譯技術,也就是直接寫入 。

前端緩衝區轉譯

前端緩衝區是螢幕用於轉譯的記憶體。距離最近的 應用程式可以直接在螢幕上繪圖。低延遲程式庫能 直接轉譯至前端緩衝區。這麼做可提高成效 防止緩衝區互換,這種情況在一般多緩衝區轉譯作業中可能會發生 或雙緩衝區轉譯 (最常見的情況)。

應用程式將資料寫入螢幕緩衝區,並從螢幕緩衝區讀取資料。
圖 9. 前端緩衝區轉譯。
應用程式寫入多緩衝區,而多緩衝區與螢幕緩衝區進行互換。應用程式從螢幕緩衝區讀取資料。
圖 10. 多緩衝區轉譯。

雖然前端緩衝區轉譯是絕佳的 但不適合用來重新整理整個螢幕畫面。取代為 前端緩衝區轉譯,應用程式會將內容轉譯到緩衝區中 螢幕正在閱讀中。因此,系統可能會 出現錯誤或撕裂 (請見下方說明)。

低延遲程式庫適用於 Android 10 (API 級別 29) 以上版本 以及搭載 Android 10 (API 級別 29) 以上版本的 ChromeOS 裝置。

依附元件

低延遲程式庫提供了用於前端緩衝區轉譯的元件 。程式庫會新增為應用程式模組中的依附元件 build.gradle 檔案:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

GLFrontBufferRenderer 回呼

低延遲程式庫包含了 GLFrontBufferRenderer.Callback敬上 介面,其中定義下列方法:

低延遲程式庫與資料類型 GLFrontBufferRenderer

然而,程式庫會將資料處理為包含數百個資料點的資料流; 因此在設計資料時,應針對記憶體用量和分配進行最佳化調整。

回呼

如要啟用轉譯回呼,請實作 GLFrontBufferedRenderer.Callback 和 覆寫 onDrawFrontBufferedLayer()onDrawDoubleBufferedLayer()GLFrontBufferedRenderer 會使用回呼,在 盡可能提高安全性

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }
   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}
宣告 GLFrontBufferedRenderer 例項

提供 SurfaceViewGLFrontBufferedRenderer 回呼函式。GLFrontBufferedRenderer 會將算繪作業最佳化 ,接著使用回呼:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
轉譯

當您呼叫 renderFrontBufferedLayer()敬上 方法,觸發 onDrawFrontBufferedLayer() 回呼。

當您呼叫 commit()敬上 函式,觸發 onDrawMultiDoubleBufferedLayer() 回呼。

在下方範例中,程序會轉譯至前端緩衝區 (快速 使用者開始在螢幕上繪圖 (ACTION_DOWN) 並移動時 指標 (ACTION_MOVE)。程序會轉譯為雙緩衝區 當指標離開螢幕表面 (ACTION_UP) 時。

別擔心!您可以使用 requestUnbufferedDispatch()敬上 要求輸入系統不要批次處理動作事件,而是 功能推出時,您可以:

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

轉譯的注意事項

✓ 建議做法

在螢幕的一小塊區域中進行手寫、繪圖和素描。

✗ 不建議

在全螢幕模式下更新、平移及縮放內容。這麼做可能導致畫面撕裂。

撕裂

當螢幕在重新整理時,畫面緩衝區出現撕裂 同時修改兩者。螢幕畫面的一部分顯示新資料,另一個部分則顯示 顯示舊資料

由於螢幕畫面重新整理,Android 圖片的上下部分並未對齊。
圖 11. 螢幕畫面由上往下重新整理,導致畫面撕裂。

動作預測

Jetpack 動作預測 程式庫減少 可預估使用者的筆觸路徑,並提供暫時性的 以人工方式指向轉譯器

動作預測程式庫會以 MotionEvent 物件形式接收使用者的實際輸入內容。 這些物件中包含 x 和 y 座標、壓力和時間 動態預測器所運用的這些結果來預測未來的MotionEvent 如需儲存大量結構化物件 建議使用 Cloud Bigtable

預測的 MotionEvent 物件只是預估值。預測事件能減少 使用者感知的延遲時間,但必須以實際的 MotionEvent 取代預測的資料 再加以接收

動作預測程式庫適用於 Android 4.4 (API 級別 19) 版和 以及搭載 Android 9 (API 級別 28) 以上版本的 ChromeOS 裝置。

延遲會導致轉譯的筆觸落後於觸控筆的位置。系統會透過預測點填補筆觸與觸控筆彼此的時間差。剩下的時間差即為感知延遲時間。
圖 12. 以動作預測縮短延遲時間。

依附元件

動作預測程式庫提供預測實作。 程式庫會新增為應用程式模組 build.gradle 檔案中的依附元件:

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

實作

動作預測程式庫包含了 MotionEventPredictor敬上 介面,其中定義下列方法:

  • record(): 將 MotionEvent 物件儲存為使用者動作的記錄
  • predict(): 傳回預測的 MotionEvent
宣告 MotionEventPredictor 的例項
var motionEventPredictor = MotionEventPredictor.newInstance(view)
為預測程序提供資料
motionEventPredictor.record(motionEvent)
預測

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

動作預測的注意事項

✓ 建議做法

在新增預測點後移除舊的預測點。

✗ 不建議

請勿使用預測點進行最終算繪。

筆記應用程式

ChromeOS 可宣告應用程式具有某些筆記相關功能。

如要在 ChromeOS 上將應用程式註冊為筆記應用程式,請參閱「輸入 相容性

如要在 Android 上註冊應用程式做為記事,請參閱建立記事 app

Android 14 (API 級別 34) 導入了 ACTION_CREATE_NOTE敬上 意圖,可讓應用程式在鎖定時啟動筆記活動 。

透過 ML Kit 辨識數位墨水

使用 ML Kit 數位墨水 辨識, 您的應用程式可以辨識數位表面的手寫文字 語言。也可以對素描進行分類。

ML Kit 提供 Ink.Stroke.Builder敬上 類別,用來建立可由機器學習模型處理的 Ink 物件 將手寫內容轉換成文字

除了手寫辨識外,模型還能 手勢 例如刪除和圈選

請參閱數位墨水 辨識

其他資源

開發人員指南

程式碼研究室