Aperture: Import count down view from Camera2

Converted to kotlin with some changes here and
there

Signed-off-by: Luca Stefani <luca.stefani.ge1@gmail.com>
Change-Id: I068daa661de3ea478c719f890506202d2bb2df84
diff --git a/app/src/main/java/org/lineageos/aperture/MainActivity.kt b/app/src/main/java/org/lineageos/aperture/MainActivity.kt
index f7bdba8..e4c8e66 100644
--- a/app/src/main/java/org/lineageos/aperture/MainActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/MainActivity.kt
@@ -13,6 +13,7 @@
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.graphics.Color
+import android.graphics.Rect
 import android.graphics.drawable.AnimatedVectorDrawable
 import android.graphics.drawable.ColorDrawable
 import android.location.Location
@@ -68,12 +69,12 @@
 import coil.request.SuccessResult
 import coil.size.Scale
 import com.google.android.material.button.MaterialButton
-import com.google.android.material.chip.Chip
 import com.google.android.material.slider.Slider
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import org.lineageos.aperture.ui.CountDownView
 import org.lineageos.aperture.ui.GridView
 import org.lineageos.aperture.utils.CameraFacing
 import org.lineageos.aperture.utils.CameraMode
@@ -92,6 +93,7 @@
     private val aspectRatioButton by lazy { findViewById<ToggleButton>(R.id.aspectRatioButton) }
     private val bottomButtonsLayout by lazy { findViewById<ConstraintLayout>(R.id.bottomButtonsLayout) }
     private val cameraModeHighlight by lazy { findViewById<MaterialButton>(R.id.cameraModeHighlight) }
+    private val countDownView by lazy { findViewById<CountDownView>(R.id.countDownView) }
     private val effectButton by lazy { findViewById<ImageButton>(R.id.effectButton) }
     private val flashButton by lazy { findViewById<ImageButton>(R.id.flashButton) }
     private val flipCameraButton by lazy { findViewById<ImageButton>(R.id.flipCameraButton) }
@@ -104,7 +106,6 @@
     private val settingsButton by lazy { findViewById<ImageButton>(R.id.settingsButton) }
     private val shutterButton by lazy { findViewById<ImageButton>(R.id.shutterButton) }
     private val timerButton by lazy { findViewById<ImageButton>(R.id.timerButton) }
-    private val timerChip by lazy { findViewById<Chip>(R.id.timerChip) }
     private val torchButton by lazy { findViewById<ImageButton>(R.id.torchButton) }
     private val videoDuration by lazy { findViewById<MaterialButton>(R.id.videoDuration) }
     private val videoModeButton by lazy { findViewById<MaterialButton>(R.id.videoModeButton) }
@@ -594,9 +595,9 @@
      */
     private fun canRestartCamera() = when (cameraMode) {
         // Disallow camera restart if we're taking a photo or if timer is running
-        CameraMode.PHOTO -> !isTakingPhoto && !timerChip.isVisible
+        CameraMode.PHOTO -> !isTakingPhoto && !countDownView.isVisible
         // Disallow camera restart if a recording in progress or if timer is running
-        CameraMode.VIDEO -> !cameraController.isRecording && !timerChip.isVisible
+        CameraMode.VIDEO -> !cameraController.isRecording && !countDownView.isVisible
         // Otherwise, allow camera restart
         else -> true
     }
@@ -1206,20 +1207,21 @@
             return
         }
 
-        lifecycleScope.launch {
-            shutterButton.isEnabled = false
-            timerChip.isVisible = true
-
-            for (i in sharedPreferences.timerMode downTo 1) {
-                timerChip.text = "$i"
-                delay(1000)
-            }
-
-            timerChip.isVisible = false
+        countDownView.setCountDownStatusListener {
+            countDownView.isVisible = false
             shutterButton.isEnabled = true
 
             runnable()
         }
+
+        shutterButton.isEnabled = false
+        countDownView.isVisible = true
+
+        val rect = Rect().apply {
+            viewFinder.getGlobalVisibleRect(this)
+        }
+        countDownView.onPreviewAreaChanged(rect)
+        countDownView.startCountDown(sharedPreferences.timerMode)
     }
 
     companion object {
diff --git a/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt b/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt
new file mode 100644
index 0000000..3bf3e53
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2022 The LineageOS Project
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ui
+
+import android.content.Context
+import android.graphics.Rect
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.IntRange
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import org.lineageos.aperture.R
+
+/**
+ * This class manages the looks of the countdown.
+ */
+class CountDownView(context: Context, attrs: AttributeSet?) : FrameLayout(
+    context, attrs
+) {
+    private val remainingSecondsView by lazy {
+        findViewById<TextView>(R.id.remainingSeconds)
+    }
+    private var remainingSeconds = 0
+    private lateinit var listener: () -> Unit
+    private val handler = MainHandler(Looper.getMainLooper())
+    private val previewArea = Rect()
+
+    /**
+     * Returns whether countdown is on-going.
+     */
+    private val isCountingDown: Boolean
+        get() = remainingSeconds > 0
+
+    /**
+     * Responds to preview area change by centering the countdown UI in the new
+     * preview area.
+     */
+    fun onPreviewAreaChanged(previewArea: Rect) {
+        this.previewArea.set(previewArea)
+    }
+
+    private fun remainingSecondsChanged(seconds: Int) {
+        remainingSeconds = seconds
+        if (seconds == 0) {
+            // Countdown has finished.
+            isInvisible = true
+            listener()
+        } else {
+            remainingSecondsView.text = seconds.toString()
+            // Fade-out animation.
+            startFadeOutAnimation()
+            // Schedule the next remainingSecondsChanged() call in 1 second
+            handler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000)
+        }
+    }
+
+    private fun startFadeOutAnimation() {
+        val textWidth = remainingSecondsView.measuredWidth
+        val textHeight = remainingSecondsView.measuredHeight
+        remainingSecondsView.scaleX = 1f
+        remainingSecondsView.scaleY = 1f
+        remainingSecondsView.translationX = previewArea.centerX() - textWidth / 2f
+        remainingSecondsView.translationY = previewArea.centerY() - textHeight / 2f
+        remainingSecondsView.pivotX = textWidth / 2f
+        remainingSecondsView.pivotY = textHeight / 2f
+        remainingSecondsView.alpha = 1f
+        val endScale = 2.5f
+        remainingSecondsView.animate().apply {
+            scaleX(endScale)
+            scaleY(endScale)
+            alpha(0f)
+            duration = ANIMATION_DURATION_MS
+        }.start()
+    }
+
+    /**
+     * Sets a listener that gets notified when the status of countdown has finished.
+     */
+    fun setCountDownStatusListener(listener: () -> Unit) {
+        this.listener = listener
+    }
+
+    /**
+     * Starts showing countdown in the UI.
+     *
+     * @param sec duration of the countdown, in seconds
+     */
+    fun startCountDown(@IntRange(from = 0) sec: Int) {
+        if (isCountingDown) {
+            cancelCountDown()
+        }
+        isVisible = true
+        remainingSecondsChanged(sec)
+    }
+
+    /**
+     * Cancels the on-going countdown in the UI, if any.
+     */
+    private fun cancelCountDown() {
+        if (remainingSeconds > 0) {
+            remainingSeconds = 0
+            handler.removeMessages(SET_TIMER_TEXT)
+            isInvisible = true
+        }
+    }
+
+    private inner class MainHandler(looper: Looper) : Handler(looper) {
+        override fun handleMessage(message: Message) {
+            when (message.what) {
+                SET_TIMER_TEXT -> remainingSecondsChanged(remainingSeconds - 1)
+                else -> {}
+            }
+        }
+    }
+
+    companion object {
+        private const val SET_TIMER_TEXT = 1
+        private const val ANIMATION_DURATION_MS = 800L
+    }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 3dc2d8a..d235c16 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -180,18 +180,22 @@
         app:layout_constraintStart_toStartOf="@+id/viewFinder"
         app:layout_constraintTop_toTopOf="@+id/viewFinder" />
 
-    <com.google.android.material.chip.Chip
-        android:id="@+id/timerChip"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="8dp"
-        android:layout_marginEnd="16dp"
-        android:textColor="@color/white"
-        android:visibility="gone"
-        app:chipSurfaceColor="@color/black"
+    <org.lineageos.aperture.ui.CountDownView
+        android:id="@+id/countDownView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="@+id/viewFinder"
-        app:rippleColor="#00ffffff" />
+        app:layout_constraintTop_toTopOf="parent">
+
+        <TextView
+            android:id="@+id/remainingSeconds"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center"
+            android:textColor="@android:color/white"
+            android:textSize="125sp" />
+    </org.lineageos.aperture.ui.CountDownView>
 
     <com.google.android.material.slider.Slider
         android:id="@+id/zoomLevel"