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
}
}
}
@@ -0,0 +1,8 @@
{
"platforms": ["android"],
"android": {
"modules": [
"com.locphamtran.recipescamera.volume.VolumeShutterModule"
]
}
}
+13
View File
@@ -0,0 +1,13 @@
import { requireNativeModule } from 'expo-modules-core';
export interface VolumeShutterModule {
/**
* Claims the volume keys while true; while false every key is handed back to
* the window, so they stay volume keys everywhere but the camera screen.
*/
setEnabled(enabled: boolean): void;
/** Fired once per press (a held key does not auto-repeat). */
addListener(event: 'onShutter', listener: () => void): { remove(): void };
}
export default requireNativeModule<VolumeShutterModule>('RecipesCamVolumeShutter');
+8
View File
@@ -0,0 +1,8 @@
{
"name": "recipescam-volume",
"version": "0.1.0",
"description": "Volume keys as a shutter release for Recipescam.",
"main": "index.ts",
"license": "MIT",
"private": true
}