支援進階觸控筆功能

觸控筆可讓使用者輕鬆與應用程式互動,且無論是記事、素描、處理效率提升應用程式,都能讓使用者輕鬆自如,透過遊戲和娛樂類應用程式放鬆身心。

Android 和 ChromeOS 提供各種 API,可在應用程式中打造卓越的觸控筆體驗。MotionEvent 類別提供使用者與螢幕互動的相關資訊,包括觸控筆壓力、螢幕方向、傾斜度、懸停和手掌偵測資訊。低延遲圖像和動作預測程式庫能讓觸控筆在螢幕上轉譯,帶來有如直接使用紙筆的自然流暢體驗。

MotionEvent

MotionEvent 類別代表使用者輸入內容的互動情形,例如螢幕上觸控點的位置和移動情形。如果是觸控筆的輸入內容,MotionEvent 也會提供壓力、方向、傾斜度和懸停資料。

事件資料

如要在以 View 為基礎的應用程式中存取 MotionEvent 資料,請設定 onTouchListener

Kotlin

val onTouchListener = View.OnTouchListener { view, event ->
  // Process motion event.
}

Java

View.OnTouchListener listener = (view, event) -> {
  // Process motion event.
};

事件監聽器會從系統接收 MotionEvent 物件,以便應用程式處理。

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

  • 動作:與裝置的實際互動 - 輕觸螢幕、將指標移至螢幕表面、將滑鼠遊標懸停在螢幕表面上
  • 指標:與螢幕互動的物件 ID 包括手指、觸控筆、滑鼠
  • 軸:資料類型,包括 x 和 y 座標、壓力、傾斜度、方向和懸停值 (距離)

動作

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

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

操作 說明
動作「向下」
動作_POINTER_DOWN
指標已與螢幕接觸。
行動 指標在畫面上移動。
行動_UP
行動_POINTER_UP
指標不再與螢幕接觸
ACTION_取消 之前或當前的動作集應取消的時機。

您的應用程式可以執行以下工作:在 ACTION_DOWN 發生時開始新的筆觸、使用 ACTION_MOVE, 繪製筆觸,以及在觸發 ACTION_UP 時完成筆觸。

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

指標

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

指標索引範圍從 0 開始,到 MotionEvent#pointerCount() 傳回的指標數量減去 1。

指標的軸值可透過 getAxisValue(axis, pointerIndex) 方法存取。省略指標索引時,系統會傳回第一個指標的值,也就是零 (0)。

MotionEvent 物件包含使用中指標類型的相關資訊。您可以疊代指標索引並呼叫 getToolType(pointerIndex) 方法,以取得指標類型。

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

觸控筆輸入

您可以使用 TOOL_TYPE_STYLUS 篩選觸控筆輸入內容:

Kotlin

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

Java

boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);

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

Kotlin

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

Java

boolean 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 座標。

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

位置

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

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

氣壓

您可以使用下列呼叫擷取指標壓力:

getAxisValue(AXIS_PRESSURE)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 與螢幕接觸的值,觸控筆在螢幕和觸控筆上的懸停距離 (點) 取決於螢幕和觸控筆的製造商。由於實作方式可能不同,請勿仰賴應用程式重要功能的精確值。

觸控筆懸停功能可用來預覽筆刷大小,或指示即將選取的按鈕。

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

注意:Compose 提供了一組修飾符元素,可用於變更 UI 元素的狀態:

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

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

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

因此,您必須保留使用者輸入內容的記錄,以便從螢幕上移除不必要的觸控動作,並重新轉譯合理的使用者輸入內容。

ACTION_CANCEL 和 FLAG_CANCELED

ACTION_CANCELFLAG_CANCELED 都是用來通知您,應從最後一個 ACTION_DOWN 取消先前的 MotionEvent 集,舉例來說,您可以藉此復原繪圖應用程式中特定指標的最後一筆動作。

ACTION_取消

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

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

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

  • 導覽手勢
  • 防止誤觸

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

FLAG_CANCELED

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

FLAG_CANCELED 表示指標上移是使用者不小心輕觸的。這個標記通常是在使用者不小心觸碰螢幕時設定,例如手持裝置或將手掌放在螢幕上時。

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

Kotlin

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

Java

boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;

如果已設定標記,您需要從該指標的最後一個 ACTION_DOWN 中,復原最後一個 MotionEvent 集。

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

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

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

如果應用程式為全螢幕模式,且在畫面邊緣附近有可操作的元素 (例如繪圖或記事應用程式的畫布),只要從螢幕底部向上滑動即可顯示導覽畫面,或是將應用程式移至背景,但這可能對畫布進行不必要的觸控動作。

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

您可以利用插邊ACTION_CANCEL,防止手勢在應用程式中觸發不必要的觸控動作。

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

使用 setSystemBarsBehavior() 方法和 WindowInsetsControllerBEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,可避免導覽手勢造成不必要的觸控事件:

Kotlin

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

Java

// Configure the behavior of the hidden system bars.
windowInsetsController.setSystemBarsBehavior(
    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 會使用回呼,盡可能以最佳方式轉譯資料。

Kotlin

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.
   }
}

Java

GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks =
    new GLFrontBufferedRenderer.Callback<DATA_TYPE>() {
        @Override
        public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager,
            @NonNull BufferInfo bufferInfo,
            @NonNull float[] transform,
            DATA_TYPE data_type) {
                // OpenGL for front buffer, short, affecting small area of the screen.
        }

    @Override
    public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager,
        @NonNull BufferInfo bufferInfo,
        @NonNull float[] transform,
        @NonNull Collection<? extends DATA_TYPE> collection) {
            // OpenGL full scene rendering.
    }
};
宣告 GLFrontBufferedRenderer 的例項

藉由提供先前建立的 SurfaceView 和回呼,為 GLFrontBufferedRenderer 做好準備。GLFrontBufferedRenderer 會使用回呼,對前端緩衝區和雙緩衝區的轉譯作業進行最佳化調整:

Kotlin

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

Java

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

呼叫 renderFrontBufferedLayer() 方法 (觸發 onDrawFrontBufferedLayer() 回呼) 時,系統會啟動前端緩衝區轉譯作業。

當您呼叫 commit() 函式觸發 onDrawMultiDoubleBufferedLayer() 回呼時,系統會繼續進行雙緩衝區轉譯作業。

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

您可以使用 requestUnbufferedDispatch() 要求輸入系統不要批次處理事件,而是在可用時立即傳送事件:

Kotlin

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()
   }
}

Java

switch (motionEvent.getAction()) {
   case MotionEvent.ACTION_DOWN: {
       // Deliver input events as soon as they arrive.
       surfaceView.requestUnbufferedDispatch(motionEvent);

       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE);
   }
   break;
   case MotionEvent.ACTION_MOVE: {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE);
   }
   break;
   case MotionEvent.ACTION_UP: {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit();
   }
   break;
   case MotionEvent.ACTION_CANCEL: {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel();
   }
   break;
}

轉譯的注意事項

請這麼做

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

錯誤做法

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

撕裂

如果螢幕在重新整理的同時修改螢幕緩衝區,畫面會發生撕裂。螢幕畫面的一部分顯示新資料,另一個部分則顯示舊資料。

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

動作預測

Jetpack 動作預測程式庫可預估使用者的筆觸路徑,並向轉譯器提供臨時的人工預測點,藉此縮短使用者的感知延遲時間。

動作預測程式庫會以 MotionEvent 物件形式接收使用者的實際輸入內容。物件中包含 x 和 y 座標、壓力和時間的資訊,可供動態預測程序預測未來的 MotionEvent 物件。

預測的 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 的例項

Kotlin

var motionEventPredictor = MotionEventPredictor.newInstance(view)

Java

MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
為預測程序提供資料

Kotlin

motionEventPredictor.record(motionEvent)

Java

motionEventPredictor.record(motionEvent);
預測

Kotlin

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

Java

switch (motionEvent.getAction()) {
   case MotionEvent.ACTION_MOVE: {
       MotionEvent predictedMotionEvent = motionEventPredictor.predict();
       if(predictedMotionEvent != null) {
           // use predicted MotionEvent to inject a new artificial point
       }
   }
   break;
}

動作預測的注意事項

請這麼做

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

錯誤做法

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

筆記應用程式

ChromeOS 可宣告應用程式某些記事動作。

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

如要在 Android 上將應用程式註冊為記事應用程式,請參閱「建立記事應用程式」。

Android 14 (API 級別 34) 導入了 ACTION_CREATE_NOTE 意圖,可讓應用程式在螢幕鎖定畫面上啟動記事活動。

透過 ML Kit 辨識數位墨水

有了 ML Kit 數位墨水辨識功能,您的應用程式便可識別數位平面上數百種語言的手寫文字,也可以對素描進行分類。

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

除了手寫辨識外,模型還能辨識手勢,例如刪除和畫圓動作。

詳情請參閱「數位墨水辨識」。

其他資源

開發人員指南

程式碼研究室