Camera¶
Two ways to photograph. camera.open() opens a camera screen your app owns,
with its own controls and a review step. camera_view() puts a live viewfinder
inside your layout, with your own buttons around it.
Both are new in 1.8.0.
The camera screen¶
from apkpy_lib import Screen, button, camera, label, run
home = Screen(id="home")
status = label("Ready", id="status", screen=home)
def captured(success, path):
if success:
status.set_value(path)
else:
status.set_value("Nothing was kept")
def failed(reason):
status.set_value("Camera: " + reason)
def take_photo():
camera.open(mode="photo", lens="back", flash="auto", grid=True,
on_result=captured, on_error=failed)
button("Photo", id="photo", command=take_photo, screen=home)
run(start_screen=home)
| Call | What it does |
|---|---|
camera.open(...) |
The camera screen, in mode="photo" or "video" |
camera.capture(...) |
Photo. The 1.7.0 callback, now with the in-app camera |
camera.record(...) |
camera.open(mode="video", ...), said shortly |
camera.capabilities(on_result=...) |
What this hardware can actually do |
camera.available() |
Whether there is a camera at all |
capture() and record() take every option below except mode, and everything
after on_result is a named argument.
The two callbacks¶
on_result(success, path) runs once — when the person accepts, cancels, or
a fatal error closes the camera.
on_error(reason) is optional and receives one string, before an unsuccessful
result, for failures that close the camera. Cancelling is not an error. A
photo that fails but can be retried stays inside the camera, so the person can
simply try again.
The path is not a filesystem path on Android
Android answers a content:// URI for a file private to your app. It is a
handle, not a location on disk. Nothing is published to the gallery or
uploaded unless your app does it. The Previewer answers a filename.
Options¶
| Option | Values and meaning |
|---|---|
mode |
photo / video |
lens |
back / front. A missing lens falls back, visibly |
flash |
off / on / auto. Photo only, needs a flash, off under manual exposure |
quality |
sd / hd / fhd / uhd. A video preference, with a supported fallback |
aspect_ratio |
4:3 / 16:9. A preference for photo and preview, not a promise about video dimensions |
zoom |
Ratio from 1.0, clamped to the lens's range |
exposure |
Compensation index, not EV. The step and range come from capabilities() |
timer |
Seconds, 0–30 |
grid |
The framing grid |
audio |
Microphone permission and sound while recording. False records silently |
mirror |
Mirror the front lens, default True |
max_duration |
1–3600 seconds, default 60 |
jpeg_quality |
1–100, default 95. Not a resolution setting |
iso, shutter_ms |
Both 0 for automatic, or both positive. ISO, and milliseconds |
focus_distance |
-1 autofocus, 0 infinity, positive is diopters |
white_balance |
auto, daylight, cloudy, incandescent, fluorescent |
Manual settings tell the truth¶
# 10 ms is 1/100 second. Needs a lens with manual sensor control.
camera.capture(iso=200, shutter_ms=10, white_balance="daylight",
focus_distance=-1, on_result=captured, on_error=failed)
A setting the hardware cannot honour reports an error instead of pretending it applied. Ask first if you mean to offer manual controls in your interface:
def hardware_known(info):
# JSON on Android, a dict in the Previewer. json_get() reads both.
status.set_value(str(info))
camera.capabilities(on_result=hardware_known)
Capabilities cover lenses, flash, zoom, the exposure range and step, ISO and
shutter ranges, focus range, white-balance modes, and a simulated flag that is
true in the Previewer. A RAW flag in there describes the hardware — this API
does not capture RAW.
Shaping the camera's interface¶
controls, labels, colors, title and button_radius reshape the camera
screen. Leave them out and the default interface is unchanged. Each opening
carries its own configuration and changes nothing else in your app.
camera.capture(
controls=["close", "shutter", "flip"],
title="RECEIPT CAMERA",
labels={"close": "Back", "capture": "Take receipt",
"flip": "Switch lens", "use_photo": "Keep receipt"},
colors={"accent": "#a5b4fc", "on_accent": "#151a2d"},
button_radius=10,
controls_position="bottom",
grid=False, audio=False,
on_result=captured,
)
| Option | Contract |
|---|---|
controls |
None for all of them; a list to choose and order. Omitted controls leave no gap |
title |
1–48 characters |
labels |
A partial map. Anything you leave out keeps its default |
colors |
A partial palette of opaque #RRGGBB |
button_radius |
0–32, dp on Android and logical pixels in the Previewer |
controls_position |
auto (bottom in portrait, side when wide), or top / bottom |
Control names: close, pro, flash, timer, grid, quality, ratio,
torch, zoom, exposure, flip, shutter, pause, mode, audio.
Palette keys: background, surface, accent, on_accent, text, muted.
Label keys are the control names plus capture, record, stop, resume,
retake, use_photo and use_video.
What cannot be taken away:
closeandshutterare required. An unknown, duplicated or missing required name is an error, not a silent shrug.- Review always keeps Retake and Use photo/video, and recording always uses the shutter as Stop. Those cannot vanish mid-capture.
- Hiding a control hides the button, not the feature.
grid=Truestill draws the grid; pinch and tap-to-focus still work with no slider on screen.
Close and Pro stay in the header; the rest follow your order, wrapping after
three buttons, with sliders on a full row. It is a responsive panel, not
free-form placement. For an Android build, controls, labels and colors
must be literal lists and dictionaries written in the call.
The embedded viewfinder¶
visor = camera_view(id="visor", screen=home, lens="back", fit="cover",
on_ready=ready, on_capture=saved, on_error=failed)
button("Take photo", id="shoot", command=lambda: visor.capture(), screen=home)
button("Switch lens", id="flip", command=lambda: visor.flip(), screen=home)
No toolbar, no review, no mandatory anything — a live picture in your layout and
ordinary ApkPy widgets around it. Arguments: lens, flash, zoom, mirror,
fit (cover or contain), aspect_ratio, autostart, and the three
callbacks. Change things at runtime with the instance's methods; the values in
the constructor must be literals for an Android build.
What the Previewer really does¶
The camera is drawn inside the phone frame, not in a separate window, and it
never starts your webcam or microphone. The screen says so:
PREVIEW SIMULATOR · NO CAMERA ACCESS.
- The viewfinder shows a synthetic calibration scene, or an image you import. Imported files are never overwritten.
- The files are real. Photos are genuine JPEGs. Video is a playable MP4, and
the footer states exactly what it is rather than implying it matches a phone:
Video simulation: 640px · 12 fps · no audio. - Zoom, crop, mirroring and white balance affect the output. Focus, flash and manual exposure are rehearsals, not measurements of real optics or light.
- Pause and resume, duration limits, review, retake, cancellation and every callback work locally — so every branch your app must handle can be exercised at a desk.
- Pro also simulates a refused permission, an unavailable camera and a storage failure. Those are simulator tools, not Android controls.
On Android¶
CameraX and ExifInterface are added only when your app uses the camera. Manual controls go through Camera2 interop. Encoding and review decoding happen off the UI thread, and your callbacks arrive on it. Callback routing and a pending review survive the screen being rebuilt. Camera and torch are released on exit.
Backgrounding the camera stops recording: it never records out of sight.
Not in this API¶
RAW/DNG, vendor HDR, night and portrait modes, slow motion and high-speed
frame rates, concurrent cameras, choosing a physical lens, live frame analysis,
publishing to the gallery, and capture from the background or the lock screen.
Barcodes are the separate scan API.
UHD, long recordings and manual settings all depend on the hardware, on free
storage and on how hot the phone is. Ask capabilities(), and handle
on_error.