- 投稿日:2020-02-14T19:57:21+09:00
Android 10 (API 29)対応で画像や動画ファイルをサーバに送信させたときの話
targetSdkVersionを29したときに画像のサーバ送信で苦戦したので備忘録として
問題点
https://developer.android.com/training/data-storage/files/external-scoped
「ファイル アクセスに必要なパーミッション」の項にあるこの部分
READ_EXTERNAL_STORAGE パーミッションが付与されていたとしても、外部ストレージ デバイスの未加工ファイル システム ビューにアクセスするアプリの場合、アクセスできるのは、アプリ固有ディレクトリに限られます。アプリが未加工ファイル システム ビューを使用してアプリ固有ディレクトリの外部にあるファイルを開こうとすると、エラーが発生します。
どうやら
「自分が作成した画像(動画)以外は使っちゃだめよ!
ユーザが画像を選択後に、内部的に別のアプリがその画像を加工してたとしたら大変でしょ?(意訳」
という事らしい一応逃げ道として
アプリが対象範囲別ストレージに完全に対応するまでは、アプリのターゲット SDK レベルや requestLegacyExternalStorage マニフェスト属性に基づいて、一時的に対象範囲別ストレージ機能を無効にすることができます。
とあるがいつまで使えるかはよくわからん
要件まとめ
・targetSdkVersionを29に変更する
・画像をサーバーに送れるようにする
・アプリは複数ある(minSdkVersionはまちまち
)
・すぐに更新しなければならなくなる事を考えて requestLegacyExternalStorage は使いたくない解決策
選択したファイルを自アプリのキャッシュに一度保存してそかからサーバにアップする
苦戦した事
・minSdkVersionがまちまちのため、使える手段が少なかった(Paths も Files も使えない)
・FileInputStreamしただけでも怒られる
・1Byteづつコピーするのは時間がかかりすぎる実際の対応
URIからInputStreamを作ってそっからChannelをかませて自アプリのキャッシュDirに出力した
実際の実装(一部抜粋)
cacheFile = new File(activity.getCacheDir(), fileName); try { InputStream inputStream = activity.getContentResolver().openInputStream(fileUri); FileOutputStream fOutputStream = new FileOutputStream(cacheFile); try { fOutputStream.getChannel().transferFrom(Channels.newChannel(inputStream), 0, Long.MAX_VALUE); } finally { fOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); } return cacheFile.getPath();最後に
色々長時間ごにょごにょしてた気がするけど終わってみれば「こんなものですむのか・・・」といったエンジニアあるある
何かご指摘事項等ありましたらお手柔らかにお願いします
- 投稿日:2020-02-14T19:23:28+09:00
Build React Native Custom Checkbox Component for Android and iOS
In this tutorial, we are going to learn how to create a custom Checkbox component in React Native application for Android and iOS platforms. We will learn step by step how to get Multiple checkboxes values on button click using React Native APIs.
READ MORE TO CLICKHERE
https://www.positronx.io/build-react-native-custom-checkbox-for-android-and-ios/
- 投稿日:2020-02-14T19:13:36+09:00
【Android】テキストエディタ(EditText)にアンドゥ・リドゥ機能を付ける
概要
Androidアプリの
EditText
で、アンドゥ(元に戻す)・リドゥ(やり直し)ができるようにしたかった。しかし、標準ライブラリではアンドゥ・リドゥ機能は用意されていないらしい(多分)。
アンドゥ・リドゥを実装する方法を調べた。アンドゥ・リドゥを実現する方法
android.text
パッケージにTextWatcher
というインターフェースが用意されているため、それを利用して実装する。
TextWatcher
とはWhen an object of this type is attached to an Editable, its methods will be called when the text is changed.
Editable
にこの型のオブジェクトが付いているとき、テキストが変更されたときにTextWatcher
のメソッドが呼ばれる。
呼ばれるメソッドは以下の3つである。TextWatcher
はインターフェースであるため、3つともオーバーライドする必要がある。
beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int)
s
中のstart
番目から始まるcount
個の文字が、長さafter
の新しいテキストに置き換えられようとしているときに呼ばれるメソッドonTextChanged(s: CharSequence, start: Int, before: Int, count: Int)
s
中のstart
番目から始まるcount
個の文字が、長さbefore
の古いテキストを置き換えたときに呼ばれるメソッドafterTextChanged(s: Editable)
s
中のどこかで、テキストが変更されたときに呼ばれるメソッドUndoTextWatcher.ktclass UndoTextWatcher : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // テキスト変更が起こる直前の処理(undo用の文字列保持など) } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など) } override fun afterTextChanged(s: Editable) { // テキストが変更された後の処理(undo・redoの可否判定など) } }例えば「あいうえお」→「あいうえおかきくけこ」の場合は以下のようになる。
logSystem.out: beforeTextChanged // (「お」の直後の)0文字が5文字のafterに置き換えられようとしている System.out: s: あいうえお, start: 5, count: 0, after: 5 System.out: onTextChanged // 「か」から5文字が0文字のbeforeを置き換えた System.out: s: あいうえおかきくけこ, start: 5, before: 0, count: 5 System.out: afterTextChanged // 「あいうえおかきくけこ」になった System.out: s: あいうえおかきくけこ挙動を実装する
まず、
undoing
(アンドゥ・リドゥ実行中かどうか)・undos
(アンドゥのリスト)・redos
(リドゥのリスト)と、before
やafter
を保持するクラスを用意しておく。UndoTextWatcher.ktclass EditEvent { private val beforePosition: Int // 置き換えられる文字列の開始位置 private val before: CharSequence // 置き換えられる文字列 private var afterPosition: Int = 0 // 置き換えた文字列の開始位置 private var after: CharSequence = "" // 置き換えた文字列 constructor(beforePosition: Int, before: CharSequence) { this.beforePosition = beforePosition this.before = before } fun setAfter(afterPosition: Int, after: CharSequence) { this.afterPosition = afterPosition this.after = after } fun undo(editable: Editable) { editable.replace(afterPosition, afterPosition + after.length, before) } fun redo(editable: Editable) { editable.replace(beforePosition, beforePosition + before.length, after) } } class UndoTextWatcher : TextWatcher { var undoing = false // undo・redo実行中かどうか val undos = LinkedList<EditEvent>() // undoリスト val redos = LinkedList<EditEvent>() // redoリスト override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // テキスト変更が起こる直前の処理(undo用の文字列保持など) } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など) } override fun afterTextChanged(s: Editable) { // テキストが変更された後の処理(undo・redoの可否判定など) } }
beforeTextChanged
でやること[0] (
undoing
がtrue
のとき、何もしない。)
[1]start
と、元文字列s
のstart
からcount
文字(==置き換えられる文字列)をundos
に追加する。
[2]redos
が空でない場合、redos
を空にする。UndoTextWatcher.ktclass UndoTextWatcher : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { // テキスト変更が起こる直前の処理(undo用の文字列保持など) if (undoing) return // [0] val event = EditEvent(start, s.subSequence(start, start + count)) // undoする文字列と位置を保持する undos.addLast(event) // [1] clearRedos() // [2] } private fun clearRedos() { while (!redos.isEmpty()) { redos.removeFirst() } } ...(略)... }
onTextChanged
でやること[0] (
undoing
がtrue
のとき、何もしない。)
[1]beforeTextChanged
でundos
に追加した要素に、start
と、新文字列s
のstart
からcount
文字(==置き換えた文字列)の情報を追加する。UndoTextWatcher.ktclass UndoTextWatcher : TextWatcher { ...(略)... override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など) if (undoing) return // [0] val event = undos.getLast() // undoリストに最後に追加した要素 event.setAfter(start, s.subSequence(start, start + count)) // [1] } ...(略)... }アンドゥボタンが押されたときにやること
[0] (
undos
が空の場合何もしない。)
[1]undos
から最新の要素を取り出して、redos
に追加する。
[2]undoing
をtrue
にする。
[3] 文字列全体のafterPosition
からafter
の長さ分の文字列(つまりafter
)を、before
に置き換える。
[4]undoing
をfalse
にする。UndoTextWatcher.ktclass UndoTextWatcher : TextWatcher { ...(略)... fun undo(editable: Editable) { if (undos.isEmpty()) return // [0] val event = undos.removeLast() // [1] redos.addLast(event) // [1] undoing = true // [2] try { event.undo(editable) // [3] } finally { undoing = false // [4] } } }リドゥボタンが押されたときにやること
[0] (
redos
が空の場合何もしない。)
[1]redos
から最新の要素を取り出して、undos
に追加する。
[2]undoing
をtrue
にする。
[3] 文字列全体のbeforePosition
からbefore
の長さ分の文字列(つまりbefore
)を、after
に置き換える。
[4]undoing
をfalse
にする。UndoTextWatcher.ktclass UndoTextWatcher : TextWatcher { ...(略)... fun redo(editable: Editable) { if (redos.isEmpty()) return // [0] val event = redos.removeLast() // [1] undos.addLast(event) // [1] undoing = true // [2] try { event.redo(editable) // [3] } finally { undoing = false // [4] } } }これでアンドゥ・リドゥ機能の挙動の実装は完成した。
EditText
で使えるようにする実際にアプリの画面でアンドゥ・リドゥできるようにする。
EditText
・undoボタン・redoボタンは適当に用意しておく。Fragment.ktval textWatcher = UndoTextWatcher() editText.addTextChangedListener(textWatcher) // undo・redo機能を付けたTextWatcherをEditTextにセットする undoButton.setOnClickListener { textWatcher.undo(editText.text) // undoボタンを押してundoする } redoButton.setOnClickListener { textWatcher.redo(editText.text) // redoボタンを押してredoする }これで、アプリ上でアンドゥ・リドゥを実行できるようになる。
その他
アンドゥ・リドゥできるか判定する
これ以上アンドゥできない時にボタンを無効にしたいなどの場合に、アンドゥできるかどうかを判定する。
Fragment.ktclass UndoTextWatcher : TextWatcher { ...(略)... override fun afterTextChanged(s: Editable) { hasUndos = !undos.isEmpty() // undoできるか(undoリストが空でないか) hasRedos = !redos.isEmpty() // redoできるか(redoリストが空でないか) } } if (!hasUndos) { // ボタン無効などの処理 } if (!hasRedos) { // ボタン無効などの処理 }まとめ
TextWatcher
を利用して置き換える文字列や位置を取得し、アンドゥ・リドゥ機能を実装した。EditText.addTextChangedListener()
でTextWatcher
をセットすると、アプリ上でアンドゥ・リドゥが使えるようになる。参考
- 投稿日:2020-02-14T15:11:28+09:00
【kotlin/Android】三目並べを作ってみよう!
学生時代では、Java + eclipse環境で、RPGのAndroidアプリを作成した経験がありますが、
最近のAndroidプログラミング言語はKorlin、開発環境はAndroid Studioが主流になっているみたいです。
スマホアプリ作りたくなったので、言語と環境を覚えるために、Kotlinでサンプルの三目並べゲームを作ってみました。activity_main.xml
赤いエリア → palette。部品を選択するところです。
水色のエリア → Component Tree。画面に設置した部品の関係ツリーを表示します。
緑のエリア → スマホの画面。ここに部品を配置します。
オレンジのエリア → 緑色のエリアで選択している部品のプロパティが表示されます。今回はTable LayoutにTable Rowを配置してその中にbuttonを配置して、三目並べの形を作りました!
それで、それぞれのbuttonに、onClickイベントがあるので、ここで呼び出すfunction名を書き込むと、
MainActivity.kt側のfunctionを呼び出すことができるようになります。コードはこちら
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/whiteColorBackground" android:gravity="center" tools:context=".MainActivity"> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_marginTop="3px"> <Button android:id="@+id/bu1" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu2" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu3" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_marginTop="3px"> <Button android:id="@+id/bu4" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu5" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu6" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:layout_marginTop="3px"> <Button android:id="@+id/bu7" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu8" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> <Button android:id="@+id/bu9" android:layout_width="40pt" android:layout_height="40pt" android:layout_marginLeft="3pt" android:background="@color/whiteBu" android:onClick="buClick" /> </TableRow> </TableLayout>colors.xml
先程の、activity_main.xmlでbackgroundに色を指定しているのですが、その色は直接色コードを指定する他に、
名前をつけて定義することもできます。
こちらのほうが変更するときに後々楽なので定義します。
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#D81B60</color> <color name="whiteColorBackground">#D3D3D3</color> <color name="whiteBu">#F9F9F9</color> <color name="blue">#FF33B5E5</color> <color name="darkGreen">#FF669900</color> </resources>MainActivity.kt
実行コードです。
ボタンを押されたとき、buClickイベントが実行されてゲームが進みます。
package com.example.tictactoeapp import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.View import android.widget.Button import android.widget.Toast import kotlinx.android.synthetic.main.activity_main.* import java.util.Random class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } fun buClick(view: View) { val buSelected = view as Button var cellId = 0 when(buSelected.id){ R.id.bu1 -> cellId = 1 R.id.bu2 -> cellId = 2 R.id.bu3 -> cellId = 3 R.id.bu4 -> cellId = 4 R.id.bu5 -> cellId = 5 R.id.bu6 -> cellId = 6 R.id.bu7 -> cellId = 7 R.id.bu8 -> cellId = 8 R.id.bu9 -> cellId = 9 } //Log.d("buClick", buSelected.id.toString()) //Log.d("buClick: cellId", cellId.toString()) playGame(cellId, buSelected) } var activityPlayer = 1 var player1 = ArrayList<Int>() var player2 = ArrayList<Int>() fun playGame(cellId:Int, buSelected:Button){ if(activityPlayer == 1){ buSelected.text = "X" buSelected.setBackgroundResource(R.color.blue) player1.add(cellId) activityPlayer = 2 autoPlay() }else{ buSelected.text = "O" buSelected.setBackgroundResource(R.color.darkGreen) player2.add(cellId) activityPlayer = 1 } buSelected.isEnabled = false checkWinner() } fun checkWinner(){ var winner = -1 //row1 if(player1.contains(1) && player1.contains(2) && player1.contains(3)) { winner = 1 } if(player2.contains(1) && player2.contains(2) && player2.contains(3)){ winner = 2 } //row2 if(player1.contains(4) && player1.contains(5) && player1.contains(6)) { winner = 1 } if(player2.contains(4) && player2.contains(5) && player2.contains(6)){ winner = 2 } //row3 if(player1.contains(7) && player1.contains(8) && player1.contains(9)) { winner = 1 } if(player2.contains(7) && player2.contains(8) && player2.contains(9)){ winner = 2 } //col1 if(player1.contains(1) && player1.contains(4) && player1.contains(7)) { winner = 1 } if(player2.contains(1) && player2.contains(4) && player2.contains(7)){ winner = 2 } //col2 if(player1.contains(2) && player1.contains(5) && player1.contains(8)) { winner = 1 } if(player2.contains(2) && player2.contains(5) && player2.contains(8)){ winner = 2 } //col3 if(player1.contains(3) && player1.contains(6) && player1.contains(9)) { winner = 1 } if(player2.contains(3) && player2.contains(6) && player2.contains(9)){ winner = 2 } //cross1 if(player1.contains(1) && player1.contains(5) && player1.contains(9)) { winner = 1 } if(player2.contains(1) && player2.contains(5) && player2.contains(9)){ winner = 2 } //cross2 if(player1.contains(3) && player1.contains(5) && player1.contains(7)) { winner = 1 } if(player2.contains(3) && player2.contains(5) && player2.contains(7)){ winner = 2 } if(winner == 1){ player1WinnerCount += 1 Toast.makeText(this, "Player 1 win the game", Toast.LENGTH_LONG).show() restartGame() }else if(winner == 2){ player2WinnerCount += 1 Toast.makeText(this, "Player 2 win the game", Toast.LENGTH_LONG).show() restartGame() } } fun autoPlay(){ var emptyCells = ArrayList<Int>() for(cellId in 1..9){ if(!(player1.contains(cellId) || player2.contains(cellId))){ emptyCells.add(cellId) } } if(emptyCells.size == 0){ restartGame() } val r = Random() val randIndex = r.nextInt(emptyCells.size) val cellId = emptyCells[randIndex] var buSelected:Button? buSelected = when(cellId){ 1 -> bu1 2 -> bu2 3 -> bu3 4 -> bu4 5 -> bu5 6 -> bu6 7 -> bu7 8 -> bu8 9 -> bu9 else -> {bu1} } playGame(cellId, buSelected) } var player1WinnerCount = 0 var player2WinnerCount = 0 fun restartGame(){ activityPlayer = 1 player1.clear() player2.clear() for(index in 1..9){ val buSelected:Button buSelected = when(index){ 1 -> bu1 2 -> bu2 3 -> bu3 4 -> bu4 5 -> bu5 6 -> bu6 7 -> bu7 8 -> bu8 9 -> bu9 else -> {bu1} } buSelected!!.text = "" buSelected!!.setBackgroundResource(R.color.whiteBu) buSelected!!.isEnabled = true } Toast.makeText(this, "Player1: $player1WinnerCount ,Player2: $player2WinnerCount", Toast.LENGTH_LONG).show() } }実行結果
エミュレーターで実行するとこんな感じです。
感想
AndroidStudioが登場したことで、Java+eclipseの時代よりも、簡単に開発環境の構築と
コーディングできるようになっていると思います。
kotlinについてはJavaのライブラリも最新のkotlinのライブラリも使えつつ、現代的なコードなので生産性の向上が期待できると思います。
もっと多くのアプリを作ってみたいところです。
- 投稿日:2020-02-14T14:25:08+09:00
16進数文字列から画像に変換する方法(Kotlin)
https://so-wh.at/entry/20041012/p1
上記のページを参考にしました。もしかしたら他の方が書いているかもしれませんが自分用メモとして。前提として、画像を16進文字列に変換したテキストファイルをアセットに保管してあります。
fun hextoBitmap(file: String): Bitmap { val openfile = assets.open(file) var imagetext = "" try { val inst = InputStreamReader(openfile) val br = BufferedReader(inst) var line = br.readLine() while (line != null) { imagetext += line line = br.readLine() } inst.close() br.close() }catch (e:IOException) { Log.e("IOException", e.toString()) } val bytes = mutableListOf<Byte>() for(index in 1..imagetext.length/2) { val byte = Integer.parseInt(imagetext.substring((index - 1) * 2,index * 2),16) bytes.add(byte.toByte()) } println(bytes) val byteArray = bytes.toByteArray() val bitmap = BitmapFactory.decodeByteArray(bytearray, 0, byteArray.size) return bitmap }
- 投稿日:2020-02-14T12:21:52+09:00
FrescoのSimpleDraweeViewで新しい画像をセットしても古い画像が表示された問題の解消
問題
Androidの開発でFrescoを使ってることは多いと思う。
そのFrescoのSimpleDraweeViewを使用していた際、新しい画像をセットしても画面に反映されない現象に遭遇した。
解決方法
キャッシュを消す。(by stack overflow)
Fresco.getImagePipeline().evictFromMemoryCache(uri)解決までの流れ
R1. 新しくセットするuriをBitmap化して、そのuriがセットしたい画像かをDebugで確認する
・ 下記コードでbitmap
変数を作成し、このコード直後にbreak pointを貼る。val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)R2. break pointで止まったら、ToolbarのDebugから
bitmap
変数のView Bitmap
をクリックして画像を目視確認する。R3. 画像がセットしたい画像なので、キャッシュを疑い消す。
R4. 解決。
- 投稿日:2020-02-14T12:11:58+09:00
ESP32をBluetoothキーボードにして楽譜めくり用フットペダルを作ってみる
概要
ESP32をBLEキーボードにするという記事を見かけました。
普段からAndroidタブレットで楽譜を見たりしているのですが、演奏中に使うと譜めくりが大変です。Androidタブレットで楽譜を表示して、足でスクロールできるように、レゴでペダルスイッチを作ってみた。中身は携帯電話型キーボードっていう謎の物体。そのボタンをてきとうに押すようにして、PerfectViewerでスクロール設定。 pic.twitter.com/jXJaKtjxnV
— オカリナ太郎 (@ocarinaTaro) April 21, 2016以前に上のようなレゴブロックと特殊キーボードを使ったなんちゃってフットペダル(譜めくり用)を作ってみたことがあるのですが、こんな感じでESP32を使ってBluetooth接続のものにしたいと思います。
ESP32評価ボード(安いの)
ESP32はAmazonで見つけたのを適当に買いました。
(これ → https://t.co/1FKYttcJcp )
買っては見たものの、ピン配置が他の評価ボードと同じかどうかもよく分からずなんとなく使ってます。
(どうせたいしたことはしないので)ESP32への書き込みはArduino IDEを使ってますが、ここではArduino IDEのインストールやセットアップについては触れません。
BLEキーボードライブラリ
https://github.com/T-vK/ESP32-BLE-Keyboard
こちらを利用させていただきました。
ダウンロードしてきて、Arduino IDEのlibrariesフォルダ(普通にインストールするとこの辺? → C:\Program Files (x86)\Arduino\libraries\)に入れて完了。
(これ以降の記事ほぼ不要)プログラム
まとまってないけどこんな感じ。
ボタン2つで、'f' と 'b' を送るだけのもの。
キーイベントが送られてきた後はAndroid側でどうにかします。#include <BleKeyboard.h> #define PIN_LED0 2 #define PIN_BTN0 4 #define PIN_BTN1 5 BleKeyboard bleKeyboard; int gConnect = 0; int gBtn0 = HIGH; int gBtn1 = HIGH; uint8_t chrBtn0 = 'f'; uint8_t chrBtn1 = 'b'; void setup() { Serial.begin(115200); Serial.println("Starting BLE Keyboard"); bleKeyboard.begin(); pinMode(PIN_LED0, OUTPUT); pinMode(PIN_BTN0, INPUT_PULLUP); pinMode(PIN_BTN1, INPUT_PULLUP); } void loop() { int btn0 = digitalRead(PIN_BTN0); int btn1 = digitalRead(PIN_BTN1); if(bleKeyboard.isConnected()) { if(gConnect == 0){ gConnect = 1; digitalWrite(PIN_LED0, HIGH); Serial.println("BLE Keyboard: connected"); } if(gBtn0 != btn0){ if(btn0 == LOW){ Serial.println("Button 0"); bleKeyboard.write(chrBtn0); } } if(gBtn1 != btn1){ if(btn1 == LOW){ Serial.println("Button 1"); bleKeyboard.write(chrBtn1); } } } else { if(gConnect == 1){ gConnect = 0; digitalWrite(PIN_LED0, LOW); Serial.println("BLE Keyboard: disconnected"); } } gBtn0 = btn0; gBtn1 = btn1; delay(100); }PerfectViewer
Android側はこのソフトで楽譜を表示することにします。
漫画ビューアとして有名ですが、
- 余白自動カット
- ハードキー設定
- スクロール量設定
どの機能をとっても今回の用途に最適です。
このソフトはきっと楽譜ビューアだと思います。
設定項目 設定値 余白自動カット ON ハードキー設定 'f'→「次のページにスクロール」
'b'→「前のページにスクロール」スクロール量設定 お好みで こんな感じに設定。
回路
回路というのはばかられるくらい簡単。
GPIO4,5をINPUT_PULLUP
にしているので、タクトスイッチを挟んでGPIO4,5とGNDを直結するだけ。ブレッドボード上だとこんな感じ。
作ってみた
ESP32と電池とボタンを百均で買ってきた適当な大きさのケースに詰め込みます。
配線が余って大変なことになっていますが気にしません。動かしてみた
実際に動かしてみたのがこちら。
ケースの角を押すとボタンが押されて楽譜がスクロールします。Bluetoothフットペダルできた。ページ送りと戻しの機能のみのシンプルなやつ。電池も中に収まったのでけっこう小さくできた。動画は手で押してるけど、足で踏んでも十分動く。 pic.twitter.com/scjA4ovwAQ
— オカリナ太郎 (@ocarinaTaro) December 15, 2019まとめ
これくらいコンパクトで数個のボタンがついててカスタマイズ可能なフットペダル売ってないですかね、、?
- 投稿日:2020-02-14T11:21:31+09:00
MediaCodecとBluetoothを使ってミラーリングソフトを作る
目的
Android搭載ウェアラブル端末触る際に出力されてる画面を手元のスマホに写すようにすれば楽じゃね?ってのがスタート。
初Androidアプリ作成&初Kotlinなのでたくさん叩いてください。今回は非同期処理になるのでMediaCodecのCallBackで処理します。
送信端末がZenfone5、受信側がPixel3で動作確認をしています。
OSのバージョンによっては修正が必要な箇所がでますが適宜書き換えてください。使ったもの、参考にしたもの
- AndroidDevelopers
- 公式。
- BluetoothChat
- Bluetoothの接続、送受信に使用
- Bluetooth周りは全てここ使ってます。
- Androidの画面をPCにミラーリングするソフトを作る1
- この記事をBluetooth版にしたものが今回作成したもの。
- MediaProjectionのところは全部真似しています。
- MediaCodec クラス概要 和訳
- MediaCodecの理解、日本語助かります。
流れ
送信側
- エンコーダの作成
- 解像度、FPSなどの指定
- コールバック関数の設定
- エンコーダに設定値反映
- エンコーダ入力用Surfaceの取得
- エンコーダの開始
- エンコーダが出力完了する度にコールバック関数が呼ばれ
- フレームごとにBluetoothで送信
受信側
- デコーダの作成
- 解像度、FPS、出力先などの指定
- エンコーダに設定値反映
- コールバック関数の設定
- デコーダの開始
- デコーダへ入力
- Surfaceが更新される
解説
送信側
エンコーダの作成
createEncoderByTypeでcodecのインスタンスを生成
戻り値にcodecインスタンス、引数には使用するエンコード方法を指定。
文字列で指定しますが定数があるので覚えなくても問題なし。
エンコード/デコードの種類codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)解像度、FPSなどの指定
MediaFormat.createVideoFormatでformatの作成。
format = MediaFormat.createVideoFormat(MIMETYPE, width, height)引数にはエンコード方法、縦横の解像度を指定。
作成したformatにsetIntegerメソッドで足りていない情報を設定。
今回は
- カラーフォーマット
- ビットレート
- FPS
- キャプチャレート
- Iフレームインターバル
を設定しています。
formatの設定値format.setInteger( MediaFormat.KEY_COLOR_FORMAT, // 設定するパラメータ MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface // 設定値 )コールバック関数の設定
冒頭にも書きましたが非同期処理の場合はコールバックにて入出力のタイミングが通知される。
コーデックの抽象メソッドは下記4つ// コーデックの入力バッファが使用可能になった時に呼ばれる onInputBufferAvailable(codec: MediaCodec, index: Int) // コーデックの出力バッファが使用可能になった時に呼ばれる onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) // コーデックエラー時に呼ばれる onError(codec: MediaCodec, e: MediaCodec.CodecException) // bitrateやfps、解像度などフォーマット変更時に呼ばれる(ここは理解不足です) onOutputFormatChanged(codec: MediaCodec, format: MediaFormat)エンコード側は入力→Surfaceから自動取得、出力→フレームをBluetooth送信なので
onOutputBufferAvailableのみ実装、残りは空実装してください。override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { val buffer = codec.getOutputBuffer(index) val array = ByteArray(info.size + 4) buffer?.run { this.get(array, 0, info.size) // emulation_prevention_three_byte // 0x00000002はH264のNALパケットには出てこない array[0 + info.size] = 0.toByte() array[1 + info.size] = 0.toByte() array[2 + info.size] = 0.toByte() array[3 + info.size] = 2.toByte() } val intent = Intent() intent.action = "DISPLAY_UPDATE" intent.putExtra("IMAGE", array) // アプリ内のみに対してブロードキャスト送信 val mBroadcastReceiver = LocalBroadcastManager.getInstance(context) mBroadcastReceiver.sendBroadcast(intent) // バッファを解放 codec.releaseOutputBuffer(index, false) }codec.getOutputBuffer(index)でByteBuffer型のフレームデータを取得。
このデータがH264フォーマットの1フレーム分ですが、このまま投げると受信側で切れ目がわからなくなるのでフッタを追加。codec.releaseOutputBuffer()でバッファの開放を忘れないこと。
第二引数をTrueにするとSurfaceへ出力される。エンコーダ側なのでFalse。送るデータの準備ができたらBluetoothクラスに送信します!が理解不足のため、onOutputBufferAvailable内で送信することができないのでIntentでMainActivityに渡しています。
エンコーダに設定値反映
MediaFormat.createVideoFormatとformat.setIntegerで設定したformatをcodecに反映させる。
// エンコーダを設定 codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)第一引数にformat、第二引数に出力先Surfaceを指定。
第四引数にはフラグを設定しますが、送信側(エンコーダ)の場合はCONFIGURE_FLAG_ENCODEを設定。エンコーダ入力用Surfaceの取得
コーデック側の対応はcodec.createInputSurface()でSurfaceを取得しておくことのみ。
// エンコーダにフレームを渡すのに使うSurfaceを取得 // configureとstartの間で呼ぶ必要あり surface = codec.createInputSurface()エンコーダの開始
設定とコールバックの実装が終わったらエンコーダの開始。
引数もなしにstart,のみ。
この時点ではコーデックになにも入力されていないため出力無し。codec.start()仮想ディスプレイの出力先を↑のSurfaceに指定
入力はSurafceを使うのでMediaProjectionを使ってAndroidの画面をキャプチャし続ける。
その辺りはAndroidの画面をPCにミラーリングするソフトを作る1を参考エンコーダが出力完了する度にコールバック関数が呼ばれ、
フレームごとにBluetoothで送信
↑この2つはコールバック関数の設定のところに記載。
Bluetooth部分についてはBluetoothChatのコードや解説記事などがたくさんあるのでそちらを参照。受信側
デコーダの作成
エンコーダの作成をほとんど同じです。
create En coderByTypeとcreate De coderByTypeの違いのみ。
もちろんコーデック方法があっていないとデコードできないので送信側と合わせる。
codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)解像度、FPS、出力先などの指定
ここもエンコーダとほとんど同じ。
デコーダは送られてくるデータを処理するだけなのでIフレームの設定など、エンコーダに比べて設定箇所は減る。// この2つはエンコーダのみ必要 // format.setInteger(MediaFormat.KEY_FRAME_RATE, fps) // format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)コールバック関数の設定
デコーダ側は 入力→Bluetoothでフレーム受信、出力→Surfaceへ出力
onInputBufferAvailableとonOutputBufferAvailableを実装。override fun onInputBufferAvailable(mc:MediaCodec, inputBufferId:Int) { codecBufId = inputBufferId codecInputBuffer = codec.getInputBuffer(inputBufferId)!! isCodecBufAvailabile = true } // 出力バッファ使用可能時(フレーム作成完了時) override fun onOutputBufferAvailable( mc: MediaCodec, index: Int, info: MediaCodec.BufferInfo ) { if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { codec.releaseOutputBuffer(index, false) return } // 第2引数がTrueならSurfaceに出力 codec.releaseOutputBuffer(index, true) }入力処理は
- 入力バッファが使用可能になる(onInputBufferAvailableが呼ばれる)
- codecInputBuffer = codec.getInputBuffer()でバッファを取得
- codecInputBuffer.put(ByteArray, offset, size)でバッファにデータを格納
- codec.queueInputBuffer()でデコーダにデータをキューイング
今回は2まではonInputBufferAvailableで処理し、3,4はBluetoothの受信処理の方で行っている。
出力処理は
codec.releaseOutputBuffer(index, true)のtrue,falseが肝。
infoをみてコンフィグデータだった場合は、falseを指定してSurfaceへ出力しないようにしている。
残りのフレームデータの場合はtrueを指定。
キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い【GOP】エンコーダに設定値反映
デコーダの場合は第二引数にSurfaceを出力先として指定、第四引数はエンコーダの時のみ指定なので0で良い。
codec.configure(format, surface, null, 0)デコーダの開始
ここはエンコーダと同じ
codec.start()デコーダへ入力
コールバック関数の設定でバッファを取得後、フレームデータを入力。
/* 受信処理 ・受信完了時、recvStateをWAIT_INPUTBUFFER_AVAILABLE ・ダブルバッファで2フレーム分保持できるようにしておく 受信用:readBuf,確定した1フレーム分:queuingBuffer */ // 入力バッファ使用許可 + 入力フレーム格納済みの場合 if(isCodecBufAvailabile && recvState == WAIT_INPUTBUFFER_AVAILABLE) { // フラグOff デコーダのCallbackによってOnされる isCodecBufAvailabile = false val bufDataSize = queuingBuffer.position() // 1フレーム分をデコーダのバッファにコピー codecInputBuffer.put(queuingBuffer.array(), 0, bufDataSize) codec.queueInputBuffer( codecBufId, 0, bufDataSize, 0, 0 ) // デコーダのバッファにコピーしたので古いフレームはクリアしておく codecInputBuffer.clear() queuingBuffer.clear() }Surfaceが更新される
コールバックのところに書いたようにフレームデータがデコードされ、出力準備が完了すると
onOutputBufferAvailableが呼ばれる。
releaseOutputBufferで第二引数をtrueにしてSurfaceを更新させる。結果
480x360@30fps
Iフレームは1枚/秒
で設定したもの。
Bluetoothでミラーリング pic.twitter.com/zQqscysH7L
— February 14, 2020つぎ
次は受信側のタッチイベントを送信してなんちゃって遠隔操作をしたいと思います。
コード