たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 8918
どうもこんばんわ。
パイセンの犯人捕まったそうですね。全然関係ない枠で知った。
(テレビにパイセン映し出されたの普通に面白すぎる)
ニコ生でパイセンの放送何回か見たことあるけど、
パイセンでもちゃんと警察動いててなんか凄いなーって(ひどい言い方だよ)
そんなパイセンを忘れないようにここに書いておくことにします。 → すまん前科が多すぎて忘れられそうにない。いつ狙われてもおかしくないような人だったし、、
2018 年ってもう 6 年も前になるの・・・えぇ
懐かしすぎる、
ニコ生年齢あんけ in パイセンの枠
2018 年くらいらしい
2020 年にニコ生戻ってきて?やってたそうだけど、生前どこでやってたかはわからん。
FC2
とかもBAN
とかなんとか。
それとこれとは関係ないのですが、
(決してパイセンがドラッグやって捕まったからドラッグアンドドロップに繋がったわけではない)
Jetpack Compose
でテキストとかファイルの、ドラッグアンドドロップ機能がつけられるようになったみたいなので、
試してみることにします。
ドラッグアンドドロップ自体はAndroid 7
くらいからありますが、
Android 14 でパワーアップしたのでそれも紹介。あんまり盛り上がってなくて悲しい。
なまえ | あたい |
---|---|
Android Studio | Android Studio Jellyfish 2023.3.1 Patch 1 |
Jetpack Compose | 2024.05.00 |
minSdk | 24 (ドラッグアンドドロップが Android 7 以降?) |
テキストをドラッグアンドドロップで貼り付けるとかは、そんなに難しくない。
画像とか、バイナリデータをやり取りしたい場合は一気に面倒になる。
まずは簡単な、テキストをやり取りしてみることにします。
送信と受信が両方試せるように、ドラッグアンドドロップの開始、終了をそれぞれ置きました。
処理はこのあと書きます
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "ドラッグアンドドロップ") })
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
DragAndDropSendContainer(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
)
DragAndDropReceiveContainer(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
)
}
}
}
@Composable
private fun DragAndDropSendContainer(modifier: Modifier = Modifier) {
val inputText = remember { mutableStateOf("") }
OutlinedCard(modifier = modifier) {
Column(modifier = Modifier.padding(10.dp)) {
Text(text = "ドラッグアンドドロップ 送信側")
OutlinedTextField(
value = inputText.value,
onValueChange = { inputText.value = it }
)
Box(
modifier = Modifier
.size(100.dp)
.border(1.dp, MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(text = "長押し!")
}
}
}
}
@Composable
private fun DragAndDropReceiveContainer(modifier: Modifier = Modifier) {
val receiveText = remember { mutableStateOf("") }
OutlinedCard(modifier = modifier) {
Column(modifier = Modifier.padding(10.dp)) {
Text(text = "ドラッグアンドドロップ 受信側")
Box(
modifier = Modifier
.size(200.dp)
.border(1.dp, MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(text = "ここに持ってくる")
}
HorizontalDivider()
Text(text = receiveText.value)
}
}
}
ClipData
にドラッグアンドドロップで送りたいデータを詰め込みます。
あとは、ドラッグアンドドロップしたいBox()
とかのコンポーネントのModifier
へ、dragAndDropSource { }
することで、長押し時に移動できるようになります。
ClipData.newPlainText
の第1引数、"Text"
の部分、ドキュメントにはユーザーへ表示する値とかなんとか書いてありますが、
私もそんなUI
見たこと無いので、おそらく開発側が自由に決めていい値のはずです。
Box(
modifier = Modifier
.size(100.dp)
.border(1.dp, MaterialTheme.colorScheme.primary)
.dragAndDropSource {
detectTapGestures(onLongPress = {
// value に文字をいれる
val clipData = ClipData.newPlainText("Text", inputText.value)
startTransfer(DragAndDropTransferData(clipData = clipData, flags = View.DRAG_FLAG_GLOBAL))
})
},
contentAlignment = Alignment.Center
) {
Text(text = "長押し!")
}
まずはドラッグアンドドロップの受信コールバックを用意します。
ドラッグアンドドロップの操作が開始した、領域の中に入った、出た、ドラッグアンドドロップが終了した。等を知ることが出来ます。
ドラッグアンドドロップで投げ込まれたときのコールバックは必須で、残りの開始とか終了とかは自由に。
今回はUI
側に反映させたいので、コールバックで受け取ることにします。
val isProgressDragAndDrop = remember { mutableStateOf(false) }
val callback = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
// TODO この後すぐ!
return true
}
override fun onStarted(event: DragAndDropEvent) {
super.onStarted(event)
isProgressDragAndDrop.value = true
}
override fun onEnded(event: DragAndDropEvent) {
super.onEnded(event)
isProgressDragAndDrop.value = false
}
}
}
次に、ドラッグアンドドロップを受信したいコンポーネントのModifier
で、dragAndDropTarget
を呼び出して、受け取ることを示します。
引数ですが、受け取れるデータの種類(MIME-Type
とか見れる)と、さっき作ったコールバックです。
Box(
modifier = Modifier
.size(200.dp)
.border(1.dp, MaterialTheme.colorScheme.primary)
.background(
// ドラッグアンドドロップ操作中はコンテナの背景色を変化
color = if (isProgressDragAndDrop.value) {
MaterialTheme.colorScheme.primary.copy(0.5f)
} else {
Color.Transparent
}
)
.dragAndDropTarget(
shouldStartDragAndDrop = { event ->
// 受け取れる種類。とりあえずテキスト
event
.mimeTypes()
.contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
},
target = callback
),
contentAlignment = Alignment.Center
) {
Text(text = "ここに持ってくる")
}
最後に、ドラッグアンドドロップで投げ込まれたデータのパースをします。
適当に、最初のClipData
を取り出して、テキストとして表示するようにしてみます。
ClipData
の詳細(MIME-Type
とかは)ClipDescription
に入っています。
マルチプラットフォームを意識しているのか、toAndroidDragEvent()
で取り出す必要があります。
val receiveText = remember { mutableStateOf("") }
val isProgressDragAndDrop = remember { mutableStateOf(false) }
val callback = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
val androidEvent = event.toAndroidDragEvent()
// 最初のデータを取り出します
val mimeType = androidEvent.clipDescription.getMimeType(0)
val text = androidEvent.clipData.getItemAt(0).text
receiveText.value = """
受信したデータ
MIME-Type: $mimeType
text: $text
""".trimIndent()
return true
}
override fun onStarted(event: DragAndDropEvent) {
super.onStarted(event)
isProgressDragAndDrop.value = true
}
override fun onEnded(event: DragAndDropEvent) {
super.onEnded(event)
isProgressDragAndDrop.value = false
}
}
}
テキストボックスに適当に文字を入れて、「長押し!」を押すとドラッグアンドドロップが始まって、
それと同時に、受信先のコンポーネントの背景色が変化して、受信先で指を離すと中身が表示されるはずです。どうだろ???
また、アプリを超えても利用できることがこれで分かるはず。
おんなじアプリを2つ作って、アプリを超えてドラッグアンドドロップしてみましたが、これもちゃんと動きます。
もちろん、ドラッグアンドドロップに対応したアプリへの送信、受信も出来ます。
https://9to5google.com/2023/05/19/android-14-drag-and-drop/
iOS
にはすでにあったそうですが、Android
にも来ました。
ドラッグアンドドロップでアプリを超えたい場合、マルチウィンドウ(画面分割)する必要がなくなりました。
ドラッグアンドドロップ中でも、その指を離さなければ、別の指でホーム画面に戻って別アプリを起動したり、アプリを切り替えたり出来るようになりました。
そしてドラッグアンドドロップ中の指を離せばそのアプリに貼り付けられます。
画面分割するほど画面が大きくない場合に便利そう。
もちろんコード上でなにかする必要はありません。
画像を入れる場合、ClipData
へ画像は入れられず、バイナリを共有するための仕組みを使い、Uri
を発行してもらい、そのUri
をClipData
へ入れる必要があります。
画像のファイルパスを入れれば良いとか、そういう話ではないのでかなり面倒くさい。
大雑把にこんな感じ。
Android
のIntent
のサイズ上限が出来たあたり(私は知らない)で、このFileProvider
の知見が結構あるので助かる。。。
https://developer.android.com/reference/androidx/core/content/FileProvider
アプリの固有ストレージ内のファイル(Context#getExternalFileDir()
内のファイルとか)をUri
を経由して外部へ公開できるやつ。
ContentProvider
をいい感じに実装してくれたものになります。ので、頑張ればContentProvider
でも同じことが出来るはず??(何もわからない)
FileProvider
が存在することをAndroidManifest.xml
に書くContext#getExternalFileDir()
とかContext#getFilesDir()
に保存する
getExternalFileDir
は/sdcard/Android/data/{アプリケーションID}
Android Studio
のDevice Explorer
機能でもAndroid
標準ファイラー(com.android.documentsui
)でも見れます
Files by Google
のことではありませんgetFilesDir
は/data/data/{アプリケーションID}
root
を取らない限り見れませんFileProvider
へ登録する
Uri
がもらえるUri
をClipData
に入れるClipData
からUri
を取り出しますActivityCompat.requestDragAndDropPermissions
を呼び出します
Uri
へアクセスできませんUri
が使えるようになりますCoil
とかGlide
にUri
を渡せば表示できるのでは無いでしょうか!?!?
BitmapFactory
でUri
からBitmap
を作ります...Coil
、Glide
を使うべきです)。面倒事を全部やってくれるので他のことに集中できます。ちなみに、画像とか言っていますが別に画像じゃなくても任意のバイナリをやり取りできるはず。
送る側はFileProvider
の用意が必要なので面倒。
まずはres/xml
の中に、file_path.xml
を作ります。
多分名前は何でもいいんですけど、ドキュメント通りに行こうと思います。
中身です。
<external-files-path
はContext#getExternalFileDir()
の中にあるファイルを共有するためですね。
Context#getFilesDir()
の場合は<files-path
にする必要があります。
https://developer.android.com/reference/androidx/core/content/FileProvider
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="images" path="images/"/>
</paths>
次に、FileProvider
を継承したクラスを作ります。
コンストラクタの引数はさっき作ったxml
です。
import androidx.core.content.FileProvider
class DragAndDropFileProvider : FileProvider(R.xml.file_paths)
最後に、AndroidManifest
にFileProvider
を登録してFileProvider 編
は終了。
<provider
android:name=".DragAndDropFileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
レイアウトを作ります。
あと、画像を用意するのが面倒なので、自身のアプリのアイコンをBitmap
で取って、ファイルに保存することにしようと思います。
自身のアイコン画像の取得はこんな感じです。
@Composable
private fun ImageDragAndDropSendContainer(modifier: Modifier = Modifier) {
val context = LocalContext.current
// 自分のアプリのアイコン
val bitmap = remember { mutableStateOf<Bitmap?>(null) }
LaunchedEffect(key1 = Unit) {
// Bitmap を取り出す
val iconDrawable = context.packageManager.getApplicationIcon(context.packageName)
bitmap.value = iconDrawable.toBitmap()
}
OutlinedCard(modifier = modifier) {
Column(modifier = Modifier.padding(10.dp)) {
Text(text = "画像ドラッグアンドドロップ 送信側")
// 画像を表示、ドラッグアンドドロップも兼ねて
if (bitmap.value != null) {
Image(
modifier = Modifier,
bitmap = bitmap.value!!.asImageBitmap(),
contentDescription = null
)
}
}
}
}
MainScreen()
に設置するのも忘れないでね。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "ドラッグアンドドロップ") })
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState()) // スクロールしたい
) {
// 省略...
HorizontalDivider()
// 今作ったやつ
ImageDragAndDropSendContainer(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
)
}
}
}
どうでしょう?
次はBitmap
をgetExternalFileDir
に保存して、Uri
を取得します。
先述のとおり、getExternalFileDir
は、見ようと思えば見れるフォルダなので、もしまずいようならgetFilesDir
を選んでください。(xml
も直してね)
Bitmap
の保存先をgetExternalFileDir
の中に作ります。
/images
フォルダを先に作ってますが、これはfile_paths.xml
でpath=""
をimages
にしたからですね。
getUriForFile
でUri
が出来ます。エラーの場合は例外が投げられます。
// 自分のアプリのアイコン
val bitmap = remember { mutableStateOf<Bitmap?>(null) }
// 共有で使える Uri
val shareUri = remember { mutableStateOf<Uri?>(null) }
LaunchedEffect(key1 = Unit) {
// Bitmap を取り出す
val iconDrawable = context.packageManager.getApplicationIcon(context.packageName)
bitmap.value = iconDrawable.toBitmap()
// ファイル getExternalFilesDir の中に作って保存する
// images フォルダの images は、 file_paths.xml の path="" が images だからです。
val imageFolder = context.getExternalFilesDir(null)!!.resolve("images").apply { mkdir() }
val imageFile = imageFolder.resolve("${System.currentTimeMillis()}.png").apply { createNewFile() }
imageFile.outputStream().use { outputStream ->
bitmap.value!!.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
// FileProvider に登録して Uri を取得
shareUri.value = FileProvider.getUriForFile(context, "${context.packageName}.provider", imageFile)
// 生成される Uri はこんな感じ
// content://io.github.takusan23.jetpackcomposefiledraganddrop.provider/images/1716833782696.png
}
テキストと同じ用にdragAndDropSource
を呼び出して、ドラッグアンドドロップで掴めるようにします。
テキストとは違い、newUri
を使って、Uri
を入れたClipData
を作ります。MIME-Type
とかはUri
を使ってMediaStore
に問い合わせて自動でセットしてくれるらしい。
それから、flags
ですが、Uri
に読み取り権限を付与するためのView.DRAG_FLAG_GLOBAL_URI_READ
フラグを立てておく必要があります。
FileProvider
のドキュメントにはIntent
の場合しか書かれてませんが、ClipData
の場合も権限を付与しないと、ドラッグアンドドロップ先で読み取りできません。
他にも書き込み権限とかあります。View
のドキュメント参照。
View
のドキュメントクソ重いので開くときは注意→ https://developer.android.com/reference/android/view/View#DRAG_FLAG_GLOBAL_URI_READ
Image(
modifier = Modifier.dragAndDropSource {
detectTapGestures(onLongPress = {
// ClipData へ Uri を
val nonnullUri = shareUri.value ?: return@detectTapGestures
val clipData = ClipData.newUri(context.contentResolver, "image_uri", nonnullUri)
startTransfer(
DragAndDropTransferData(
clipData = clipData,
flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ // Uri 読み取り可能ですよのフラグを or で立てておく。ビット演算
)
)
})
},
bitmap = bitmap.value!!.asImageBitmap(),
contentDescription = null
)
これで、送信側は完成のはずです。
Google Keep
とかにドラッグアンドドロップ出来るはず!!!!!できた!?!?!?!?
こっちはいくらか簡単です。
受け取る側くらいは考えてもいいんじゃないでしょうか?
mimeTypes()
のチェックは、画像用に直す必要があります。これは画像以外のバイナリ(動画とか)の場合もそうですが。
今回は画像だけなので、MIME-Type
がimage/
で始まっているかを見ています。
Uri
を受け取った後、ActivityCompat.requestDragAndDropPermissions
を呼び出す必要があります。
これを呼び出さないと、Uri
を使って画像へアクセスしようとしても、ブロックされます。Uri
が用済みになったら、release()
してあげましょう。
requestDragAndDropPermissions
した後はUri
を使ってアクセス出来るようになるのでcontext.contentResolver.openInputStream
でデータを読み出して、
Bitmap
にしています。先述のとおり、Glide
やCoil
のライブラリが使えるなら使うべきです。今回はこのためだけにわざわざ入れないですが。。。
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ImageDragAndDropReceiveContainer(modifier: Modifier = Modifier) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
// 受け取った画像
val receiveBitmap = remember { mutableStateOf<Bitmap?>(null) }
val isProgressDragAndDrop = remember { mutableStateOf(false) }
val callback = remember {
object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
val androidEvent = event.toAndroidDragEvent()
// DragAndDropPermission を作らないと、Uri を使ったアクセスが出来ません
val dragAndDropPermissions = ActivityCompat.requestDragAndDropPermissions(context as Activity, androidEvent)
// 最初のデータを取り出します
val receiveUri = androidEvent.clipData.getItemAt(0).uri
// UI 処理なので一応コルーチンで
scope.launch(Dispatchers.IO) {
// Uri から Bitmap を作る
// Glide や Coil が使えるなら使うべきです
receiveBitmap.value = context.contentResolver.openInputStream(receiveUri)
?.use { inputStream -> BitmapFactory.decodeStream(inputStream) }
// とじる
dragAndDropPermissions?.release()
}
return true
}
override fun onStarted(event: DragAndDropEvent) {
super.onStarted(event)
isProgressDragAndDrop.value = true
}
override fun onEnded(event: DragAndDropEvent) {
super.onEnded(event)
isProgressDragAndDrop.value = false
}
}
}
OutlinedCard(modifier = modifier) {
Column(modifier = Modifier.padding(10.dp)) {
Text(text = "画像ドラッグアンドドロップ 受信側")
// ドラッグアンドドロップを待ち受ける
Box(
modifier = Modifier
.size(300.dp)
.border(1.dp, MaterialTheme.colorScheme.primary)
.background(
// ドラッグアンドドロップ操作中はコンテナの背景色を変化
color = if (isProgressDragAndDrop.value) {
MaterialTheme.colorScheme.primary.copy(0.5f)
} else {
Color.Transparent
}
)
.dragAndDropTarget(
shouldStartDragAndDrop = { event ->
// 受け取れる種類。とりあえず
val supportedMimeTypePrefix = "image/"
event
.mimeTypes()
.all { receiveMimeType -> receiveMimeType.startsWith(supportedMimeTypePrefix) }
},
target = callback
)
) {
// 画像表示
if (receiveBitmap.value != null) {
Image(
bitmap = receiveBitmap.value!!.asImageBitmap(),
contentDescription = null
)
}
}
}
}
}
これをMainScreen()
に置いて完成!
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
Scaffold(
topBar = {
TopAppBar(title = { Text(text = "ドラッグアンドドロップ") })
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
// 省略...
ImageDragAndDropSendContainer(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
)
ImageDragAndDropReceiveContainer(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
)
}
}
}
どうだろ???
画像も受け取れるアプリが出来ましたか???
https://github.com/takusan23/JetpackComposeFileDragAndDrop
ありざいす