Shoot from the volume keys, while the camera is up

This commit is contained in:
2026-09-16 10:18:25 +07:00
parent 78630d8a2d
commit e1ce7af76f
7 changed files with 136 additions and 0 deletions
@@ -0,0 +1,15 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'com.locphamtran.recipescamera'
version = '0.1.0'
android {
namespace "com.locphamtran.recipescamera.volume"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
}
@@ -0,0 +1,59 @@
package com.locphamtran.recipescamera.volume
import android.view.KeyEvent
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
/**
* Volume keys as a shutter release.
*
* Android exposes no key stream to JS, and the volume keys never reach a view -
* the window (PhoneWindow.OnKeyListener) turns them into a stream-volume change
* before any view sees them. The one place that runs first is the activity's
* dispatchKeyEvent, so MainActivity hands every key here and only passes on
* what comes back false.
*
* Enabled from JS only while the camera screen is up: with the flag off the keys
* go straight back to the window, so volume still works in the library.
*/
class VolumeShutterModule : Module() {
override fun definition() = ModuleDefinition {
Name("RecipesCamVolumeShutter")
Events("onShutter")
// The activity is the one dispatching, but it does not own the module -
// publish the single live instance so it can be reached from there.
OnCreate { instance = this@VolumeShutterModule }
OnDestroy { if (instance === this@VolumeShutterModule) instance = null }
Function("setEnabled") { enabled: Boolean -> VolumeShutterModule.enabled = enabled }
}
companion object {
@Volatile private var instance: VolumeShutterModule? = null
@Volatile private var enabled = false
/**
* Every key event of the activity goes through here first. Returns true when
* the key is ours and has been consumed - that consumption is what keeps the
* press from also changing the media volume, the way a stock camera app
* behaves.
*
* Only ACTION_DOWN with repeatCount 0 fires: the UP and the auto-repeat of a
* held button are still swallowed (so holding it does not leak a volume
* change on release) but produce no second frame.
*/
fun onKeyEvent(event: KeyEvent): Boolean {
if (!enabled) return false
if (event.keyCode != KeyEvent.KEYCODE_VOLUME_UP && event.keyCode != KeyEvent.KEYCODE_VOLUME_DOWN) {
return false
}
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
instance?.sendEvent("onShutter")
}
return true
}
}
}