【Android】ルパン三世風タイトルコールをなるべく簡単に作る


# はじめにまとめ
- ルパン三世のタイトルコールをAndroidで実装してみた
- ただ作るだけだとつまらないのでDataBinding+Kotlin Coroutineで実装
- もっと簡単に作れる方法あったら教えてください:relaxed:

# No Lupin The Third, No Life
突然ですが、人生で一度もルパン三世に関わったことのない日本人は少ないと思います。

日本人一度は金曜ロードショーの何回目か分からない「カリオストロの城」の再放送を目にし、吹奏楽部に入れば一度は「ルパン三世のテーマ」を演奏し、エンジニアになれば一度はルパン三世TVアニメ版第二期（通称赤ルパン）の**タイプライターのカシャカシャいうタイトル**を自作するのです。そうに違いないのです（断言）

# 例のタイトルコールを作ってみた
というわけで（？）、有名なあのタイトルコールを表示させるアプリをAndroidで実装してみました。
![Dec-26-2019 11-09-17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309569/335e69b7-64a9-6e80-b3de-e2f79ee5b67c.gif)
実際は一応音も流れます。
※♪デデッデデ　デデッデデ　デデデ　の部分は著作権の関係でありません
効果音はフリー音素材を作っていらっしゃる[魔王魂](https://maoudamashii.jokersounds.com/)さんのものを使用させて頂きました。

ただ作るだけではアレなので、DataBindingと勉強中のKotlin Coroutineを使ってなるはやで作れるものを目指してみました。
実装にかかった時間はだいたい30分くらいだと思います。

# 実装
実際の実装を紹介します。
リポジトリは[こちら](https://github.com/nanaten/Lupin-Title)に置いておきました。

## Build.Gradle
```groovy:build.gladle
// Android Architecture Components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-rc03"
implementation "androidx.lifecycle:lifecycle-livedata:2.2.0-rc03"

// Fragment
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc04'

// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
```

CoroutineとViewModelを使うためにAACを導入します。
`Fragment-KTX`は`viewModels()`を使うために入れただけなので無くても良いです。

忘れずにDataBindingを有効化します。
余談ですが、`viewModels()`を使うためにjvmTargetを1.8にしてます。

```groovy:build.gradle
android {
     ...
    dataBinding {
        enabled = true
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
```

## レイアウト
レイアウトを以下のように作成します。

```xml:activity_main
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="viewModel"
            type="com.nanaten.lupin_title.MainViewModel" />

        <import type="android.view.View" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:id="@+id/title_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:layout_marginEnd="32dp"
            android:hint="タイトルを入力"
            android:imeOptions="actionDone"
            android:inputType="text"
            android:maxLines="1"
            android:text="@={viewModel.title}"
            android:textColor="#000000"
            android:textSize="16sp"
            android:visibility="@{viewModel.isTyping ? View.GONE : View.VISIBLE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="Autofill" />

        <Button
            android:id="@+id/start_button"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:text="START"
            android:visibility="@{viewModel.isTyping ? View.GONE : View.VISIBLE}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_text" />

        <androidx.appcompat.widget.AppCompatTextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#000000"
            android:gravity="center"
            android:maxLines="1"
            android:paddingStart="16dp"
            android:paddingEnd="16dp"
            android:text="@{viewModel.typewriter}"
            android:textColor="#FFFFFF"
            android:visibility="@{viewModel.isTyping ? View.VISIBLE : View.GONE}"
            app:autoSizeMaxTextSize="100sp"
            app:autoSizeMinTextSize="16sp"
            app:autoSizeTextType="uniform"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

```
<img width="460" alt="スクリーンショット 2019-12-26 11.46.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309569/26db9e1b-262a-c7b3-5ad4-dd4250f9a5c7.png">
画像だと分かりにくいですが、中央に`EditText`を置き、その下にボタンを置いています。
また、画面全体を覆うようにタイトル表示用の`TextView`を配置しました。
AutoSizingを使うために`AppCompatTextView`を使っています。

`EditText`は双方向データバインディングを使っています。

```kotlin
    android:text="@={viewModel.title}" // @=で双方向データバインディング
```

細かいですが、デフォルトの状態だとアクションバーが邪魔なのでstyleをNoActionBarにしてあります。

```xml:styles.xml
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
```

## ViewModel
MainViewModelを作成します。

```kotlin:MainViewModel
class MainViewModel : ViewModel() {
    val title = ObservableField<String>()
    val typewriter = ObservableField<String>()
    val isTyping = ObservableField<Boolean>()

    fun startTitleCall() {
        val fullTitle = title.get()
        if(fullTitle.isNullOrBlank()) return

        isTyping.set(true)
        GlobalScope.launch {
            fullTitle.map {
                delay(150)
                typewriter.set(it.toString())
            }
            delay(150)
            typewriter.set(fullTitle)
            delay(2000)
            isTyping.set(false)
            typewriter.set("")
        }
    }
}
```
`ObservableField`型の変数を3つ作成しています。
これらをレイアウトから参照してデータバインディングを行っています。

`startTitleCall`がメインの処理です。1つずつ解説していきます。

```kotlin
    val fullTitle = title.get()
    if(fullTitle.isNullOrBlank()) return
```
`title`には双方向バインディングにより`EditText`に入力した文字列が自動的に格納されます。
ここは単純に`title`がnullもしくは空文字だったら処理を抜けているだけです。

```kotlin
    isTyping.set(true)
    ..
    isTyping.set(false)
```

`isTyping`にtrueがセットされると、　レイアウトでセットしている`visibility`によって、タイトルコール用の`TextView`が表示状態になり、逆に`EditText`と`Button`は非表示になります。

```xml:activity_main
    <!-- Buttonも同様 -->
    <EditText
    ...
    android:visibility="@{viewModel.isTyping ? View.GONE : View.VISIBLE}"
    ...
    />
    
    <androidx.appcompat.widget.AppCompatTextView
    ...
    android:visibility="@{viewModel.isTyping ? View.VISIBLE : View.GONE}"
    ...
    />
```

```kotlin
        GlobalScope.launch {
            fullTitle.map {
                delay(150)
                typewriter.set(it.toString())
            }
            delay(150)
            typewriter.set(fullTitle)
            delay(2000)
            isTyping.set(false)
            typewriter.set("")
        }
```
`GlobalScope.launch`でCoroutineを生成します。（本当はGlobalScopeをほいほい使うのは良くないらしい…）
mapで`fullTitle`の文字列を一文字ずつ取り出し、`typewriter`にセットしています。
`typewriter`はデータバインディングにより`TextView`に紐付けされているので、150ms間隔で**カシャカシャします（語彙貧困）**。

最後にタイトル全体の文字列をセットして2000ms待機させます。
`typewriter`の値をそのままにしておくと、次に実行した時に前回のタイトルが一瞬表示されてしまうので、値をクリアしています。

## Activity

MainActivityで行っているのは`DataBinding`と`OnClickListener`の設定だけです。

```kotlin:MainActivity
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel
        binding.startButton.setOnClickListener { viewModel.startTitleCall() }
    }
}
```

ここまでで表示処理は完成です。
このままだと寂しいので、効果音を付けてより**カシャカシャ感**を出したいと思います。

## 効果音を付ける
`SoundPool`で効果音を再生させるようにします。
素材ファイルは[魔王魂](https://maoudamashii.jokersounds.com/)さんからDLした以下のoggファイルを`/res/raw`フォルダに格納してあります。
・銃03 -> title
・点火04 -> typewriter
<img width="212" alt="スクリーンショット 2019-12-26 13.34.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309569/79c5de27-82d2-642b-3c82-5c4957f67b8b.png">
`startTitleCall`を以下のように修正します。

```kotlin:MainViewModel
    fun startTitleCall(context: Context) { // Contextを渡すように修正
        // SoundPool
        val attributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build()
        val sound = SoundPool.Builder().setMaxStreams(1).setAudioAttributes(attributes).build()
        val resType = sound.load(context, R.raw.typewriter, 1)
        val resCall = sound.load(context, R.raw.title, 1)

        val title = title.get()
        if(title.isNullOrBlank()) return

        isTyping.set(true)
        GlobalScope.launch {
            title.map {
                delay(150)
                sound.play(resType, 1.0f, 1.0f, 0, 0, 1.0f)
                typewriter.set(it.toString())
            }
            delay(150)
            sound.play(resCall, 1.0f, 1.0f, 0, 0, 1.0f)
            typewriter.set(title)
            delay(2000)
            isTyping.set(false)
            typewriter.set("")
        }
    }
```

```kotlin:MainActivity
    binding.startButton.setOnClickListener { viewModel.startTitleCall(this) } // Contextを渡すように修正
```

以上で完成です！

# 最後に
Coroutineを使うと言いつつただの**delayを使ったベタ書き**になってしまって反省しております。
こうすればもっと簡単にできるよ！などの投稿お待ちしております。

また、DataBindingの勉強をするにあたって坂口亮太さんの著作[「実践 Android Data Binding」](https://nextpublishing.jp/book/10257.html)には大変お世話になりました。
この場を借りてお礼申し上げます。