Aperture: Add VerticalSlider widget

This will be used for exposure level adjustment.

Change-Id: Icf30cab3c8a2d7d38d40b2a70bdfbc753af13549
diff --git a/app/src/main/java/org/lineageos/aperture/ui/VerticalSlider.kt b/app/src/main/java/org/lineageos/aperture/ui/VerticalSlider.kt
new file mode 100644
index 0000000..d1dfa4e
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ui/VerticalSlider.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The LineageOS Project
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ui
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.util.Range
+import android.view.MotionEvent
+import android.view.View
+import org.lineageos.aperture.R
+import org.lineageos.aperture.mapToRange
+import org.lineageos.aperture.px
+
+class VerticalSlider @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+    private val trackPaint = Paint().apply {
+        style = Paint.Style.FILL
+        xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+        setShadowLayer(1f, 0f, 0f, Color.BLACK)
+    }
+
+    private val thumbPaint = Paint().apply {
+        style = Paint.Style.FILL
+        xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+        setShadowLayer(3f, 0f, 0f, Color.BLACK)
+    }
+
+    private val thumbTextPaint = Paint()
+
+    var progress = 0.5f
+        set(value) {
+            field = value.coerceIn(0f, 1f)
+            invalidate()
+        }
+    var onProgressChangedByUser: ((value: Float) -> Unit)? = null
+
+    var textFormatter: (value: Float) -> String = {
+        "%.01f".format(it)
+    }
+
+    var steps = 0
+
+    init {
+        context.obtainStyledAttributes(attrs, R.styleable.VerticalSlider, 0, 0).apply {
+            try {
+                trackPaint.color = getColor(R.styleable.VerticalSlider_trackColor, Color.WHITE)
+                thumbPaint.color = getColor(R.styleable.VerticalSlider_thumbColor, Color.BLACK)
+                thumbTextPaint.color =
+                    getColor(R.styleable.VerticalSlider_thumbTextColor, Color.WHITE)
+                thumbTextPaint.textSize =
+                    getDimension(R.styleable.VerticalSlider_thumbTextSize, 10.px.toFloat())
+            } finally {
+                recycle()
+            }
+        }
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        super.onDraw(canvas!!)
+
+        drawTrack(canvas)
+        drawThumb(canvas)
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        super.onTouchEvent(event)
+
+        if (!isEnabled) {
+            return false
+        }
+
+        when (event?.action) {
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE,
+            MotionEvent.ACTION_UP -> {
+                progress = (height - event.y.coerceIn(0f, height.toFloat())) / height
+                onProgressChangedByUser?.invoke(progress)
+            }
+        }
+
+        return true
+    }
+
+    private fun track(): RectF {
+        val trackWidth = width / 5
+
+        val left = (width - trackWidth) / 2f
+        val right = left + trackWidth
+
+        val top = width / 2f
+        val bottom = height - top
+
+        return RectF(left, top, right, bottom)
+    }
+
+    private fun drawTrack(canvas: Canvas) {
+        val track = track()
+        val trackRadius = track.width() * 0.75f
+
+        // Draw round rect
+        canvas.drawRoundRect(track, trackRadius, trackRadius, trackPaint)
+    }
+
+    private fun drawThumb(canvas: Canvas) {
+        val track = track()
+        val trackHeight = track.height()
+
+        // Draw circle
+        val cx = width / 2f
+        val cy = if (steps > 0) {
+            val progress = Int.mapToRange(Range(0, steps), progress).toFloat() / steps
+            (trackHeight - (trackHeight * progress)) + track.top
+        } else {
+            (trackHeight - (trackHeight * progress)) + track.top
+        }
+        canvas.drawCircle(cx, cy, width / 2.15f, thumbPaint)
+
+        // Draw text
+        val text = textFormatter(progress)
+        val textBounds = Rect().apply {
+            thumbTextPaint.getTextBounds(text, 0, text.length, this)
+        }
+        canvas.drawText(
+            text, (width - textBounds.width()) / 2f, cy + (textBounds.height() / 2), thumbTextPaint
+        )
+    }
+}
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..ca271a6
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="VerticalSlider">
+        <attr name="trackColor" format="color" />
+        <attr name="thumbColor" format="color" />
+        <attr name="thumbTextColor" format="color" />
+        <attr name="thumbTextSize" format="dimension" />
+    </declare-styleable>
+</resources>