【殴り書き】ダイアルページを作成するにあたって苦労したところ
Flutterで学んだことを実践するためにiPhoneの電話アプリを再現している。 以下ダイアルパッドのページを作成するにあたって苦労した点を箇条書きしていく。
Widgetを非表示にする方法
数字ボタンを押したら表示され、数字がすべてなくなると非表示になる「番号を追加」ボタンの制御方法がわからなかった。
→Visibility Widgetを使い解決
ダイアルパッドは画面下部に寄せて、番号表示などは上寄せにする方法
Columnを
mainAxisAlignment: MainAxisAlignment.end,
で下寄せにするも、Column内のすべての要素が下寄せになってしまい、画面上部がすかすかになってしまった。
「番号を追加」ボタンとダイアルパッドの間にSpacer()を追加することで解決。
Spacerは
Expanded(child: Container()) と同じ意味(Spacerの方が新しく便利)。
backspaceのアイコン
ElevatedButtonの左辺を三角にする方法がわからないため、Icons.backspaceで代用。
しかし、アイコンの色を灰色にすると✗マークが白抜きのため見づらくなってしまった。
Stackで背景に黒色のContainerを追加し、ゴリ押しで✗が黒く見えるようにした。
backspaceのアイコンをタップできるようにしたが、Inkwellの表示がおかしい
Iconをタップ可能にするためにInkwell Widgetを使った。似た用途のWidgetとしてGestuorDetectorがあるがそちらはタップ時のアニメーションがなく、タップしたかどうかが分からないのでInkwellを採用した。
ただ、InkwellがIconの形を考慮しないので、四角い背景がタップするときに表示されてしまう。未解決。
テキストを改行する方法
ElevatedButtonのテキストにはColumnが使えないのでテキストだけで改行を実現する必要がある。
2つの方法があり、1つは改行記号(\n)を使う方法、2つ目が''' '''で囲みソースコード中で改行する方法。
今回はシンプルに数字とアルファベットの2行だけだったので、改行記号の方法を採用。
一つのテキストの中で一部だけ文字を小さくする方法
htmlであればspanで囲って、一部だけ文字を小さくしたりアンダーラインを引いたりするのは簡単だが、dartでどうやるか分からなかった。
RichText( text: [TextSpan(), TextSpan(style: TextStyle())]
でspanごとに設定できた。
「1」だけ上の方にずれてしまう
2行目に何も表示するものがないと、1行目が上寄せになってしまった。 半角スペースを2行目に表示させることで解決。
アスタリスクが上にずれてしまう
と#は中央揃えにしたかったため、改行記号による改行はしていない。 #は期待通り真ん中に表示されたが、は上の方にずれていた。 *の前に改行記号を入れることで真ん中に表示されるようになった。(なぜ?)
【Android】BGMをバックグラウンドで再生するには
AndroidでBGMを再生するにはMediaPlayerクラスを使います。
(よく似たクラスにSoundPoolがありますが、こちらは効果音など短めの曲を流すのに使います)
Serviceを使う
アプリがアクティブでない時*1も再生し続けたい場合は、Serviceを使います。
実装が必要なメソッドはonStartCommandとonPreparedの2つです。
(onBindもoverrideを求められますが、バインドサービスとして使用するのでなければ中身は空でも問題ありません)
class MyService: Service(), MediaPlayer.OnPreparedListener { private var mMediaPlayer: MediaPlayer? = null override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { } override fun onPrepared(mp: MediaPlayer?) { } }
onStartCommandにMediaPlayerの初期化処理を書いていきます。
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when(intent.action) { ACTION_PLAY -> { //再生ボタンが押された時 } } }
ここではまずIntentからアクションを取り出しています。
ここで分岐させることによって、再生・停止などの処理を分岐させることができます。
ACTION_PLAYは定数として
private const val ACTION_PLAY: String = "com.example.musiccplayerblogsample.PLAY"
のように定義しておきます。
この定数はServiceを起動する側でも同じ定数を指定します。
val intent = Intent(this, MyService::class.java) intent.action = ACTION_PLAY //同じ定数を指定 startService(intent)
再生準備
ここまでで、Serviceの起動と呼び出しイベントの分岐が出来ました。
次に再生ボタンが押された時のロジックを実装していきます。
ACTION_PLAY -> { mMediaPlayer = MediaPlayer() mMediaPlayer?.apply { setDataSource(this@MyService, Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_bgm)) setOnPreparedListener(this@MyService) prepareAsync() // 自動的に別スレッドでMediaPlayerの再生準備を行ってくれるメソッド } }
setDataSourceで再生する曲のリソースを指定しています。
ここではrawフォルダ内の曲をStringで指定し、Uriにパースしました。
なぜMediaPlayer.create()ではなくsetDataSource()で指定しているかというと、
create()が自動的にprepare()まで行ってしまうからです。
prepare()は呼び出し元と同一スレッドで実行されるため、処理に時間がかかった場合UIのタスクをブロックしてしまう可能性があります。
再生
非同期によるMediaPlayerの準備処理が終わると、setOnPreparedListenerで指定したリスナーのonPrepared(mp: MediaPlayer?)メソッドが呼び出されます。
override fun onPrepared(mp: MediaPlayer?) { mMediaPlayer?.start() }
このメソッド内でstart()を呼び出すことによって再生が開始されます。
バックグラウンドで再生する方法は以上です。
リソースの開放
MediaPlayerはシステムリソースを大量に消費するので、適切にリリースすることが推奨されています。
そのため、以下もServiceクラスに忘れずに実装しましょう。
override fun onDestroy() { super.onDestroy() mediaPlayer?.release() }
まとめ
最後に今回作成したServiceクラスをまとめておきます。
package com.example.musicplayerblogsample import android.app.Service import android.content.Intent import android.media.MediaPlayer import android.net.Uri import android.os.IBinder private const val ACTION_PLAY: String = "com.example.musiccplayerblogsample.PLAY" class MyService: Service(), MediaPlayer.OnPreparedListener { private var mMediaPlayer: MediaPlayer? = null override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when(intent.action) { ACTION_PLAY -> { mMediaPlayer = MediaPlayer() mMediaPlayer?.apply { setDataSource(this@MyService, Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_bgm)) setOnPreparedListener(this@MyService) prepareAsync() // prepare async to not block main thread } } } return START_NOT_STICKY } override fun onPrepared(mp: MediaPlayer?) { mMediaPlayer?.start() } override fun onBind(intent: Intent?): IBinder? { TODO("Not yet implemented") } }
*1:アプリがバックグラウンドになった、他のアプリに遷移した時など