【Android】＋Buttonを長押して高速カウントアップ in Kotlin

# ➊ はじめに
「**[イベントのハンドリング](https://qiita.com/PoodleMaster/items/276b6f74777cf382084b)**」と「**[ハンドラによる周期処理](https://qiita.com/PoodleMaster/items/69e96da401ed040f0603)**」の基礎を勉強したので、これを用い、よくある「**＋Button長押して、高速カウントアップ**」みないな UI を作ってみました。

# ➋ どんな感じ？
百聞は一見にしかずということで、こんな感じ～になります。

## (1) イメージ
「`TextView`」1個と「`Button`」3個のシンプルなアプリです😁
<img width="200" src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/ae47b07a-a247-a3db-cd05-cbb8d76c2dc3.gif'>

## (2) アプリの仕様
- `＋`ボタン押下：カウントアップ
- `＋`ボタン長押し：高速カウントアップ
- `－`ボタン押下：カウントダウン
- `－`ボタン長押し：高速カウントダウン
- `CLEAR`ボタン押下：カウント0クリア
- Acitivityを廃棄してもカウンタは引き継がれる
- カウンタは0未満にはならない


# ➌ お勉強ポイント
今まで勉強してきたものを組み合わせると、簡単にできちゃいます！

- [Button押下イベントのハンドリング](https://qiita.com/PoodleMaster/items/276b6f74777cf382084b)
- [Handlerによる周期処理](https://qiita.com/PoodleMaster/items/69e96da401ed040f0603)
- データ保存方法
- [アクティビティのライフサイクル](https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ja)

# ➍ イベント処理方法
ただの1回のカウントアップなら、**Click処理**で良いです。また長押しを1回のみ検出するだけなら、**LongClick処理**で良いです。しかし、今回は、「**長押し中に高速カウントアップを継続する**」という処理を行うので、長押しを検出後、**周期処理**が必要となります。周期処理は、Handlerを

## (1) イベント処理整理
分かりやすくするために「`＋ボタン`」のみで考えます。
イベントに対する処理は、こんな感じです。
これを組み合わせると、**長押しでカウントを高速でカウントアップ**できます。
![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/853450a7-afe6-6670-5010-d06a4e84f68f.png)

## (2) 例えば
例えば、「`＋ボタン`」を長押してから指を離した場合、以下のようになります。
LongClick検出で1回カウントアップし、その後指を離すまで、周期処理でカウントアップを行います。タイマが100msなので、カウントが高速カウントアップになります。その後、指を離したタイミングで、Runnableをクリアすれば周期起動が止まり、カウントアップが止まります。

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405376/3dd76683-ec29-5d5d-554e-b46722900c1e.png)

※LongClick検出後からRunnable送信のディレイを故意に1000msにしています。なので、実は、周期起動の1回目は1秒後にカウントアップし、その後は100ms毎にカウントアップするようにしています。ここはお好きに設定ください。

# ➎ データ保存
Acitivityが破棄されるとデータも破棄されてしまいます。Acitivityのライフサイクルを確認しながら、Acitivityが破棄してもデータを戻せるように、データ保存の処理も付け加えました。データの保存、読出は以下の手順で実施可能です。

## (1) データ保存
```kotlin:snippet
        var count : Int = 100
        val dataStore : SharedPreferences = getSharedPreferences("DataStore", MODE_PRIVATE)
        dataStore.edit().putInt("count", count).apply()
```
## (2) データ読出し
```kotlin:snippet
        val dataStore : SharedPreferences = getSharedPreferences("DataStore", MODE_PRIVATE)
        var count : Int = dataStore.getInt("count", 0)
```

# ➏ 実装
仕組みが分かったところで、上記をまとめ、諸々実装していきます。
言語は、「**Kotlin**」で実装します。

## (1) strings.xml
> app/src/main/res/values/strings.xml

```xml:strings.xml
<resources>
    <string name="app_name">APP5</string>
    <string name="textView_label">0</string>
    <string name="buttonCountUp">+</string>
    <string name="buttonCountDown">-</string>
    <string name="buttonCountClear">Clear</string>
</resources>
```

## (2) activity_main.xml
> app/src/main/res/layout/activity_main.xml

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/textView_label"
        android:textSize="96sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.388"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/buttonCountClear"
        android:layout_width="124dp"
        android:layout_height="155dp"
        android:text="@string/buttonCountClear"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.773"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.627" />

    <Button
        android:id="@+id/buttonCountDown"
        android:layout_width="124dp"
        android:layout_height="70dp"
        android:text="@string/buttonCountDown"
        android:textSize="34sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.237"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.667" />

    <Button
        android:id="@+id/buttonCountUp"
        android:layout_width="124dp"
        android:layout_height="70dp"
        android:text="@string/buttonCountUp"
        android:textSize="34sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.237"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.56" />

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

## (3) activity_main.kt
> app/src/main/java/com/poodlemaster/app5/MainActivity.kt

```kotlin:MainActivity.kt
package com.poodlemaster.app5

import android.annotation.SuppressLint
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.TextView

class MainActivity : AppCompatActivity(), Runnable, View.OnClickListener, View.OnLongClickListener, View.OnTouchListener {

    companion object {
        const val UNDER_NULL = 0
        const val UNDER_COUNT_UP = 1
        const val UNDER_COUNT_DOWN = 2
    }

    private var underUpDown = UNDER_NULL
    private var count : Int = 0
    private val handler : Handler = Handler(Looper.getMainLooper())

    //--------------------------------------------------------------------------------------
    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.d("app5/status", "onCreate")
        setContentView(R.layout.activity_main)

        // ’＋’ボタン
        val buttonCountUp : Button = findViewById(R.id.buttonCountUp)
        buttonCountUp.setOnTouchListener(this)              // タッチ
        buttonCountUp.setOnClickListener(this)              // クリック
        buttonCountUp.setOnLongClickListener(this)          // ロングクリック

        // ’－’ボタン
        val buttonCountDown : Button = findViewById(R.id.buttonCountDown)
        buttonCountDown.setOnTouchListener(this)            // タッチ
        buttonCountDown.setOnClickListener(this)            // クリック
        buttonCountDown.setOnLongClickListener(this)        // ロングクリック

        // ’CLEAR’ボタン
        val buttonCountClear : Button = findViewById(R.id.buttonCountClear)
        buttonCountClear.setOnClickListener(this)           // クリック
    }

    //--------------------------------------------------------------------------------------
    override fun onClick(view: View){
        when (view.id) {

            // `＋`をクリック
            R.id.buttonCountUp -> {
                Log.d("app5/onClick", "buttonCountUp")
                countUp()
            }

            // `－`をクリック
            R.id.buttonCountDown -> {
                Log.d("app5/onClick", "buttonCountDown")
                countDown()
            }

            // 'CLEAR'をクリック
            R.id.buttonCountClear -> {
                Log.d("app5/onClick", "buttonCountClear")
                countClear()
            }
        }
    }

    //--------------------------------------------------------------------------------------
    override fun onLongClick(view: View) : Boolean {
        when (view.id) {

            // `＋`をロングクリック
            R.id.buttonCountUp -> {
                Log.d("app5/onLongClick", "buttonCountUp")
                countUp()
                handler.postDelayed(this, 1000)
            }

            // `－`をロングクリック
            R.id.buttonCountDown -> {
                Log.d("app5/onLongClick", "buttonCountDown")
                countDown()
                handler.postDelayed(this, 1000)
            }
        }

        return(true)       // trueはonClick処理しない。falseでonClick処理を続けて実行。
    }

    //--------------------------------------------------------------------------------------
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouch(view: View, motEvent: MotionEvent): Boolean {
        when (view.id) {

            // '＋'をタッチ
            R.id.buttonCountUp -> {

                when (motEvent.action) {
                    MotionEvent.ACTION_DOWN -> {    // 押す
                        Log.d("app5/onTouch", "buttonCountUp(ACTION_DOWN)")
                    }
                    MotionEvent.ACTION_UP -> {      // 離す
                        Log.d("app5/onTouch", "buttonCountUp(ACTION_UP)")

                        // Runnable解除
                        handler.removeCallbacks(this)
                    }
                    else -> {                       // else
                        Log.d("app5/onTouch", "buttonCountUp(" + motEvent.action.toString() + ")")
                    }
                }
            }

            // '－'をタッチ
            R.id.buttonCountDown -> {

                when (motEvent.action) {
                    MotionEvent.ACTION_DOWN -> {    // 押す
                        Log.d("app5/onTouch", "buttonCountDown(ACTION_DOWN)")
                    }
                    MotionEvent.ACTION_UP -> {      // 離す
                        Log.d("app5/onTouch", "buttonCountDown(ACTION_UP)")

                        // Runnable解除
                        handler.removeCallbacks(this)
                    }
                    else -> {                       // else
                        Log.d("app5/onTouch", "buttonCountDown(" + motEvent.action.toString() + ")")
                    }
                }
            }
        }

        return(false)       // trueでコールバック処理終了。falseはコールバック処理を継続。
    }

    //--------------------------------------------------------------------------------------
    private fun countUp() : Int {
        underUpDown = UNDER_COUNT_UP

        // カウントアップ
        if(count < Integer.MAX_VALUE) {
            count++
        }
        Log.d("app5/countUp(count)", count.toString())

        val textView : TextView = findViewById(R.id.textView)
        textView.text = count.toString()

        return(count)
    }

    //--------------------------------------------------------------------------------------
    private fun countDown() : Int {
        underUpDown = UNDER_COUNT_DOWN

        // カウントダウン
        if(count > 0) {
            count--
        }
        Log.d("app5/countDown(count)", count.toString())

        val textView : TextView = findViewById(R.id.textView)
        textView.text = count.toString()

        return(count)
    }

    //--------------------------------------------------------------------------------------
    private fun countClear() {
        // カウントクリア
        count = 0
        Log.d("app5/countClear(count)", count.toString())

        val textView : TextView = findViewById(R.id.textView)
        textView.text = count.toString()
    }

    //--------------------------------------------------------------------------------------
    private fun saveCount() {
        // データストア(書き込み)
        val dataStore : SharedPreferences = getSharedPreferences("DataStore", MODE_PRIVATE)
        dataStore.edit().putInt("count", count).apply()
        Log.d("app5/saveCount(count)", count.toString())
    }

    //--------------------------------------------------------------------------------------
    private fun readCount() {
        // データストア(読み取り)
        val dataStore : SharedPreferences = getSharedPreferences("DataStore", MODE_PRIVATE)
        count = dataStore.getInt("count", 0)
        Log.d("app5/readCount(count)", count.toString())
    }

    //--------------------------------------------------------------------------------------
    override fun run() {
        when(underUpDown){
            UNDER_COUNT_UP -> {
                countUp()
            }
            UNDER_COUNT_DOWN -> {
                countDown()
            }
        }
        handler.postDelayed(this, 100)
    }

    //--------------------------------------------------------------------------------------
    override fun onStart() {
        super.onStart()
        Log.d("app5/status", "onStart")
    }

    //--------------------------------------------------------------------------------------
    override fun onResume() {
        super.onResume()
        Log.d("app5/status", "onResume")
        readCount()
        val textView : TextView = findViewById(R.id.textView)
        textView.text = count.toString()
    }

    //--------------------------------------------------------------------------------------
    override fun onPause() {
        super.onPause()
        Log.d("app5/status", "onPause")
        saveCount()
        handler.removeCallbacks(this)
    }

    //--------------------------------------------------------------------------------------
    override fun onStop() {
        super.onStop()
        Log.d("app5/status", "onStop")
    }

    //--------------------------------------------------------------------------------------
    override fun onRestart() {
        super.onRestart()
        Log.d("app5/status", "onRestart")
    }

    //--------------------------------------------------------------------------------------
    override fun onDestroy() {
        super.onDestroy()
        Log.d("app5/status", "onDestroy")
    }
}
```

# ➐ 以上
とりあえず、今回は、ほぼ今まで勉強してきたものでなんとかなりました～🙂👌

今回は周期処理させるものが軽い処理(count++)のみでしたので、UIスレッド(メインスレッド)上のLooperへRunnableをQueueingしましたが、重い処理を実行する場合は、これをUIスレッド(メインスレッド)でやると反応が遅くなり、ユーザビリティが失われてしまいます。そのため、重い処理を実行する場合は、バッググラウンドで動作しているスレッドにリクエストを行うようにします。今後は、マルチスレッドも勉強していきたいと思っています。

「**Android Studio**」でkotlin言語を使って簡単なアプリを作れたら楽しいだろうなと思って少し使い始めました。まずは、機能の基礎勉強中です。同士の役に立てれば本望です😉

お疲れ様でした😊