1. 始める前に
タッチペンはペンの形をしたツールで、正確なタスクを実行するのに役立ちます。この Codelab では、android.os
ライブラリと androidx
ライブラリを使用してオーガニックなタッチペン エクスペリエンスを実装する方法を学びます。また、MotionEvent
クラスを使用して、圧力、傾斜、向き、パーム リジェクションをサポートし、不要なタップを防ぐ方法も学びます。さらに、モーション予測でタッチペンのレイテンシを低減し、OpenGL と SurfaceView
クラスで低レイテンシのグラフィックスを取得する方法についても学びます。
前提条件
- Kotlin とラムダの使用経験。
- Android Studio の使用方法についての基礎知識があること。
- Jetpack Compose に関する基本的な知識。
- 低レイテンシ グラフィックのための OpenGL に関する基礎知識。
学習内容
- タッチペンに
MotionEvent
クラスを使用する方法。 - タッチペン機能(圧力、傾斜、向きのサポートなど)を実装する方法。
Canvas
クラスを描画する方法。- モーション予測を実装する方法
- OpenGL と
SurfaceView
クラスを使用して低レイテンシのグラフィックをレンダリングする方法。
必要なもの
- 最新バージョンの Android Studio
- ラムダを含む Kotlin 構文の使用経験。
- Compose に関する基本的な経験。Compose に慣れていない場合は、Jetpack Compose の基本の Codelab を修了してください。
- タッチペン対応のデバイス。
- アクティブなタッチペン。
- Git。
2. スターター コードを取得する
スターター アプリのテーマ設定と基本設定を含むコードを取得する手順は次のとおりです。
- この GitHub リポジトリのクローンを作成します。
git clone https://github.com/android/large-screen-codelabs
advanced-stylus
フォルダを開きます。start
フォルダにはスターター コードが含まれ、end
フォルダには解答コードが含まれています。
3. 基本的な描画アプリを実装する
まず、ユーザーが描画を行える基本的な描画アプリ用の必要なレイアウトを作成し、Canvas
Composable
関数を使用して画面にタッチペン属性を表示します。次の画像のように表示されます。
上段は Canvas
Composable
関数で、タッチペンのビジュアリゼーションを描画し、向き、傾斜、圧力など、タッチペンのさまざまな属性を表示します。下端は、タッチペン入力を受け取って単純なストロークを描画する Canvas
Composable
関数です。
描画アプリの基本レイアウトを実装する手順は次のとおりです。
- Android Studio で、クローンを作成したリポジトリを開きます。
app
>java
>com.example.stylus
をクリックします。次に、MainActivity
をダブルクリックします。MainActivity.kt
ファイルが開きます。MainActivity
クラスで、StylusVisualization
およびDrawArea
Composable
関数に注目してください。このセクションでは、DrawArea
Composable
関数に焦点を当てます。
StylusState
クラス を作成します。
- 同じ
ui
ディレクトリで、[File] > [New] > [Kotlin/Class file] をクリックします。 - テキスト ボックスで、Name プレースホルダを
StylusState.kt
に置き換え、Enter
(macOS ではreturn
)を押します。 StylusState.kt
ファイルでStylusState
データクラスを作成し、次の表の変数を追加します。
変数 | 種類 | デフォルト値 | 説明 |
|
| 0~1.0 の範囲の値。 | |
|
| -pi から pi の範囲のラジアン値。 | |
|
| 0 から pi/2 の範囲のラジアン値。 | |
|
|
|
StylusState.kt
package com.example.stylus.ui
import androidx.compose.ui.graphics.Path
data class StylusState(
var pressure: Float = 0F,
var orientation: Float = 0F,
var tilt: Float = 0F,
var path: Path = Path(),
)
MainActivity.kt
ファイルでMainActivity
クラスを探し、mutableStateOf()
関数でタッチペンの状態を追加します。
MainActivity.kt
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.example.stylus.ui.StylusState
class MainActivity : ComponentActivity() {
private var stylusState: StylusState by mutableStateOf(StylusState())
DrawPoint
クラス
DrawPoint
クラスは、画面に描画されたそれぞれの点に関するデータを保存します。これらの点をリンクすると、線が作成されます。これは、Path
オブジェクトの動作を模倣しています。
DrawPoint
クラスは PointF
クラスを拡張します。これには、次のデータが含まれます。
パラメータ | 種類 | 説明 |
|
| Coordinate |
|
| Coordinate |
|
| ポイントの種類 |
DrawPointType
列挙型で記述される DrawPoint
オブジェクトには、次の 2 種類があります。
種類 | 説明 |
| 線の始点を所定の位置に移動します。 |
| 前の点から線をトレースします。 |
DrawPoint.kt
import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)
データポイントをパスにレンダリングする
このアプリの場合、StylusViewModel
クラスは、線のデータを保持し、レンダリング用のデータを用意して、パーム リジェクションのために Path
オブジェクトに対してなんらかのオペレーションを実行します。
- 線のデータを保持するには、
StylusViewModel
クラスでDrawPoint
オブジェクトの可変リストを作成します。
StylusViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint
class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()
データポイントをパスにレンダリングする手順は次のとおりです。
StylusViewModel.kt
ファイルのStylusViewModel
クラスにcreatePath
関数を追加します。Path()
コンストラクタを使用して、Path
型のpath
変数を作成します。currentPath
変数内の各データポイントを反復処理するfor
ループを作成します。- データポイントの種類が
START
の場合は、moveTo
メソッドを呼び出して、指定したx
座標とy
座標で線を開始します。 - それ以外の場合は、データポイントの
x
座標とy
座標を指定してlineTo
メソッドを呼び出して、前の点にリンクします。 path
オブジェクトを返します。
StylusViewModel.kt
import androidx.compose.ui.graphics.Path
import com.example.stylus.data.DrawPoint
import com.example.stylus.data.DrawPointType
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
private fun createPath(): Path {
val path = Path()
for (point in currentPath) {
if (point.type == DrawPointType.START) {
path.moveTo(point.x, point.y)
} else {
path.lineTo(point.x, point.y)
}
}
return path
}
private fun cancelLastStroke() {
}
MotionEvent
オブジェクトを処理する
タッチペン イベントは MotionEvent
オブジェクトを介して渡されます。このオブジェクトは、実行されたアクションとそれに関連するデータ(ポインタの位置や圧力など)に関する情報を提供します。次の表に、MotionEvent
オブジェクトの定数とそのデータの一部を示します。これらを使用して、画面上でのユーザー操作を特定できます。
定数 | データ |
| ポインタで画面をタップします。これは、 |
| ポインタが画面上を移動します。これが描画される線です。 |
| ポインタでの画面のタップを停止します。線の終わりです。 |
| 不要なタップが検出されました。最後のストロークをキャンセルします。 |
アプリが新しい MotionEvent
オブジェクトを受け取ると、新しいユーザー入力を反映するように画面がレンダリングされます。
StylusViewModel
クラスのMotionEvent
オブジェクトを処理するには、線の座標を収集する関数を作成します。
StylusViewModel.kt
import android.view.MotionEvent
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
currentPath.add(
DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.START)
)
}
MotionEvent.ACTION_MOVE -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_UP -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_CANCEL -> {
// Unwanted touch detected.
cancelLastStroke()
}
else -> return false
}
return true
}
UI にデータを送信する
UI が StylusState
データクラスの変更を収集できるように StylusViewModel
クラスを更新する手順は次のとおりです。
StylusViewModel
クラスで、StylusState
クラスのMutableStateFlow
型の_stylusState
変数と、StylusState
クラスのStateFlow
型のstylusState
変数を作成します。_stylusState
変数は、StylusViewModel
クラスでタッチペンの状態が変更されるたびに変更され、stylusState
変数はMainActivity
クラスの UI によって使用されます。
StylusViewModel.kt
import com.example.stylus.ui.StylusState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
StylusState
オブジェクト パラメータを受け入れるrequestRendering
関数を作成します。
StylusViewModel.kt
import kotlinx.coroutines.flow.update
...
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
...
private fun requestRendering(stylusState: StylusState) {
// Updates the stylusState, which triggers a flow.
_stylusState.update {
return@update stylusState
}
}
processMotionEvent
関数の最後に、StylusState
パラメータを使用してrequestRendering
関数の呼び出しを追加します。StylusState
パラメータで、motionEvent
変数から傾斜、圧力、方向の値を取得し、createPath()
関数を使用してパスを作成します。この操作でフローイベントがトリガーされ、後で UI で接続できます。
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
else -> return false
}
requestRendering(
StylusState(
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
path = createPath()
)
)
UI を StylusViewModel
クラスにリンクする
MainActivity
クラスで、onCreate
関数のsuper.onCreate
関数を見つけて、状態コレクションを追加します。状態収集の詳細については、ライフサイクルを意識した方法でのフローの収集をご覧ください。
MainActivity.kt
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.onEach
import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.flow.collect
...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stylusState
.onEach {
stylusState = it
}
.collect()
}
}
これで、StylusViewModel
クラスが新しい StylusState
状態を送信すると、アクティビティはその状態を受け取り、新しい StylusState
オブジェクトはローカルの MainActivity
クラスの stylusState
変数を更新します。
DrawArea
Composable
関数の本体で、Canvas
Composable
関数にpointerInteropFilter
修飾子を追加してMotionEvent
オブジェクトを指定します。
- 次のように、
MotionEvent
オブジェクトを StylusViewModel のprocessMotionEvent
関数に送信して処理します。
MainActivity.kt
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.pointerInteropFilter
...
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
}
}
stylusState
path
属性を指定してdrawPath
関数を呼び出し、色とストローク スタイルを指定します。
MainActivity.kt
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
with(stylusState) {
drawPath(
path = this.path,
color = Color.Gray,
style = strokeStyle
)
}
}
}
- アプリを実行すると、画面に描画できることがわかります。
4. 圧力、向き、傾斜のサポートを実装する
前のセクションでは、圧力、向き、傾斜などのタッチペン情報を MotionEvent
オブジェクトから取得する方法を確認しました。
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
ただし、このショートカットは、最初のポインタに対してのみ機能します。マルチタッチが検出されると、複数のポインタが検出され、このショートカットは最初のポインタの値、または画面上の最初のポインタの値のみを返します。特定のポインタに関するデータをリクエストするには、pointerIndex
パラメータを使用します。
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)
ポインタとマルチタッチについて詳しくは、マルチタッチ ジェスチャーを処理するをご覧ください。
圧力、向き、傾斜の可視化を追加する
MainActivity.kt
ファイルでStylusVisualization
Composable
関数を見つけ、StylusState
フロー オブジェクトの情報を使用して可視化をレンダリングします。
MainActivity.kt
import StylusVisualization.drawOrientation
import StylusVisualization.drawPressure
import StylusVisualization.drawTilt
...
class MainActivity : ComponentActivity() {
...
@Composable
fun StylusVisualization(modifier: Modifier = Modifier) {
Canvas(
modifier = modifier
) {
with(stylusState) {
drawOrientation(this.orientation)
drawTilt(this.tilt)
drawPressure(this.pressure)
}
}
}
- アプリを実行します。画面上部に、向き、圧力、傾斜を示す 3 つのインジケーターが表示されます。
- 画面をタッチペンでスクリブルし、各ビジュアリゼーションが入力にどのように反応するかを確認します。
StylusVisualization.kt
ファイルを調べて、各可視化がどのように構成されているかを理解します。
5. パーム リジェクションを実装する
画面で不要なタップを登録できる。たとえば、これはユーザーが手書き入力のサポート時に自然に画面に手に置いた場合に発生します。
パーム リジェクションは、この動作を検出するためのメカニズムであり、最後の MotionEvent
オブジェクト セットをキャンセルするようデベロッパーに通知します。MotionEvent
オブジェクトのセットは ACTION_DOWN
定数で始まります。
つまり、画面から不要なタップを取り除き、正当なユーザー入力を再レンダリングできるように、入力の履歴を維持する必要があります。幸い、履歴はすでに currentPath
変数の StylusViewModel
クラスに保存されています。
Android には MotionEvent
オブジェクトからの ACTION_CANCEL
定数が用意されており、不要なタップについてデベロッパーに通知できるようになっています。Android 13 以降では、MotionEvent
オブジェクトは FLAG_CANCELED
定数を提供しており、ACTION_POINTER_UP
定数を確認する必要があります。
cancelLastStroke
関数を実装します。
- 最後の
START
データポイントからデータポイントを削除するには、StylusViewModel
クラスに戻り、最後のSTART
データポイントのインデックスを見つけて、インデックスから 1 を引いた値になるまで最初のデータポイントからそのデータのみを保持するcancelLastStroke
関数を作成します。
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
private fun cancelLastStroke() {
// Find the last START event.
val lastIndex = currentPath.findLastIndex {
it.type == DrawPointType.START
}
// If found, keep the element from 0 until the very last event before the last MOVE event.
if (lastIndex > 0) {
currentPath = currentPath.subList(0, lastIndex - 1)
}
}
ACTION_CANCEL
と FLAG_CANCELED
の定数を追加する
StylusViewModel.kt
ファイルで、processMotionEvent
関数を見つけます。ACTION_UP
定数で、現在の SDK バージョンが Android 13 以降かどうか、およびFLAG_CANCELED
定数がアクティブになっているかどうかを確認するcanceled
変数を作成します。- 次の線で、
canceled
変数が true であるかどうかを確認する条件を作成します。その場合は、cancelLastStroke
関数を呼び出して、MotionEvent
オブジェクトの最後のセットを削除します。それ以外の場合は、currentPath.add
メソッドを呼び出して、MotionEvent
オブジェクトの最後のセットを追加します。
StylusViewModel.kt
import android.os.Build
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> {
val canceled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
(motionEvent.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED
if(canceled) {
cancelLastStroke()
} else {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
}
ACTION_CANCEL
定数内のcancelLastStroke
関数に注目してください。
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_CANCEL -> {
// unwanted touch detected
cancelLastStroke()
}
パーム リジェクションが実装されました。作業コードは palm-rejection
フォルダにあります。
6. 低レイテンシを実装する
このセクションでは、パフォーマンスを高めるために、ユーザー入力と画面レンダリングの間のレイテンシを短縮します。レイテンシには複数の原因があり、その一つは長いグラフィック パイプラインです。フロント バッファ レンダリングを使用して、グラフィック パイプラインを削減します。フロント バッファ レンダリングにより、デベロッパーは画面バッファに直接アクセスできるため、手書きやスケッチから優れた結果を得ることができます。
androidx.graphics
ライブラリが提供する GLFrontBufferedRenderer
クラスは、フロントおよびダブル バッファ レンダリングを処理します。また、onDrawFrontBufferedLayer
コールバック関数による高速レンダリングと onDrawDoubleBufferedLayer
コールバック関数による通常のレンダリングのために SurfaceView
オブジェクトを最適化します。GLFrontBufferedRenderer
クラスと GLFrontBufferedRenderer.Callback
インターフェースは、ユーザー提供データ型とやり取りします。この Codelab では、Segment
クラスを使用します。
使用を開始するには、以下のステップを実行してください。
- Android Studio で
low-latency
フォルダを開き、必要なファイルをすべて取得します。 - プロジェクトの新しいファイルは次のとおりです。
build.gradle
ファイルで、implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
宣言とともに、androidx.graphics
ライブラリがインポートされています。LowLatencySurfaceView
クラスはSurfaceView
クラスを拡張して、画面に OpenGL コードをレンダリングします。LineRenderer
クラスは、画面上に線をレンダリングするために OpenGL コードを保持します。FastRenderer
クラスを使用すると、レンダリングを高速化し、GLFrontBufferedRenderer.Callback
インターフェースを実装できます。また、MotionEvent
オブジェクトをインターセプトします。StylusViewModel
クラスは、LineManager
インターフェースを持つデータポイントを保持します。Segment
クラスは、次のようにセグメントを定義します。x1
、y1
: 第 1 点の座標x2
、y2
: 第 2 点の座標
次の図は、各クラス間でデータがどのように移動するかを示しています。
低レイテンシのサーフェスとレイアウトを作成する
MainActivity.kt
ファイルで、MainActivity
クラスのonCreate
関数を見つけます。onCreate
関数の本体で、FastRenderer
オブジェクトを作成し、viewModel
オブジェクトで渡します。
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- 同じファイルで、
DrawAreaLowLatency
Composable
関数を作成します。 - 関数の本文で、
AndroidView
API を使用してLowLatencySurfaceView
ビューをラップし、fastRendering
オブジェクトを指定します。
MainActivity.kt
import androidx.compose.ui.viewinterop.AndroidView
import com.example.stylus.gl.LowLatencySurfaceView
class MainActivity : ComponentActivity() {
...
@Composable
fun DrawAreaLowLatency(modifier: Modifier = Modifier) {
AndroidView(factory = { context ->
LowLatencySurfaceView(context, fastRenderer = fastRendering)
}, modifier = modifier)
}
onCreate
関数で、Divider
Composable
関数の後に、DrawAreaLowLatency
Composable
関数をレイアウトに対して追加します。
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
Surface(
modifier = Modifier
.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
StylusVisualization(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
)
Divider(
thickness = 1.dp,
color = Color.Black,
)
DrawAreaLowLatency()
}
}
gl
ディレクトリでLowLatencySurfaceView.kt
ファイルを開き、LowLatencySurfaceView
クラスで次のことを確認します。
LowLatencySurfaceView
クラスはSurfaceView
クラスを拡張します。fastRenderer
オブジェクトのonTouchListener
メソッドを使用します。- コールバックで
SurfaceView
ビューにレンダリングできるよう、onAttachedToWindow
関数が呼び出されたときに、fastRenderer
クラスを介したGLFrontBufferedRenderer.Callback
インターフェースをSurfaceView
オブジェクトに接続する必要があります。 onDetachedFromWindow
関数が呼び出されたら、fastRenderer
クラスを介したGLFrontBufferedRenderer.Callback
インターフェースを解放する必要があります。
LowLatencySurfaceView.kt
class LowLatencySurfaceView(context: Context, private val fastRenderer: FastRenderer) :
SurfaceView(context) {
init {
setOnTouchListener(fastRenderer.onTouchListener)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastRenderer.attachSurfaceView(this)
}
override fun onDetachedFromWindow() {
fastRenderer.release()
super.onDetachedFromWindow()
}
}
onTouchListener
インターフェースを使用して MotionEvent
オブジェクトを処理する
ACTION_DOWN
定数が検出されたときに MotionEvent
オブジェクトを処理する手順は次のとおりです。
gl
ディレクトリで、FastRenderer.kt
ファイルを開きます。ACTION_DOWN
定数の本体で、MotionEvent
オブジェクトのx
座標を格納するcurrentX
変数と、y
座標を格納するcurrentY
変数を作成します。currentX
パラメータの 2 つのインスタンスとcurrentY
パラメータの 2 つのインスタンスを受け入れるSegment
オブジェクトを格納するSegment
変数を作成します。これは線の先頭であるためです。segment
パラメータを指定してrenderFrontBufferedLayer
メソッドを呼び出し、onDrawFrontBufferedLayer
関数でコールバックをトリガーします。
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_DOWN -> {
// Ask that the input system not batch MotionEvent objects,
// but instead deliver them as soon as they're available.
view.requestUnbufferedDispatch(event)
currentX = event.x
currentY = event.y
// Create a single point.
val segment = Segment(currentX, currentY, currentX, currentY)
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
ACTION_MOVE
定数が検出されたときに MotionEvent
オブジェクトを処理する手順は次のとおりです。
ACTION_MOVE
定数の本体で、currentX
変数を格納するpreviousX
変数と、currentY
変数を格納するpreviousY
変数を作成します。MotionEvent
オブジェクトの現在のx
座標を保存するcurrentX
変数と、現在のy
座標を保存するcurrentY
変数を作成します。previousX
、previousY
、currentX
、currentY
の各パラメータを受け入れるSegment
オブジェクトを格納するSegment
変数を作成します。segment
パラメータを指定してrenderFrontBufferedLayer
メソッドを呼び出し、onDrawFrontBufferedLayer
関数でコールバックをトリガーして OpenGL コードを実行します。
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_MOVE -> {
previousX = currentX
previousY = currentY
currentX = event.x
currentY = event.y
val segment = Segment(previousX, previousY, currentX, currentY)
// Send the short line to front buffered layer: fast rendering
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
ACTION_UP
定数が検出されたときにMotionEvent
オブジェクトを処理するには、commit
メソッドを呼び出してonDrawDoubleBufferedLayer
関数の呼び出しをトリガーし、OpenGL コードを実行します。
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_UP -> {
frontBufferRenderer?.commit()
}
GLFrontBufferedRenderer
コールバック関数を実装する
FastRenderer.kt
ファイルで、onDrawFrontBufferedLayer
および onDrawDoubleBufferedLayer
コールバック関数は OpenGL コードを実行します。それぞれのコールバック関数の開始時に、次の OpenGL 関数は Android データを OpenGL ワークスペースにマッピングします。
GLES20.glViewport
関数は、シーンをレンダリングする長方形のサイズを定義します。Matrix.orthoM
関数は、ModelViewProjection
マトリックスを計算します。Matrix.multiplyMM
関数はマトリックス乗算を実行して Android データを OpenGL 参照に変換し、projection
マトリックスのセットアップを提供します。
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDraw[Front/Double]BufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
val bufferWidth = bufferInfo.width
val bufferHeight = bufferInfo.height
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
// Map Android coordinates to OpenGL coordinates.
Matrix.orthoM(
mvpMatrix,
0,
0f,
bufferWidth.toFloat(),
0f,
bufferHeight.toFloat(),
-1f,
1f
)
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
コードのその部分を設定すると、実際のレンダリングを行うコードに集中できます。onDrawFrontBufferedLayer
コールバック関数は、画面の小さな領域をレンダリングします。Segment
型の param
値を提供するため、単一のセグメントをすばやくレンダリングできます。LineRenderer
クラスは、線の色とサイズを適用するブラシ用の OpenGL レンダラです。
onDrawFrontBufferedLayer
コールバック関数を実装する手順は次のとおりです。
FastRenderer.kt
ファイルで、onDrawFrontBufferedLayer
コールバック関数を見つけます。onDrawFrontBufferedLayer
コールバック関数の本体で、obtainRenderer
関数を呼び出してLineRenderer
インスタンスを取得します。- 次のパラメータを指定して、
LineRenderer
関数のdrawLine
メソッドを呼び出します。
- 以前に計算された
projection
マトリックス。 Segment
オブジェクトのリスト。この場合は 1 つのセグメントです。- 行の
color
。
FastRenderer.kt
import android.graphics.Color
import androidx.core.graphics.toColor
class FastRenderer( ... ) {
...
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
obtainRenderer().drawLine(projection, listOf(param), Color.GRAY.toColor())
}
- アプリを実行すると、最小限のレイテンシで画面に描画できるようになります。ただし、
onDrawDoubleBufferedLayer
コールバック関数を引き続き実装する必要があるため、アプリで線が維持されません。
onDrawDoubleBufferedLayer
コールバック関数は、commit
関数の後に呼び出され、線の永続化を可能にします。このコールバックは、Segment
オブジェクトのコレクションを含む params
値を提供します。永続化のために、フロント バッファのすべてのセグメントがダブルバッファでリプレイされます。
onDrawDoubleBufferedLayer
コールバック関数を実装する手順は次のとおりです。
StylusViewModel.kt
ファイルでStylusViewModel
クラスを探し、Segment
オブジェクトの変更可能なリストを格納するopenGlLines
変数を作成します。
StylusViewModel.kt
import com.example.stylus.data.Segment
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
val openGlLines = mutableListOf<List<Segment>>()
private fun requestRendering(stylusState: StylusState) {
FastRenderer.kt
ファイルで、FastRenderer
クラスのonDrawDoubleBufferedLayer
コールバック関数を見つけます。- シーンをゼロからレンダリングできるように、
onDrawDoubleBufferedLayer
コールバック関数の本体でGLES20.glClearColor
メソッドとGLES20.glClear
メソッドで画面を消去し、viewModel
オブジェクトに線を追加して永続化します。
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
viewModel
オブジェクトのそれぞれの線を反復してレンダリングするfor
ループを作成します。
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
// Render the entire scene (all lines).
for (line in viewModel.openGlLines) {
obtainRenderer().drawLine(projection, line, Color.GRAY.toColor())
}
}
- アプリを実行します。画面上に描画できるようになり、
ACTION_UP
定数がトリガーされた後も線が保持されます。
7. モーション予測を実装する
タッチペンのコースを分析し、次のポイントの位置を予測してレンダリングするために挿入する androidx.input
ライブラリによって、レイテンシをさらに改善できます。
モーション予測を設定する手順は次のとおりです。
app/build.gradle
ファイルで、dependencies セクションのライブラリをインポートします。
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- [File] > [Sync Project with Gradle Files] をクリックします。
FastRendering.kt
ファイルのFastRendering
クラスで、motionEventPredictor
オブジェクトを属性として宣言します。
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
attachSurfaceView
関数で、motionEventPredictor
変数を初期化します。
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
onTouchListener
変数でmotionEventPredictor?.record
メソッドを呼び出して、motionEventPredictor
オブジェクトがモーション データを取得できるようにします。
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
次のステップでは、predict
関数を使用して MotionEvent
オブジェクトを予測します。ACTION_MOVE
定数を受信したときと MotionEvent
オブジェクトを記録した後に予測することをおすすめします。つまり、ストロークがいつ始まるかを予測する必要があります。
predict
メソッドを使用して、人工のMotionEvent
オブジェクトを予測します。- 現在および予測される x 座標と y 座標を使用する
Segment
オブジェクトを作成します。 frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment)
メソッドを使用して、予測されたセグメントの高速レンダリングをリクエストします。
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
...
frontBufferRenderer?.renderFrontBufferedLayer(segment)
val motionEventPredicted = motionEventPredictor?.predict()
if(motionEventPredicted != null) {
val predictedSegment = Segment(currentX, currentY,
motionEventPredicted.x, motionEventPredicted.y)
frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment)
}
}
...
}
予測されたイベントがレンダリング用に挿入され、レイテンシが改善されます。
- アプリを実行すると、レイテンシが改善されたことがわかります。
レイテンシを改善することで、タッチペンのユーザー エクスペリエンスがより自然になります。
8. 完了
おめでとうございます。プロのようにタッチペンを扱う方法を把握しました。
また、MotionEvent
オブジェクトを処理して圧力、向き、傾斜に関する情報を抽出する方法を学習しました。また、androidx.graphics
ライブラリと androidx.input
ライブラリの両方を実装して、レイテンシを改善する方法についても学習しました。これらの機能強化は、よりオーガニックなタッチペン体験を実現します。