Code It!

コーディング中に調べたことをまとめています。

【殴り書き】ダイアルページを作成するにあたって苦労したところ

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:アプリがバックグラウンドになった、他のアプリに遷移した時など