AndroidStudio(kotlin)デジタル時計（文字サイズ変更、ミリ秒表示、背景写真設定）


# やりたいこと

デジタル時計を実装する。仕様は下記の通り。

* シークバーを操作して、時計の文字のサイズを変更できる。
* ボタン押下で、ミリ秒の表示ができる。
* ギャラリー内の写真を選択し、背景に好きな写真を設定できる。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75451/6c931a74-9701-5b75-c5f8-0baa42442a96.png)


# 実装

## 画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75451/b6729eeb-c913-197f-39fb-324253dc33f8.png)


```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:id="@+id/constraintLayout"
    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:fontFamily="serif"
        android:text="時：分：秒"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.458" />

    <SeekBar
        android:id="@+id/skb"
        android:layout_width="304dp"
        android:layout_height="71dp"
        android:layout_marginBottom="24dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/milliSecond"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="ミリ秒"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Switch
        android:id="@+id/switch1"
        android:layout_width="134dp"
        android:layout_height="52dp"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:text="文字サイズ"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/openPhotoGalleryBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="写真"
        app:layout_constraintEnd_toStartOf="@+id/milliSecond"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textViewMilliSec"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="ミリ秒"
        android:textSize="36sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

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


## メイン処理

```kotlin

package com.example.yamato200605c

import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.BitmapFactory
import android.icu.util.Calendar
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.view.View
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.Toast
import androidx.annotation.RequiresApi
import kotlinx.android.synthetic.main.activity_main.*
import kotlin.concurrent.timer

class MainActivity : AppCompatActivity() {

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        skb.visibility = View.INVISIBLE
        textViewMilliSec.visibility = View.INVISIBLE
        milliSecond.setOnClickListener(object : View.OnClickListener {
            override
            fun onClick(view: View) {
                textViewMilliSec.visibility = View.VISIBLE
            }
        })

        switch1.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked) {
                skb.visibility = View.VISIBLE
            } else {
                skb.visibility = View.INVISIBLE
            }
        }

        setupOpenPhotoGalleryButton()
        setupSeekBar()

        var millisecound: Boolean = true
        handler(millisecound)
    }

    /**
     * シークバーを設置する。
     */
    @RequiresApi(Build.VERSION_CODES.O)
    private fun setupSeekBar()
    {
        skb.progress = 10
        skb.min = 35
        skb.max = 80
        skb.setOnSeekBarChangeListener(
                object : SeekBar.OnSeekBarChangeListener {
                    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                        textView.textSize = progress.toFloat()
                    }
                    override fun onStartTrackingTouch(seekBar: SeekBar) {
                    }
                    override fun onStopTrackingTouch(seekBar: SeekBar) {
                    }
                })
    }

    /**
     * フォトギャラリーから戻ってきたときの処理。
     */
    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (resultCode != RESULT_OK) {
            return
        }
        when (requestCode) {
            READ_REQUEST_CODE -> {
                try {
                    resultData?.data?.also { uri ->
                        val inputStream = contentResolver?.openInputStream(uri)
                        val image = BitmapFactory.decodeStream(inputStream)

                        var iv: ImageView = ImageView(applicationContext)
                            iv.setImageBitmap(image)
                        constraintLayout.background = iv.drawable
                    }
                } catch (e: Exception) {
                    Toast.makeText(this, "エラーが発生しました", Toast.LENGTH_LONG).show()
                }
            }
        }
    }

    /**
     * フォトギャラリーを開くためのボタンを設置する。
     */
    private fun setupOpenPhotoGalleryButton()
    {
        openPhotoGalleryBtn.setOnClickListener(object : View.OnClickListener {
            override
            fun onClick(view: View) {
                openPhotoGallery()
            }
        })
    }

    /**
     * Android端末のフォトギャラリー（写真を保存している内部フォルダ）を開く。
     */
    private fun openPhotoGallery() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "image/*"
        }
        startActivityForResult(intent, READ_REQUEST_CODE)
    }
    companion object {
        private const val READ_REQUEST_CODE: Int = 42
    }

    /**
     * 時計の表示を更新する。引数でtrueを受け取るとミリ秒まで表示する。
     */
    @SuppressLint("SetTextI18n")
    @RequiresApi(Build.VERSION_CODES.N)
    private fun handler(millisecound: Boolean)
    {
        var periodNum: Long = 1000 // 1秒ごとに更新する。
        if (millisecound) {
            periodNum = 10
        }
        val handler = Handler()
        timer(name = "tm", period = periodNum) {
            val calendar = Calendar.getInstance()
            val hour = calendar.get(Calendar.HOUR_OF_DAY)
            val hourPad: String = hour.toString().padStart(2, '0')
            val minute = calendar.get(Calendar.MINUTE)
            val minutePad = minute.toString().padStart(2, '0')
            val second = calendar.get(Calendar.SECOND)
            val secondPad: String = second.toString().padStart(2, '0')
            val millisecond = calendar.get(Calendar.MILLISECOND) / 10
            val millisecondPad = millisecond.toString().padStart(2, '0')
            handler.post {
                if (millisecound) {
                    textView.text = "${hourPad}:${minutePad}:${secondPad}"
                    textViewMilliSec.text = "${millisecondPad}"
                } else {
                    textView.text = "${hourPad}:${minutePad}:${secondPad}"
                }
            }
        }
    }
}
```