【2022年】少し複雑な懐中電灯アプリを実装してみる

# 簡単な懐中電灯アプリ実装方法

https://qiita.com/princess114514/items/b22db4cd6774bff9833b

記事の最後に課題と書いてある通り、問題点があるので、それを修正する

# 修正案・仕様定義

・アプリ起動時にライトが点灯しない（ソースの簡略化のためにわざと切ってる）

実行時にライトが点灯するようにする

・ライトを点灯した状態でアプリをバックグラウンドにしたり終了してもライトつけっぱなし

アプリをバックグラウンド等にやった時等に実行される「OnStop」関数
アプリを再度フォアグランドにやった時等に実行される「OnRestart]関数

上記を用いて対応する

・画面の縦横切り替え等でライトの点灯フラグが初期化されておかしくなる

ViewModelクラスを用いて、上記場合に点灯フラグを保持するようにする

・UIの見た目でライトのOn/Offの判断ができない

Buttonを「ToggleButton」に変えて対応

# Viewの見た目

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/88c6dde3-56ae-7531-c43b-3b476eb8a029.png)

上から順に
・取得したカメラのID
・ライトの点灯/消灯の回数
・ライトの点灯/消灯状態
・ライトの点灯/消灯切り替えボタン（ToggleButton)

ToggleButtonをタップすることでライトの点灯/消灯を切り替える

# 参考

https://qiita.com/princess114514/items/909826f0553f3c73bab6

単方向バインディングとViewModelの情報はここを参照

# プロジェクト作成

「Enpty Activity」でプロジェクト作成をする
パッケージ名とプロジェクト名はなんでもいいが、自分は下記にした

パッケージ：com.flashlight.flashlight
プロジェクト名：FlashLight

# Build.Gradle(app) 設定

https://qiita.com/princess114514/items/909826f0553f3c73bab6

上記のBuild.Gradle（app)設定と同じ


# フォルダ構成、作成クラスファイル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/13bb726a-8091-b991-1cb8-cd1596303c61.png)

新規に追加するのは「MainActivityFactory.kt」と「MainActivityViewModel.kt」の２つ
めんどくさいので、両方ともMainActivity.ktと同じパッケージ階層に作成する

# MainActivityFactory.kt

~~~ MainActivityFactory.kt
package com.flashlight.flashlight
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivityViewModelFactory : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel> create( modelClass : Class<T> ) : T
    {
        return MainActivityViewModel() as T
    }
}
~~~

ViewModelクラスを作るだけのFactoryクラス
何も考えず脳死で作るだけでOK

# MainActivityVieｗModel.kt

~~~ MainActivityViewModel.kt
package com.flashlight.flashlight
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel

class MainActivityViewModel : ViewModel() {

    var currentCameraId: MutableLiveData<String> = MutableLiveData()
    var flashCount : MutableLiveData<Int> = MutableLiveData()
    var flashSwitch : MutableLiveData<Boolean> = MutableLiveData()

    init
    {
        currentCameraId.value = ""
        flashCount.value = 0
        //flashSwitch.value = false //これは初期化しない
    }

    fun GetCurrentCameraId () : String
    {
        return currentCameraId.value!!
    }

}
~~~

# main_activity.xml

~~~ main_activity.xml

<?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="vm"
            type="com.flashlight.flashlight.MainActivityViewModel" />
    </data>

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

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@{vm.currentCameraId}" />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@{vm.flashCount.toString()}" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@{vm.flashSwitch.toString()}" />

                <ToggleButton
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="ToggleButton"
                    android:checked ="@{vm.flashSwitch}"
                    android:onClick="onClick_lightSwitch"/>
            </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

~~~

・単純にViewModelを読み込めるようにする

見た目部分は上から順に
・利用中のカメラのID（TextView）
・ライトを点灯/消灯した回数（TextView）
・ライトのOn、Off状態（TextView）
・ライトの点灯/消灯ボタン（ToggleButton）

を定義しただけ

# MainActivity.kt

ひとまず、全ソースコードを掲載　その後に個別に説明

~~~ MainActivity.kt

package com.flashlight.flashlight

import androidx.appcompat.app.AppCompatActivity
import android.view.View
import android.os.Bundle
import android.hardware.camera2.CameraManager
import android.os.Handler
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.Observer
import com.flashlight.flashlight.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // カメラの参照はこのクラスで保持する
    lateinit var cameraManager : CameraManager
    // その他変数
    lateinit var factory : MainActivityViewModelFactory
    lateinit var viewmodel : MainActivityViewModel
    lateinit var binding : ActivityMainBinding

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        // 各種初期化
        initialize()
        getCamera()
    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun initialize()
    {
        // viewとのbinding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // viewmodel先生
        factory = MainActivityViewModelFactory()
        viewmodel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
        // viewmodelとのbinding
        binding.vm = viewmodel
        binding.lifecycleOwner = this

        // flashSwObjerverの動きを監視して、変更がある場合は内部に掛かれた処理を実行
        val flashSwObserver = Observer<Boolean> { b ->
            cameraManager.setTorchMode(viewmodel.GetCurrentCameraId(), viewmodel.flashSwitch.value!! )
            viewmodel.flashCount.value = viewmodel.flashCount.value!! + 1
        }
        viewmodel.flashSwitch.observe(this,flashSwObserver)

    }

    @RequiresApi(Build.VERSION_CODES.M)
    private fun getCamera()
    {
        cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
        try {
            // トーチモードのコールバックを登録する
            cameraManager.registerTorchCallback(object : CameraManager.TorchCallback() {
                override fun onTorchModeChanged(id: String, enabled: Boolean) {
                    super.onTorchModeChanged(id, enabled)
                    // 前面カメラのみ有効
                    if(id != "0") return
                    // カメラidと状態を保存する
                    viewmodel.currentCameraId.value = id

                    // カメラを取得した初回のみ、ライトを強制的にOnにする
                    if( viewmodel.flashCount.value!! == 0 )
                    {
                        viewmodel.flashSwitch.value = true
                    }
                }
            }, Handler())
        } catch (e: Exception) {
            //例外握りつぶしはよくないが、今回の処理では必要ないため握りつぶす
        }
    }

    fun onClick_lightSwitch(view: View)
    {
        viewmodel.flashSwitch.value = !viewmodel.flashSwitch.value!!
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onStop() {
        super.onStop()
        // 強制的にライトをOff
        cameraManager.setTorchMode(viewmodel.GetCurrentCameraId(), false )
    }

    override fun onRestart() {
        super.onRestart()
        // 保持してたフラグ情報でライトの点灯or消灯を通知する
        // （同じ値が入ってもObserveした処理が実行される）
        viewmodel.flashSwitch.value = viewmodel.flashSwitch.value
    }

}

~~~

### ところどころについてる「RequiresApi(Build.VERSION_CODES.M)」の意味

カメラ系の関数がAndroidSDKのバージョンに依存してるため、そのままだとソース上でエラーになる（ビルドは通るが）扱いなので、それを無くすためのアノテーション
今回はAndroid マシュマロ未満の端末を考慮しないため、そういうおまじない程度に思えばいい

### import

~~~ MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.view.View
import android.os.Bundle
import android.hardware.camera2.CameraManager
import android.os.Handler
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.Observer
import com.flashlight.flashlight.databinding.ActivityMainBinding
~~~

利用するものは上記

### クラス変数制限

~~~ MainActivity.kt
    // カメラの参照はこのクラスで保持する
    lateinit var cameraManager : CameraManager
    // その他変数
    lateinit var factory : MainActivityViewModelFactory
    lateinit var viewmodel : MainActivityViewModel
    lateinit var binding : ActivityMainBinding
~~~

ViewModelで保持すべきじゃない物はMainActivityクラスでで管理する

### onCreate関数

~~~ MainActivity.kt
    super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        // 各種初期化
        initialize()
        getCamera()
~~~

初期化処理を「binding等の設定」「カメラ取得処理」の２つに分離

### initialize関数


~~~ MainActivity.kt
    @RequiresApi(Build.VERSION_CODES.M)
    private fun initialize()
    {
        // viewとのbinding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // viewmodel先生
        factory = MainActivityViewModelFactory()
        viewmodel = ViewModelProvider(this,factory).get(MainActivityViewModel::class.java)
        // viewmodelとのbinding
        binding.vm = viewmodel
        binding.lifecycleOwner = this

        // flashSwObjerverの動きを監視して、変更がある場合は内部に掛かれた処理を実行
        val flashSwObserver = Observer<Boolean> { b ->
            cameraManager.setTorchMode(viewmodel.GetCurrentCameraId(), viewmodel.flashSwitch.value!! )
            viewmodel.flashCount.value = viewmodel.flashCount.value!! + 1
        }
        viewmodel.flashSwitch.observe(this,flashSwObserver)

    }
~~~

特筆すべきは「val flashSwObserver = Observer<Boolean>」以降の行
この処理が重要
何をしてるかというと、viewmodel.flashswitchの値が変わった時に実行する処理を登録している
これで、flashSwitchの内容が変わるだけでライトの点灯/消灯処理が行われる

余談だが、同じ値を代入しても実行される

### getCamera関数


~~~ MainActivity.kt
    @RequiresApi(Build.VERSION_CODES.M)
    private fun getCamera()
    {
        cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
        try {
            // トーチモードのコールバックを登録する
            cameraManager.registerTorchCallback(object : CameraManager.TorchCallback() {
                override fun onTorchModeChanged(id: String, enabled: Boolean) {
                    super.onTorchModeChanged(id, enabled)
                    // 前面カメラのみ有効
                    if(id != "0") return
                    // カメラidと状態を保存する
                    viewmodel.currentCameraId.value = id

                    // カメラを取得した初回のみ、ライトを強制的にOnにする
                    if( viewmodel.flashCount.value!! == 0 )
                    {
                        viewmodel.flashSwitch.value = true
                    }
                }
            }, Handler())
        } catch (e: Exception) {
            //例外握りつぶしはよくないが、今回の処理では必要ないため握りつぶす
        }
    }
~~~

参考に掲載したカメラ取得処理に
「ライト点灯回数が０回目の時だけ、flashSwitchをtrueにする」処理を記載
これにより、「initialize」でObserverに登録したカメラ点灯/消灯処理が実行され、
起動時にカメラを点灯するという処理が実現される

「じゃあカメラの点灯処理をonCreateでやればいいんじゃないか」と思うが
onCreate実行直後は、カメラIDを取得できてない（IDが空っぽ）ので
その状態でカメラの点灯処理を行おうとすると、Nullエラーか何かでアプリが落ちる

### ToggleButton降下処理

~~~ MainActivity.kt
fun onClick_lightSwitch(view: View)
    {
        viewmodel.flashSwitch.value = !viewmodel.flashSwitch.value!!
    }
~~~

flashSwitchを書き換えるだけで、InitializeでObserveに登録したライト点灯/消灯処理が実行される

### onStop処理

~~~ MainActivity.kt
@RequiresApi(Build.VERSION_CODES.M)
    override fun onStop() {
        super.onStop()
        // 強制的にライトをOff
        cameraManager.setTorchMode(viewmodel.GetCurrentCameraId(), false )
    }
~~~

Activityが何らかの形で中断されたときに呼ばれる処理
この内部で、つけたライトを強制的にオフにする処理を入れる
（再度復帰したときに、保持したViewModel側のフラグを元にライト点灯処理を行いたいため、ViewModelの更新は行わない）

### onRestert処理

~~~ MainActivity.kt
@RequiresApi(Build.VERSION_CODES.M)
    override fun onRestart() {
        super.onRestart()
        // 保持してたフラグ情報でライトの点灯or消灯を通知する
        // （同じ値が入ってもObserveした処理が実行される）
        viewmodel.flashSwitch.value = viewmodel.flashSwitch.value
    }
~~~

Activity復帰時の処理
保持してたviewmodel側のライト点灯フラグを元に点灯/消灯を行う

# 以上でビルドすればできるはず

### あとはアイコンとか変えてみようか

あとはアイコンとか変えればそれっぽくなるかもね
いらすと屋の懐中電灯の絵とかで

https://www.irasutoya.com/2013/04/blog-post_3488.html

アイコンの変え方

https://www.mathkuro.com/smartphone-tablet/app-dev/create-app-icon-in-android-studio/


### ToggleButtonの見た目とか変えてみようか

https://qiita.com/nakashimaakio/items/08c467641b4eb07085bd