Aperture: Rotate selected views
This change adds rotation for the following elements:
* Primary/secondary buttons
* Countdown text
* Slider text
* Capture preview buttons
Change-Id: I065f49b18f43eb53954fe81329362141205c019f
diff --git a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
index 2547035..653d92e 100644
--- a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -29,6 +29,7 @@
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.MotionEvent
+import android.view.OrientationEventListener
import android.view.ScaleGestureDetector
import android.view.View
import android.view.WindowManager
@@ -58,9 +59,11 @@
import androidx.core.view.WindowCompat.getInsetsController
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.children
import androidx.core.view.doOnLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
+import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import coil.decode.VideoFrameDecoder
import coil.load
@@ -89,6 +92,7 @@
import org.lineageos.aperture.utils.GridMode
import org.lineageos.aperture.utils.MediaType
import org.lineageos.aperture.utils.PermissionsUtils
+import org.lineageos.aperture.utils.Rotation
import org.lineageos.aperture.utils.ShortcutsUtils
import org.lineageos.aperture.utils.StabilizationMode
import org.lineageos.aperture.utils.StorageUtils
@@ -97,6 +101,7 @@
import java.io.FileNotFoundException
import java.util.concurrent.ExecutorService
import kotlin.math.abs
+import kotlin.reflect.safeCast
@androidx.camera.camera2.interop.ExperimentalCamera2Interop
@androidx.camera.core.ExperimentalZeroShutterLag
@@ -341,6 +346,23 @@
}
}
+ private val screenRotation = MutableLiveData<Rotation>()
+ private val orientationEventListener by lazy {
+ object : OrientationEventListener(this) {
+ override fun onOrientationChanged(orientation: Int) {
+ if (orientation == ORIENTATION_UNKNOWN) {
+ return
+ }
+
+ val rotation = Rotation.fromDegreesInAperture(orientation)
+
+ if (screenRotation.value != rotation) {
+ screenRotation.value = rotation
+ }
+ }
+ }
+ }
+
enum class ShutterAnimation(val resourceId: Int) {
InitPhoto(R.drawable.avd_photo_capture),
InitVideo(R.drawable.avd_mode_video_photo),
@@ -658,6 +680,9 @@
// Bind viewfinder and preview blur view
previewBlurView.previewView = viewFinder
+ // Observe screen rotation
+ screenRotation.observe(this) { rotateViews(it) }
+
// Request camera permissions
if (!permissionsUtils.mainPermissionsGranted()) {
mainPermissionsRequestLauncher.launch(PermissionsUtils.mainPermissions)
@@ -686,6 +711,9 @@
// Register location updates
locationListener.register()
+ // Enable orientation listener
+ orientationEventListener.enable()
+
// Re-bind the use cases
bindCameraUseCases()
}
@@ -694,6 +722,9 @@
// Remove location and location updates
locationListener.unregister()
+ // Disable orientation listener
+ orientationEventListener.disable()
+
super.onPause()
}
@@ -1727,6 +1758,39 @@
}
}
+ private fun rotateViews(screenRotation: Rotation) {
+ val compensationValue = screenRotation.compensationValue.toFloat()
+
+ // Rotate sliders
+ exposureLevel.screenRotation = screenRotation
+ zoomLevel.screenRotation = screenRotation
+
+ // Rotate countdown
+ countDownView.screenRotation = screenRotation
+
+ // Rotate capture preview buttons
+ capturePreviewLayout.screenRotation = screenRotation
+
+ // Rotate secondary top bar buttons
+ ConstraintLayout::class.safeCast(
+ secondaryTopBarLayout.getChildAt(0)
+ )?.let { layout ->
+ for (child in layout.children) {
+ Button::class.safeCast(child)?.smoothRotate(compensationValue)
+ }
+ }
+
+ // Rotate secondary bottom bar buttons
+ proButton.smoothRotate(compensationValue)
+ lensSelectorLayout.screenRotation = screenRotation
+ flashButton.smoothRotate(compensationValue)
+
+ // Rotate primary bar buttons
+ galleryButtonCardView.smoothRotate(compensationValue)
+ shutterButton.smoothRotate(compensationValue)
+ flipCameraButton.smoothRotate(compensationValue)
+ }
+
fun preventClicks(@Suppress("UNUSED_PARAMETER") view: View) {}
companion object {
diff --git a/app/src/main/java/org/lineageos/aperture/ViewExt.kt b/app/src/main/java/org/lineageos/aperture/ViewExt.kt
index 4733750..e9d1f45 100644
--- a/app/src/main/java/org/lineageos/aperture/ViewExt.kt
+++ b/app/src/main/java/org/lineageos/aperture/ViewExt.kt
@@ -1,15 +1,17 @@
/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
package org.lineageos.aperture
import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.AnimationSet
import android.view.animation.TranslateAnimation
import androidx.core.view.isVisible
+import org.lineageos.aperture.utils.Rotation
internal fun View.setPadding(value: Int) {
setPadding(value, value, value, value)
@@ -57,3 +59,10 @@
})
})
}
+
+internal fun View.smoothRotate(rotation: Float) {
+ animate().cancel()
+ animate()
+ .rotationBy(Rotation.getDifference(this.rotation, rotation))
+ .interpolator = AccelerateDecelerateInterpolator()
+}
diff --git a/app/src/main/java/org/lineageos/aperture/ui/CapturePreviewLayout.kt b/app/src/main/java/org/lineageos/aperture/ui/CapturePreviewLayout.kt
index d65a57e..d43c4e7 100644
--- a/app/src/main/java/org/lineageos/aperture/ui/CapturePreviewLayout.kt
+++ b/app/src/main/java/org/lineageos/aperture/ui/CapturePreviewLayout.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -16,7 +16,9 @@
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import org.lineageos.aperture.R
+import org.lineageos.aperture.smoothRotate
import org.lineageos.aperture.utils.MediaType
+import org.lineageos.aperture.utils.Rotation
/**
* Image/video preview fragment
@@ -41,6 +43,12 @@
*/
internal var onChoiceCallback: (uri: Uri?) -> Unit = {}
+ internal var screenRotation = Rotation.ROTATION_0
+ set(value) {
+ field = value
+ updateViewsRotation()
+ }
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
@@ -94,4 +102,11 @@
}
}
}
+
+ private fun updateViewsRotation() {
+ val compensationValue = screenRotation.compensationValue.toFloat()
+
+ cancelButton.smoothRotate(compensationValue)
+ confirmButton.smoothRotate(compensationValue)
+ }
}
diff --git a/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt b/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt
index 1211bff..d852477 100644
--- a/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt
+++ b/app/src/main/java/org/lineageos/aperture/ui/CountDownView.kt
@@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: 2014 The Android Open Source Project
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -18,6 +18,8 @@
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import org.lineageos.aperture.R
+import org.lineageos.aperture.smoothRotate
+import org.lineageos.aperture.utils.Rotation
/**
* This class manages the looks of the countdown.
@@ -47,6 +49,12 @@
private val isCountingDown: Boolean
get() = remainingSeconds > 0
+ internal var screenRotation = Rotation.ROTATION_0
+ set(value) {
+ field = value
+ updateViewsRotation()
+ }
+
/**
* Responds to preview area change by centering the countdown UI in the new
* preview area.
@@ -117,6 +125,12 @@
return false
}
+ private fun updateViewsRotation() {
+ val compensationValue = screenRotation.compensationValue.toFloat()
+
+ remainingSecondsView.smoothRotate(compensationValue)
+ }
+
companion object {
private const val SET_TIMER_TEXT = 1
private const val ANIMATION_DURATION_MS = 800L
diff --git a/app/src/main/java/org/lineageos/aperture/ui/LensSelectorLayout.kt b/app/src/main/java/org/lineageos/aperture/ui/LensSelectorLayout.kt
index 83134ce..db8b1ae 100644
--- a/app/src/main/java/org/lineageos/aperture/ui/LensSelectorLayout.kt
+++ b/app/src/main/java/org/lineageos/aperture/ui/LensSelectorLayout.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -16,7 +16,9 @@
import androidx.core.view.setMargins
import org.lineageos.aperture.R
import org.lineageos.aperture.px
+import org.lineageos.aperture.smoothRotate
import org.lineageos.aperture.utils.Camera
+import org.lineageos.aperture.utils.Rotation
import java.util.Locale
@androidx.camera.camera2.interop.ExperimentalCamera2Interop
@@ -38,6 +40,12 @@
var onCameraChangeCallback: (camera: Camera) -> Unit = {}
var onZoomRatioChangeCallback: (zoomRatio: Float) -> Unit = {}
+ internal var screenRotation = Rotation.ROTATION_0
+ set(value) {
+ field = value
+ updateViewsRotation()
+ }
+
fun setCamera(activeCamera: Camera, availableCameras: Collection<Camera>) {
this.activeCamera = activeCamera
@@ -133,6 +141,15 @@
} else {
formattedZoomRatio
}
+ button.rotation = screenRotation.compensationValue.toFloat()
+ }
+
+ private fun updateViewsRotation() {
+ val rotation = screenRotation.compensationValue.toFloat()
+
+ for (button in buttonToApproximateZoomRatio.keys) {
+ button.smoothRotate(rotation)
+ }
}
private fun formatZoomRatio(zoomRatio: Float): String =
diff --git a/app/src/main/java/org/lineageos/aperture/ui/Slider.kt b/app/src/main/java/org/lineageos/aperture/ui/Slider.kt
index 4cdc597..14fb599 100644
--- a/app/src/main/java/org/lineageos/aperture/ui/Slider.kt
+++ b/app/src/main/java/org/lineageos/aperture/ui/Slider.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: 2022 The LineageOS Project
+ * SPDX-FileCopyrightText: 2022-2023 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/
@@ -17,6 +17,7 @@
import android.view.View
import org.lineageos.aperture.R
import org.lineageos.aperture.px
+import org.lineageos.aperture.utils.Rotation
abstract class Slider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
@@ -46,6 +47,12 @@
"%.01f".format(it)
}
+ var screenRotation = Rotation.ROTATION_0
+ set(value) {
+ field = value
+ invalidate()
+ }
+
var steps = 0
init {
@@ -84,8 +91,13 @@
abstract fun thumb(): Triple<Float, Float, Float>
private fun drawThumb(canvas: Canvas) {
- // Draw circle
val thumb = thumb()
+
+ // Rotate canvas
+ canvas.save()
+ canvas.rotate(screenRotation.compensationValue.toFloat(), thumb.first, thumb.second)
+
+ // Draw circle
canvas.drawCircle(thumb.first, thumb.second, thumb.third, thumbPaint)
// Draw text
@@ -99,5 +111,8 @@
thumb.second + (textBounds.height() / 2),
thumbTextPaint
)
+
+ // Restore original rotation
+ canvas.restore()
}
}
diff --git a/app/src/main/java/org/lineageos/aperture/utils/Rotation.kt b/app/src/main/java/org/lineageos/aperture/utils/Rotation.kt
new file mode 100644
index 0000000..fed3852
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/utils/Rotation.kt
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.utils
+
+import kotlin.math.abs
+
+/**
+ * Rotation utils.
+ *
+ * @property offset The offset added to (360° * k) needed to obtain the wanted rotation.
+ */
+enum class Rotation(val offset: Int) {
+ ROTATION_0(0),
+ ROTATION_90(90),
+ ROTATION_180(180),
+ ROTATION_270(270);
+
+ /**
+ * Get the rotation needed to compensate for the rotation compared to 0°.
+ */
+ val compensationValue = 360 - if (offset > 180) offset - 360 else offset
+
+ private val apertureRanges = mutableListOf<IntRange>().apply {
+ // Left side
+ if (offset < 45) {
+ add(360 - offset - 45 until 360)
+ add(0 until offset)
+ } else {
+ add(offset - 45 until offset)
+ }
+
+ // Right side
+ if (offset > 360 - 45) {
+ add(offset until 360)
+ add(0 until 360 - offset + 45)
+ } else {
+ add(offset until offset + 45)
+ }
+ }
+
+ companion object {
+ /**
+ * Get the rotation where the value is in [rotation - 45°, rotation + 45°]
+ */
+ fun fromDegreesInAperture(degrees: Int) = values().first {
+ it.apertureRanges.any { range -> degrees in range }
+ }
+
+ /**
+ * Get the fastest angle in degrees to apply to the current rotation to reach this rotation.
+ */
+ fun getDifference(currentRotation: Float, targetRotation: Float): Float {
+ val diff = (targetRotation + (360 * (currentRotation / 360).toInt())) - currentRotation
+
+ return if (abs(diff) > 180) {
+ diff - 360
+ } else {
+ diff
+ }
+ }
+ }
+}