Canvasを使って画面にスタンプを押す
スタンプ用の画像用意
pngでOK。
res>drawableに突っ込む
bitmapを保持する変数宣言
- context・・・resourcesはそのままでは使えないので、アプリのcontextから取得
- bitmap・・・BitmapFactoryというAPIを使う
rememberを使って画像を保持する(毎回デコードさせないため)
画像の大きさをリサイズして100×100で読み込み - stampPosition・・・タップ位置(位置履歴管理のためリストにする)
import
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import com.rururi.noutore.R
import androidx.core.graphics.scale
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
val context = LocalContext.current
val bitmap = remember {
BitmapFactory.decodeResource(context.resources, R.drawable.taiyo).scale(320,320) }
var stampPosition by remember { mutableStateOf(listOf<Offset>()) } //stampの位置をリストで保持
drawImageでBoxに描画
- 今回はCanvas使わずBoxに描画させる方法で検討
- modifierのpointerInputでタップ位置を取得
- 履歴管理のため位置をリストにどんどん追加
//省略
Box(
modifier = modifier
.fillMaxSize()
.pointerInput(Unit) { //入力位置取得
detectTapGestures { offset ->
stampPosition += offset //タップした位置をリストに追加
}
}
) { /*ここにイメージを入れる*/}
//省略
スタンプをImageで表示
- forEachを使ってstampPositionリストを全部Imageとして出力
- 出力位置はoffsetのintOffsetを使う
intOffsetとは?offsetの位置をpx単位(dpではない)で表すもの
offsetはFloatなので、toIntで変換する必要がある。 - タップした位置がスタンプのちょうど真ん中に来るよう、調整
//省略
Box(
//省略
) {
//スタンプ表示
stampPosition.forEach { position ->
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.offset { //stampの位置を指定 IntOffset型で渡す
IntOffset(
(position.x - bitmap.width/2).toInt(),
(position.y - bitmap.height/2).toInt()
)
}
)
}
}
}
次はBottmBar作って、他のスタンプも押せるようにしよう
ボトムバーを作る
ボトムバーを作って画像を3種類、消しゴム代わりの×を追加。
アイコン替わりなので、Imageを使う。
大きさはmodifierで64.dpに固定した。
クリッカブルを設定して。。。。
うーんw コードが長い😱 リストにしてforEachにしよう💦
ViewModelも作っておこう。。。
HomeScreenと同じフォルダにHomeViewModel.ktを作成。
//ボトムバーのリストの型用
data class BottomBarType(
@DrawableRes val icon: Int,
val label: String,
val size: Dp,
val isSelected: Boolean = false,
)
//ボトムバーのリスト
object BottomBarIcons {
val bottomBarIcons = listOf(
BottomBarType(
R.drawable.taiyo,"taiyo",64.dp,true
),
BottomBarType(
R.drawable.kumo,"kumo",64.dp,false
),
BottomBarType(
R.drawable.kaminari,"kaminari",64.dp,false
),
)
}
リストを作ったので、BottomBar.ktを新規作成し、コードは以下のようにしました。
- forEachでImageを3つ並べる
- Modifierのclickableを空で設定
クリックはできるけど、アクションしない状態 - ×アイコン(消しゴムの代わり)はImageではなく、Iconなので、個別に設定
@Composable
fun BottomBar(modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
bottomBarIcons.forEach {
Image(
painter = painterResource(id = it.icon),
contentDescription = it.label,
modifier = Modifier.size(it.size.dp)
.clickable { }
)
}
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null,
modifier = Modifier.size(64.dp)
.clickable {}
)
}
}
ここまでで、一旦実行。
タップすると太陽がポコポコスタンプされて気持ちいい^^
まぁ、太陽しかスタンプできない😅
アイコンを選択でスタンプの種類を変更
次はViewModelで現在選択されているアイコンを管理して、選択中のイメージでスタンプされるよう変更しよう。
viewModelを使うときは依存関係追加しないといけない。
面倒くさいから、デフォルトで入れておいてほしい><
- ライブラリ入れる
[versions]
lifecycle-viewmodel-compose = "2.8.7"
[libraries]
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
- gradle設定
dependencies {
//省略
//viewmodel
implementation(libs.androidx.lifecycle.viewmodel.compose)
Sync Now!!!!
よく忘れるので、忘れないように大声で叫びました。
HomeViewModel作成
- 画面(UI)の現状管理用にHomeUiStateのデータクラスを作成。
_uiState(ViewModel用)とuiState(UI用)の2つの変数にHlomeUiStateを監視付きで突っ込む。 - 次に押したスタンプの情報を入れておくStampDataデータクラスも作成。
- お天気アイコン(画像)をタップしたら、_uiStateのcurrentImageを変更する関数を作成
//現在のスタンプ
data class HomeUiState(
val currentImage: Int = R.drawable.taiyo,
val stamps: List<StampData> = emptyList()
)
//スタンプ情報
data class StampData(
val position: Offset,
@DrawableRes val icon: Int,
)
class HomeViewModel: ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
//アイコン変更(引数にクリックしたイメージのResIdを渡してもらう)
fun changeIcon(icon: Int) {
_uiState.update{
it.copy(currentImage = icon)
}
}
//スタンプ追加
fun addStamp(position: Offset) {
_uiState.update {
it.copy(stamps = it.stamps + StampData(position, uiState.value.currentImage))
}
}
}
BottomBarのお天気アイコンをクリックしたときの処理
- 引数にviewModelを追加。
viewModel: HomeViewModel = viewModel()←こいつがエラーになる
自動的にインポートしてくれないので、自分でインポート💦
import androidx.lifecycle.viewmodel.compose.viewModel
なんでインポートしてくれないの? - clicableにviewModelのchangeIcon関数を設定。引数は現在選択されているイメージのicon。
@Composable
fun BottomBar(
modifier: Modifier = Modifier,
+ viewModel: HomeViewModel = viewModel()
) {
Row(modifier = modifier.fillMaxWidth()) {
bottomBarIcons.forEach {
Image(
painter = painterResource(id = it.icon),
contentDescription = it.label,
modifier = Modifier.size(it.size)
+ .clickable { viewModel.changeIcon(it.icon) } //アイコンを変更する処理
)
}
//省略
HomeScreen画面をタップしたときの画像描画処理
記事ぐらい長くなってきた(*´Д`)
- HomeScreenにも引数にviewModel変数を追加
- viewModelに設定したuiState変数も設定。(画面の状態を保持するため)
- 元々設定してあったstampPositionの代わりに、StampDataをリストで保持するstamps変数を設定。
stampPositionは位置しか持っていないけど、stampsは位置とスタンプの種類を持っているので! - タップのたびに位置情報とスタンプの種類をstampsに追加。
- bitmapを最初に読み込んで使いまわしていたけど、タップのたびに呼び出すよう変更
- 今回の変更は緑で示した部分
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
+ viewModel: HomeViewModel = viewModel()
) {
+ val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
// val bitmap = remember {
// BitmapFactory.decodeResource(context.resources, uiState.currentImage).scale(320,320) }
Box(
modifier = modifier
.fillMaxSize()
.pointerInput(Unit) { //入力位置取得
detectTapGestures { offset ->
+ viewModel.addStamp(offset) //タップした位置をリストに追加
}
}
) {
//スタンプ表示
+ uiState.stamps.forEach { stamp ->
+ val bitmap = BitmapFactory.decodeResource(context.resources, stamp.icon).scale(320,320)
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.offset { //stampの位置を指定 IntOffset型で渡す
IntOffset(
(stamp.position.x - bitmap.width/2).toInt(),
(stamp.position.y - bitmap.height/2).toInt()
)
}
)
}
}
}
Discussion