diff --git a/app/src/main/java/app/grapheneos/camera/CamConfig.kt b/app/src/main/java/app/grapheneos/camera/CamConfig.kt index 9f11ea197..d558133f7 100644 --- a/app/src/main/java/app/grapheneos/camera/CamConfig.kt +++ b/app/src/main/java/app/grapheneos/camera/CamConfig.kt @@ -50,8 +50,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import app.grapheneos.camera.analyzer.QRAnalyzer -import app.grapheneos.camera.ktx.markAs16by9Layout -import app.grapheneos.camera.ktx.markAs4by3Layout +import app.grapheneos.camera.ktx.applyPreviewRatio import app.grapheneos.camera.ui.activities.CaptureActivity import app.grapheneos.camera.ui.activities.MainActivity import app.grapheneos.camera.ui.activities.MoreSettings @@ -249,7 +248,7 @@ class CamConfig(private val mActivity: MainActivity) { var imageCapture: ImageCapture? = null private set - private var preview: Preview? = null + var preview: Preview? = null val allowedFormats: ArrayList = arrayListOf() @@ -1791,16 +1790,7 @@ class CamConfig(private val mActivity: MainActivity) { // Focus camera on touch/tap mActivity.previewView.setOnTouchListener(mActivity) - mActivity.previewView.apply { - when (aspectRatio) { - AspectRatio.RATIO_16_9 -> { - markAs16by9Layout() - } - AspectRatio.RATIO_4_3 -> { - markAs4by3Layout() - } - } - } + camera?.cameraInfo?.let { mActivity.previewView.applyPreviewRatio(aspectRatio, it) } if (isInPhotoMode) { mActivity.sensorNotifier?.forceUpdateGyro() diff --git a/app/src/main/java/app/grapheneos/camera/ktx/PreviewView.kt b/app/src/main/java/app/grapheneos/camera/ktx/PreviewView.kt index 776dbe0d3..14286a607 100644 --- a/app/src/main/java/app/grapheneos/camera/ktx/PreviewView.kt +++ b/app/src/main/java/app/grapheneos/camera/ktx/PreviewView.kt @@ -1,15 +1,20 @@ package app.grapheneos.camera.ktx +import android.view.Surface +import androidx.camera.core.AspectRatio +import androidx.camera.core.CameraInfo import androidx.camera.view.PreviewView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updateLayoutParams -fun PreviewView.markAs4by3Layout() = applyRatio(3.0, 4.0) +fun PreviewView.applyPreviewRatio(aspectRatio: Int, cameraInfo: CameraInfo) { + val rotation = cameraInfo.getSensorRotationDegrees(display?.rotation ?: Surface.ROTATION_0) + val (short, long) = if (aspectRatio == AspectRatio.RATIO_16_9) 9 to 16 else 3 to 4 -fun PreviewView.markAs16by9Layout() = applyRatio(9.0, 16.0) - -private fun PreviewView.applyRatio(width: Double, height: Double) { updateLayoutParams { - dimensionRatio = "H,$width:$height" + dimensionRatio = when { + rotation % 180 == 0 -> "$long:$short" + else -> "$short:$long" + } } } diff --git a/app/src/main/java/app/grapheneos/camera/ui/CountDownTimerUI.kt b/app/src/main/java/app/grapheneos/camera/ui/CountDownTimerUI.kt index 28cd80ff3..9f5e818c0 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/CountDownTimerUI.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/CountDownTimerUI.kt @@ -6,10 +6,8 @@ import android.os.CountDownTimer import android.util.AttributeSet import android.view.Gravity import android.view.View -import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import androidx.appcompat.widget.AppCompatTextView -import androidx.camera.core.AspectRatio import app.grapheneos.camera.CamConfig import app.grapheneos.camera.R import app.grapheneos.camera.ui.activities.CaptureActivity @@ -104,14 +102,6 @@ class CountDownTimerUI @JvmOverloads constructor( private fun beforeTimeStarts() { - val params: ViewGroup.LayoutParams = layoutParams - params.height = if (camConfig.aspectRatio == AspectRatio.RATIO_4_3) { - mActivity.previewView.width * 4 / 3 - } else { - mActivity.previewView.height - } - layoutParams = params - mActivity.settingsIcon.visibility = View.INVISIBLE mActivity.thirdOption.visibility = View.INVISIBLE mActivity.flipCameraCircle.visibility = View.INVISIBLE diff --git a/app/src/main/java/app/grapheneos/camera/ui/CustomGrid.kt b/app/src/main/java/app/grapheneos/camera/ui/CustomGrid.kt index 7391f3332..4510c656e 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/CustomGrid.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/CustomGrid.kt @@ -6,7 +6,6 @@ import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.View -import androidx.camera.core.AspectRatio import app.grapheneos.camera.CamConfig import app.grapheneos.camera.ui.activities.MainActivity @@ -39,22 +38,16 @@ class CustomGrid @JvmOverloads constructor( return } - val previewHeight = if (camConfig.aspectRatio == AspectRatio.RATIO_16_9) { - mActivity.previewView.width * 16 / 9 - } else { - mActivity.previewView.width * 4 / 3 - } - if (camConfig.gridType == CamConfig.GridType.GOLDEN_RATIO) { val cx = width / 2f - val cy = previewHeight / 2f + val cy = height / 2f val dxH = width / 8f - val dyH = previewHeight / 8f + val dyH = height / 8f - canvas.drawLine(cx - dxH, 0f, cx - dxH, previewHeight.toFloat(), paint) - canvas.drawLine(cx + dxH, 0f, cx + dxH, previewHeight.toFloat(), paint) + canvas.drawLine(cx - dxH, 0f, cx - dxH, height.toFloat(), paint) + canvas.drawLine(cx + dxH, 0f, cx + dxH, height.toFloat(), paint) canvas.drawLine(0f, cy - dyH, width.toFloat(), cy - dyH, paint) canvas.drawLine(0f, cy + dyH, width.toFloat(), cy + dyH, paint) @@ -70,27 +63,27 @@ class CustomGrid @JvmOverloads constructor( width / seed * 2f, 0f, width / seed * 2f, - previewHeight.toFloat(), + height.toFloat(), paint ) - canvas.drawLine(width / seed, 0f, width / seed, previewHeight.toFloat(), paint) + canvas.drawLine(width / seed, 0f, width / seed, height.toFloat(), paint) canvas.drawLine( - 0f, previewHeight / seed * 2f, - width.toFloat(), previewHeight / seed * 2f, paint + 0f, height / seed * 2f, + width.toFloat(), height / seed * 2f, paint ) - canvas.drawLine(0f, previewHeight / seed, width.toFloat(), previewHeight / seed, paint) + canvas.drawLine(0f, height / seed, width.toFloat(), height / seed, paint) if (seed == 4f) { canvas.drawLine( width / seed * 3f, 0f, width / seed * 3f, - previewHeight.toFloat(), + height.toFloat(), paint ) canvas.drawLine( - 0f, previewHeight / seed * 3f, - width.toFloat(), previewHeight / seed * 3f, paint + 0f, height / seed * 3f, + width.toFloat(), height / seed * 3f, paint ) } } diff --git a/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt b/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt index ba9b3b4eb..1fab33867 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt @@ -8,9 +8,11 @@ import android.app.Dialog import android.content.Context import android.content.pm.PackageManager import android.graphics.Color +import android.graphics.Rect import android.os.Handler import android.os.Looper import android.util.Log +import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -20,6 +22,7 @@ import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.AdapterView import android.widget.ArrayAdapter +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.RadioGroup @@ -86,10 +89,9 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : var settingsFrame: View - // Window the panel region was last sized for, so the sizing runs on a rotation and not on - // every frame the panel is drawn in. - private var sizedForWindowWidth = 0 - private var sizedForWindowHeight = 0 + // Region the panel was last sized for, so the sizing runs when it moves and not on every + // frame the panel is drawn in. + private val sizedForRegion = Rect() private var moreSettingsButton: View @@ -160,6 +162,12 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : binding.root.viewTreeObserver.addOnPreDrawListener { updatePanelRegion() } + // The preview belongs to the activity's window, so resizing it schedules no traversal in + // this one: without this the panel would keep the bounds of the preview it was opened over. + mActivity.previewView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updatePanelRegion() + } + locToggle = binding.locationToggle locToggle.setOnClickListener { if (mActivity.videoCapturer.isRecording) { @@ -396,46 +404,49 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : } /** - * Height of the region the panel is centred within. The panel belongs over the preview, whose - * height the 4:3 aspect ratio ties to the window width — but that only holds while the window - * is portrait. In landscape the product is taller than the window itself, which centres the - * panel past the bottom edge and leaves nothing on screen but the top of the toggle row, so - * never let the region outgrow the window it has to be drawn in. + * The preview's rectangle, in the dialog window's coordinates. The panel is centred within + * the preview, and reading the preview is what keeps that true at any window size and aspect + * ratio — recomputing the geometry here would only be a second copy of it, free to disagree. */ - private fun panelRegionHeight(): Int { - val marginTop = (mActivity.rootView.layoutParams as ViewGroup.MarginLayoutParams) - .topMargin + private fun previewRegion(): Rect? { + val preview = mActivity.previewView + if (preview.width == 0 || preview.height == 0) { + return null + } - val previewRegion = marginTop + (binding.root.measuredWidth * 4 / 3) - val windowHeight = binding.root.measuredHeight + val previewLocation = IntArray(2) + preview.getLocationOnScreen(previewLocation) - return when { - windowHeight > 0 -> { - previewRegion.coerceAtMost(windowHeight) - } - else -> previewRegion - } + val rootLocation = IntArray(2) + binding.root.getLocationOnScreen(rootLocation) + + val left = previewLocation[0] - rootLocation[0] + val top = previewLocation[1] - rootLocation[1] + + return Rect(left, top, left + preview.width, top + preview.height) } /** - * Sizes the region to the window the panel is about to be drawn in, whenever that window is - * not the one it was last sized for. The activity declares orientation as a config change it - * handles itself rather than being recreated, so nothing else tells the panel it has been - * rotated — and it can be rotated while it is open, not only between [show]s. + * Moves the panel onto the preview whenever the preview is not where it was last sized for. + * The activity declares orientation as a config change it handles itself rather than being + * recreated, so nothing else tells the panel it has been rotated — and it can be rotated, or + * have its aspect ratio toggled, while it is open rather than between [show]s. */ private fun updatePanelRegion(): Boolean { - val width = binding.root.measuredWidth - val height = binding.root.measuredHeight - if (width == sizedForWindowWidth && height == sizedForWindowHeight) { + val region = previewRegion() ?: return true + if (region == sizedForRegion) { return true } - sizedForWindowWidth = width - sizedForWindowHeight = height + sizedForRegion.set(region) - settingsFrame.layoutParams = (settingsFrame.layoutParams as ViewGroup.MarginLayoutParams) + settingsFrame.layoutParams = (settingsFrame.layoutParams as FrameLayout.LayoutParams) .also { - it.height = panelRegionHeight() + it.gravity = Gravity.TOP or Gravity.START + it.leftMargin = region.left + it.topMargin = region.top + it.width = region.width() + it.height = region.height() } // The list is capped against the region, so it has to be measured against the new one too. @@ -460,8 +471,8 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : val totalDialogHeight = moreSettingsButton.height + moreSettingsButtonTopPadding + dialog.height val availableWidth = dialog.width - (settingsDialogHorizontalMargin * 4) - val availableHeight = availableWidth - .coerceAtMost(panelRegionHeight()) - + val regionHeight = previewRegion()?.height() ?: binding.root.measuredHeight + val availableHeight = availableWidth.coerceAtMost(regionHeight) - (totalDialogHeight - mScrollView.height) val height = if (mScrollViewContent.height < mScrollView.height) { diff --git a/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt b/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt index 50166ed64..a6e896b98 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/activities/MainActivity.kt @@ -9,6 +9,7 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.ImageDecoder import android.graphics.Point @@ -77,6 +78,7 @@ import app.grapheneos.camera.shareCapturedItem import app.grapheneos.camera.databinding.ActivityMainBinding import app.grapheneos.camera.databinding.ScanResultDialogBinding import app.grapheneos.camera.ktx.SystemSettingsObserver +import app.grapheneos.camera.ktx.applyPreviewRatio import app.grapheneos.camera.notifier.SensorOrientationChangeNotifier import app.grapheneos.camera.ui.BottomTabLayout import app.grapheneos.camera.ui.CountDownTimerUI @@ -1303,7 +1305,7 @@ open class MainActivity : AppCompatActivity(), if (videoCapturer.isRecording) return - var iconRotation = (360f - orientation) % 360 + var iconRotation = (360f - ((orientation - getRotation() + 360) % 360)) % 360 // Rotate views that should rotate irrespective of the auto-rotate setting rotateView(gCircleFrame, iconRotation) @@ -1543,6 +1545,21 @@ open class MainActivity : AppCompatActivity(), } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + // The activity declares configChanges for orientation, so nothing else refreshes + // rotation-dependent state. + // The preview follows the window; the capture use cases follow the sensor and are updated + // by onOrientationChange. + camConfig.preview?.targetRotation = previewView.display?.rotation ?: Surface.ROTATION_0 + camConfig.camera?.cameraInfo?.let { + previewView.applyPreviewRatio(camConfig.aspectRatio, it) + } + + rootView.post { sensorNotifier?.notifyListeners() } + } + private fun pauseOrientationSensor() { SensorOrientationChangeNotifier .getInstance(this)?.remove(this) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e69a7035e..b2aa0db88 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -28,8 +28,10 @@ android:id="@+id/preview" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintDimensionRatio="H,9:16" + app:layout_constraintDimensionRatio="9:16" + app:layout_constraintVertical_bias="0" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> @@ -62,104 +64,115 @@ app:layout_constraintTop_toBottomOf="@id/preview" app:layout_constraintBottom_toBottomOf="parent"/> - - - - - + android:visibility="gone"> - + android:layout_height="match_parent" + android:gravity="center" + android:rotation="0" + android:paddingBottom="14sp" + android:layout_marginBottom="9dp" + android:orientation="vertical"> + + + + + + + android:layout_height="1.5dp"/> - + - + - + - + - + - + - + - + - - - + - +