🔥
Canvasを使った手書き機能
UIの基本部分
UI作っていきます。
- Canvas作って、Boxでラップ
- Boxのmodifierを領域いっぱいまで広げ、背景色を黄色、枠線も引いておく
背景色と枠線は手書きできる場所を明確にするための処置なので、あとで消すかも。 - Canvasも領域いっぱいまで広げておく
@Composable
fun NumberCanvas(modifier: Modifier = Modifier) {
Box(
modifier=modifier
.fillMaxSize()
.background(Color.Yellow)
.border(2.dp, Color.Black)
) {
Canvas(modifier = Modifier.fillMaxSize()) {
}
}
}
Boxにジェスチャー追加
ModifierのpointerInputでジェスチャーは設定できる。
今回はドラッグをしたときに線を描画させたいので、detactDragGesturesを使う。
ジェスチャーの中のonDrag系のイベントは一旦全部空にしておく。
ViewModel作ってから設定。
NumberCanvas
//省略
Box(
modifier=modifier
.fillMaxSize()
.background(Color.Yellow)
.border(2.dp, Color.Black)
+ .pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = { },
+ onDrag = { },
+ onDragEnd = { },
+ onDragCancel = { },
+ )
}
) {
//省略
ViewModelを作る
めちゃめちゃしんどかった。
全然うまくいかなくてキーってなった😵💫
PathDataデータクラス作成
描画する線を管理しよう!とはりきって作りましたが・・・。
PathData
//描画する線のパス、色、太さ
data class PathData(
val path: Path = Path(),
val color: Color = Color.Black,
val strokeWidth: Float = 24f
)
色と線の太さなんて固定だし、pathがあればいいだけだった。
データクラスなんていらんじゃんってなった。
でも、作っちゃったし、これありきでコーディングしたし。
つらかったからもう変えたくない。
CalcViewModelクラス作成→変数設定
各変数の意味はコメント参照
rawPathsだけ、PathData(いらないデータクラス)にせず、直接Offset型にしました。
色とか変えたくなったらどうしよう💦
CalcViewModel
class CalcViewModel:ViewModel() {
private val _paths = mutableStateListOf<PathData>() //確定した線の一覧(リスト)
val paths: List<PathData> get()= _paths //UI公開用
private var rawPaths = mutableListOf<Offset>() //描画中の線の一覧(リスト)まだ未確定
var currentPath by mutableStateOf(PathData()) //現在の線
//省略
パスの再構築
最初はドラッグ中のパスをcurrentPathで管理していたのだけど、うまくいかなくて、Path()を新しく作り直すして、currentPathに突っ込むという関数を作ることになった。
CalcViewModel
private fun rebuildPath() {
val path= Path().apply {
if(rawPaths.isNotEmpty()){
moveTo(rawPaths.first().x, rawPaths.first().y) //最初の点
for (i in 1 until rawPaths.size) { //リストの要素数処理
lineTo(rawPaths[i].x, rawPaths[i].y) //前回の点と現在の点を線する
}
}
}
currentPath = PathData(
path = path, //新しいPath()インスタンスを生成→再コンポーズする
color = currentPath.color,
strokeWidth = currentPath.strokeWidth
)
}
ドラッグ開始時の処理
CalcViewModel
fun onDragStart(offset: Offset){
rawPaths.clear() //描画中だったパスのリストをクリア
rawPaths.add(offset) //新しい線をリストに追加
rebuildPath() //パスの再構築(moveToが描画)
}
ドラッグ中の処理
CalcViewModel
//ドラッグ中のパスの更新
fun onDrag(offset: Offset){
rawPaths.add(offset) //描画中の線をリストにどんどん追加
rebuildPath() //パスの再構築(lineToにより線が伸びていく)
}
ドラッグ終了時の処理
CalcViewModel
fun onDragEnd() {
_paths.add(currentPath) //描いた線をどんどん確定リストに追加
rawPaths.clear() //必要がなくなったドロップ中リストをクリア
currentPath = PathData() //現在の線もクリア
}
CalcViewModel.ktのコード
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.lifecycle.ViewModel
class CalcViewModel:ViewModel() {
private val _paths = mutableStateListOf<PathData>() //確定した線の一覧(リスト)
val paths: List<PathData> get()= _paths //UI公開用
private var rawPaths = mutableListOf<Offset>() //描画中の線の一覧(リスト)まだ未確定
var currentPath by mutableStateOf(PathData()) //現在の線
//ドラッグ開始時のパス
fun onDragStart(offset: Offset){
rawPaths.clear() //描画中だったパスのリストをクリア
rawPaths.add(offset) //新しい線をリストに追加
rebuildPath() //パスの再構築(moveToが描画)
}
//ドラッグ中のパスの更新
fun onDrag(offset: Offset){
rawPaths.add(offset) //描画中の線をリストにどんどん追加
rebuildPath() //パスの再構築(lineToにより線が伸びていく)
Log.d("calk", "onDrag: ${currentPath.path}")
}
//ドラッグが終わった時のパスのリストへの追加
fun onDragEnd() {
_paths.add(currentPath) //描いた線をどんどん確定リストに追加
rawPaths.clear() //必要がなくなったドロップ中リストをクリア
currentPath = PathData() //現在の線もクリア
}
//パスの再構築
private fun rebuildPath() {
val path= Path().apply {
if(rawPaths.isNotEmpty()){
moveTo(rawPaths.first().x, rawPaths.first().y) //最初の点
for (i in 1 until rawPaths.size) { //リストの要素数処理
lineTo(rawPaths[i].x, rawPaths[i].y) //前回の点と現在の点を線する
}
}
}
currentPath = PathData(
path = path, //新しいPath()インスタンスを生成→再コンポーズする
color = currentPath.color,
strokeWidth = currentPath.strokeWidth
)
}
}
//描画する線のパス、色、太さ
data class PathData(
val path: Path = Path(),
val color: Color = Color.Black,
val strokeWidth: Float = 24f
)
UIとviewModelをつなぐ
ジェスチャーを完成させる
NumberCanvasコンポーザブル関数に、CalcViewModelへの参照を追加し
- 各変数(viewModel、paths、currentPath)の宣言
viewModelだけおまじないのようにNumberCalcの引数に入れてます。 - onDrag系のイベントはViewModelの同名関数と、それに伴う引数をセット
onDragはchangeとammountがあるのですが、changeしか使いません
NumberCanvas
@Composable
fun NumberCanvas(
modifier: Modifier = Modifier,
+ viewModel: CalcViewModel = viewModel(),
) {
+ val paths = viewModel.paths
+ val currentPath = viewModel.currentPath
Box(
modifier=modifier
.fillMaxSize()
.background(Color.Yellow)
.border(2.dp, Color.Black)
.pointerInput(Unit) {
detectDragGestures(
+ onDragStart = { offset ->
+ viewModel.onDragStart(offset)
+ },
+ onDrag = { change, _ ->
+ viewModel.onDrag(change.position)
+ },
+ onDragEnd = { viewModel.onDragEnd() },
+ onDragCancel = { },
+ )
}
) {
//省略
Canvasに描画部分をいれる
ドラッグ中にリアルタイムで描画させる部分と、ドラッグが終わった時に一気に描画させる部分の2つを追加します。
- ドラッグ中の描画
→現在のパスで線を描画 - ドラッグが終わってからの描画
→今までのパスを集めたリストから線を描画
NumberCanvas
//省略
Canvas(modifier = Modifier.fillMaxSize()) {
//ドラッグ中のパスの描画
drawPath(
path = currentPath.path,
color = currentPath.color,
style = Stroke(currentPath.strokeWidth, cap = StrokeCap.Round)
)
//確定したパスの描画
paths.forEach { path ->
drawPath(
path = path.path,
color = path.color,
style = Stroke(path.strokeWidth, cap = StrokeCap.Round)
)
}
}
Discussion