Compare commits
3 commits
4d755dce31
...
1212604cf7
| Author | SHA1 | Date | |
|---|---|---|---|
| 1212604cf7 | |||
| 52af9f6047 | |||
| 7979ebd029 |
6 changed files with 174 additions and 20 deletions
|
|
@ -14,6 +14,15 @@ val keystoreProperties = Properties().apply {
|
|||
}
|
||||
}
|
||||
|
||||
// Load version from version.properties
|
||||
val versionProperties = Properties().apply {
|
||||
load(rootProject.file("version.properties").inputStream())
|
||||
}
|
||||
val vMajor = versionProperties["versionMajor"].toString().toInt()
|
||||
val vMinor = versionProperties["versionMinor"].toString().toInt()
|
||||
val vPatch = versionProperties["versionPatch"].toString().toInt()
|
||||
val vCode = versionProperties["versionCode"].toString().toInt()
|
||||
|
||||
android {
|
||||
namespace = "no.naiv.tiltshift"
|
||||
compileSdk = 35
|
||||
|
|
@ -34,8 +43,8 @@ android {
|
|||
applicationId = "no.naiv.tiltshift"
|
||||
minSdk = 35
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
versionCode = vCode
|
||||
versionName = "$vMajor.$vMinor.$vPatch"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ class CameraManager(private val context: Context) {
|
|||
private val _isFrontCamera = MutableStateFlow(false)
|
||||
val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow()
|
||||
|
||||
private val _previewResolution = MutableStateFlow(Size(0, 0))
|
||||
val previewResolution: StateFlow<Size> = _previewResolution.asStateFlow()
|
||||
|
||||
/** Background executor for image capture callbacks to avoid blocking the main thread. */
|
||||
private val captureExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
|
||||
|
|
@ -161,6 +164,7 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
|
||||
surfaceSize = request.resolution
|
||||
_previewResolution.value = surfaceSize
|
||||
surfaceTexture.setDefaultBufferSize(surfaceSize.width, surfaceSize.height)
|
||||
|
||||
val surface = Surface(surfaceTexture)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ import javax.microedition.khronos.opengles.GL10
|
|||
*/
|
||||
class TiltShiftRenderer(
|
||||
private val context: Context,
|
||||
private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit
|
||||
private val onSurfaceTextureAvailable: (SurfaceTexture) -> Unit,
|
||||
private val onFrameAvailable: () -> Unit
|
||||
) : GLSurfaceView.Renderer {
|
||||
|
||||
private lateinit var shader: TiltShiftShader
|
||||
|
|
@ -39,13 +40,13 @@ class TiltShiftRenderer(
|
|||
@Volatile
|
||||
private var isFrontCamera: Boolean = false
|
||||
|
||||
// Quad vertices (full screen)
|
||||
private val vertices = floatArrayOf(
|
||||
-1f, -1f, // Bottom left
|
||||
1f, -1f, // Bottom right
|
||||
-1f, 1f, // Top left
|
||||
1f, 1f // Top right
|
||||
)
|
||||
// Camera resolution for aspect ratio correction (set from UI thread)
|
||||
@Volatile
|
||||
private var cameraWidth: Int = 0
|
||||
@Volatile
|
||||
private var cameraHeight: Int = 0
|
||||
@Volatile
|
||||
private var vertexBufferDirty: Boolean = false
|
||||
|
||||
// Texture coordinates rotated 90° for portrait mode (back camera)
|
||||
// (Camera sensors are landscape-oriented, we rotate to portrait)
|
||||
|
|
@ -75,11 +76,12 @@ class TiltShiftRenderer(
|
|||
shader = TiltShiftShader(context)
|
||||
shader.initialize()
|
||||
|
||||
// Create vertex buffer
|
||||
vertexBuffer = ByteBuffer.allocateDirect(vertices.size * 4)
|
||||
// Allocate vertex buffer (8 floats = 4 vertices × 2 components)
|
||||
vertexBuffer = ByteBuffer.allocateDirect(8 * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.asFloatBuffer()
|
||||
.put(vertices)
|
||||
// Fill with default full-screen quad; will be recomputed when camera resolution is known
|
||||
vertexBuffer.put(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f))
|
||||
vertexBuffer.position(0)
|
||||
|
||||
// Create texture coordinate buffer
|
||||
|
|
@ -102,6 +104,7 @@ class TiltShiftRenderer(
|
|||
|
||||
// Create SurfaceTexture for camera frames
|
||||
surfaceTexture = SurfaceTexture(cameraTextureId).also {
|
||||
it.setOnFrameAvailableListener { onFrameAvailable() }
|
||||
onSurfaceTextureAvailable(it)
|
||||
}
|
||||
}
|
||||
|
|
@ -110,12 +113,19 @@ class TiltShiftRenderer(
|
|||
GLES20.glViewport(0, 0, width, height)
|
||||
surfaceWidth = width
|
||||
surfaceHeight = height
|
||||
vertexBufferDirty = true
|
||||
}
|
||||
|
||||
override fun onDrawFrame(gl: GL10?) {
|
||||
// Update texture with latest camera frame
|
||||
surfaceTexture?.updateTexImage()
|
||||
|
||||
// Recompute vertex buffer for crop-to-fill when camera or surface dimensions change
|
||||
if (vertexBufferDirty) {
|
||||
recomputeVertices()
|
||||
vertexBufferDirty = false
|
||||
}
|
||||
|
||||
// Update texture coordinate buffer if camera changed
|
||||
if (updateTexCoordBuffer) {
|
||||
texCoordBuffer.clear()
|
||||
|
|
@ -182,6 +192,53 @@ class TiltShiftRenderer(
|
|||
@Volatile
|
||||
private var updateTexCoordBuffer = false
|
||||
|
||||
/**
|
||||
* Sets the camera preview resolution for crop-to-fill aspect ratio correction.
|
||||
* Thread-safe — vertex buffer is recomputed on the next frame.
|
||||
*/
|
||||
fun setCameraResolution(width: Int, height: Int) {
|
||||
if (cameraWidth != width || cameraHeight != height) {
|
||||
cameraWidth = width
|
||||
cameraHeight = height
|
||||
vertexBufferDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes vertex positions to achieve crop-to-fill.
|
||||
*
|
||||
* The camera sensor is landscape; after the 90° rotation applied via texture coordinates,
|
||||
* the effective portrait dimensions are (cameraHeight × cameraWidth). We scale the vertex
|
||||
* quad so the camera frame fills the screen without stretching — the GPU clips the overflow.
|
||||
*/
|
||||
private fun recomputeVertices() {
|
||||
var scaleX = 1f
|
||||
var scaleY = 1f
|
||||
|
||||
if (cameraWidth > 0 && cameraHeight > 0 && surfaceWidth > 0 && surfaceHeight > 0) {
|
||||
// After 90° rotation: portrait width = cameraHeight, portrait height = cameraWidth
|
||||
val cameraRatio = cameraHeight.toFloat() / cameraWidth
|
||||
val screenRatio = surfaceWidth.toFloat() / surfaceHeight
|
||||
|
||||
if (cameraRatio > screenRatio) {
|
||||
// Camera wider than screen → crop sides
|
||||
scaleX = cameraRatio / screenRatio
|
||||
} else {
|
||||
// Camera taller than screen → crop top/bottom
|
||||
scaleY = screenRatio / cameraRatio
|
||||
}
|
||||
}
|
||||
|
||||
vertexBuffer.clear()
|
||||
vertexBuffer.put(floatArrayOf(
|
||||
-scaleX, -scaleY,
|
||||
scaleX, -scaleY,
|
||||
-scaleX, scaleY,
|
||||
scaleX, scaleY
|
||||
))
|
||||
vertexBuffer.position(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases OpenGL resources.
|
||||
* Must be called from GL thread.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ import androidx.compose.ui.semantics.stateDescription
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
@ -116,6 +118,7 @@ fun CameraScreen(
|
|||
val minZoom by viewModel.cameraManager.minZoomRatio.collectAsState()
|
||||
val maxZoom by viewModel.cameraManager.maxZoomRatio.collectAsState()
|
||||
val isFrontCamera by viewModel.cameraManager.isFrontCamera.collectAsState()
|
||||
val previewResolution by viewModel.cameraManager.previewResolution.collectAsState()
|
||||
val cameraError by viewModel.cameraManager.error.collectAsState()
|
||||
|
||||
// Gallery picker
|
||||
|
|
@ -161,6 +164,14 @@ fun CameraScreen(
|
|||
glSurfaceView?.requestRender()
|
||||
}
|
||||
|
||||
// Update renderer with camera preview resolution for crop-to-fill
|
||||
LaunchedEffect(previewResolution) {
|
||||
if (previewResolution.width > 0) {
|
||||
renderer?.setCameraResolution(previewResolution.width, previewResolution.height)
|
||||
glSurfaceView?.requestRender()
|
||||
}
|
||||
}
|
||||
|
||||
// Start camera when surface texture is available
|
||||
LaunchedEffect(surfaceTexture) {
|
||||
surfaceTexture?.let {
|
||||
|
|
@ -172,14 +183,28 @@ fun CameraScreen(
|
|||
LaunchedEffect(isGalleryPreview) {
|
||||
if (isGalleryPreview) {
|
||||
glSurfaceView?.onPause()
|
||||
} else {
|
||||
} else if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
glSurfaceView?.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup GL resources on GL thread (ViewModel handles its own cleanup in onCleared)
|
||||
DisposableEffect(Unit) {
|
||||
// Tie GLSurfaceView lifecycle to Activity lifecycle to prevent background rendering
|
||||
val currentIsGalleryPreview by rememberUpdatedState(isGalleryPreview)
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if (!currentIsGalleryPreview) {
|
||||
glSurfaceView?.onResume()
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_PAUSE -> glSurfaceView?.onPause()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
glSurfaceView?.queueEvent { renderer?.release() }
|
||||
}
|
||||
}
|
||||
|
|
@ -208,13 +233,16 @@ fun CameraScreen(
|
|||
GLSurfaceView(ctx).apply {
|
||||
setEGLContextClientVersion(2)
|
||||
|
||||
val newRenderer = TiltShiftRenderer(ctx) { st ->
|
||||
surfaceTexture = st
|
||||
}
|
||||
val view = this
|
||||
val newRenderer = TiltShiftRenderer(
|
||||
context = ctx,
|
||||
onSurfaceTextureAvailable = { st -> surfaceTexture = st },
|
||||
onFrameAvailable = { view.requestRender() }
|
||||
)
|
||||
renderer = newRenderer
|
||||
|
||||
setRenderer(newRenderer)
|
||||
renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
|
||||
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
|
||||
|
||||
glSurfaceView = this
|
||||
}
|
||||
|
|
|
|||
52
bump-version.sh
Executable file
52
bump-version.sh
Executable file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env bash
|
||||
# Bumps the version in version.properties before a release build.
|
||||
# Usage: ./bump-version.sh [major|minor|patch]
|
||||
# Default: patch
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROPS_FILE="$(dirname "$0")/version.properties"
|
||||
|
||||
if [[ ! -f "$PROPS_FILE" ]]; then
|
||||
echo "Error: $PROPS_FILE not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read current values
|
||||
major=$(grep '^versionMajor=' "$PROPS_FILE" | cut -d= -f2)
|
||||
minor=$(grep '^versionMinor=' "$PROPS_FILE" | cut -d= -f2)
|
||||
patch=$(grep '^versionPatch=' "$PROPS_FILE" | cut -d= -f2)
|
||||
code=$(grep '^versionCode=' "$PROPS_FILE" | cut -d= -f2)
|
||||
|
||||
bump_type="${1:-patch}"
|
||||
|
||||
case "$bump_type" in
|
||||
major)
|
||||
major=$((major + 1))
|
||||
minor=0
|
||||
patch=0
|
||||
;;
|
||||
minor)
|
||||
minor=$((minor + 1))
|
||||
patch=0
|
||||
;;
|
||||
patch)
|
||||
patch=$((patch + 1))
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [major|minor|patch]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
code=$((code + 1))
|
||||
|
||||
# Write updated values
|
||||
cat > "$PROPS_FILE" << EOF
|
||||
versionMajor=$major
|
||||
versionMinor=$minor
|
||||
versionPatch=$patch
|
||||
versionCode=$code
|
||||
EOF
|
||||
|
||||
echo "$major.$minor.$patch (versionCode=$code)"
|
||||
4
version.properties
Normal file
4
version.properties
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
versionMajor=1
|
||||
versionMinor=1
|
||||
versionPatch=1
|
||||
versionCode=3
|
||||
Loading…
Add table
Add a link
Reference in a new issue