Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 11 Multimedia
Pfeil 11.1 Audio
Pfeil 11.1.1 Audio aufnehmen und abspielen
Pfeil 11.1.2 Effekte
Pfeil 11.2 Sprachverarbeitung
Pfeil 11.2.1 Sprachsynthese
Pfeil 11.2.2 Spracherkennung
Pfeil 11.3 Fotos und Video
Pfeil 11.3.1 Vorhandene Funktionen nutzen
Pfeil 11.3.2 Die eigene Kamera-App
Pfeil 11.3.3 Videos drehen
Pfeil 11.4 Zusammenfassung
 
Zum Seitenanfang

11.3    Fotos und Video Zur vorigen ÜberschriftZur nächsten Überschrift

Mit Intents können Komponenten unterschiedlicher Apps kombiniert werden. Die Nutzung vorhandener Bausteine hat für Sie als Entwickler den Vorteil, das Rad nicht neu erfinden zu müssen. Und der Anwender findet sich schneller zurecht, weil er die Bedienung einer wiederverwendeten Komponente bereits kennt.

 
Zum Seitenanfang

11.3.1    Vorhandene Funktionen nutzen Zur vorigen ÜberschriftZur nächsten Überschrift

Wie leicht Sie die in Android eingebaute Kamera-App nutzen können, zeige ich Ihnen anhand des Programms KameraDemo1. Nach dem Start sehen Sie die Benutzeroberfläche aus Abbildung 11.7.

Die App »KameraDemo1«

Abbildung 11.7    Die App »KameraDemo1«

Klicken Sie auf FOTO AUFNEHMEN, um die Kamera-App im Still-Image-Modus zu betreiben. Diesen sehen Sie in Abbildung 11.8. Mit VIDEO AUFNEHMEN drehen Sie Filme. Zwischen dem Videomodus, der in Abbildung 11.9 dargestellt ist, und dem Fotomodus kann der Anwender jederzeit umschalten. Beachten Sie in diesem Zusammenhang, dass die Benutzeroberfläche auf unterschiedlichen Geräten zum Teil stark variiert.

Die Klasse KameraDemo1Activity ist sehr kurz. Das Starten der Kamera-App findet in zwei Implementierungen von onClick() statt (der Methodenname ist durch die Verwendung eines Lambdas nicht zu sehen). Wie Sie aus vielen anderen Beispielen in diesem Buch bereits wissen, werden android.view.View.OnClickListener-Instanzen verwendet, um auf das Anklicken von Buttons zu reagieren.

Die Kamera-App im Still-Image-Modus

Abbildung 11.8    Die Kamera-App im Still-Image-Modus

Die Kamera-App im Videomodus

Abbildung 11.9    Die Kamera-App im Videomodus

Die Klasse android.provider.MediaStore gestattet den Zugriff auf die Mediendatenbank von Android-Geräten. Für uns sind im Moment vor allem die beiden Konstanten INTENT_ACTION_STILL_IMAGE_CAMERA und INTENT_ACTION_VIDEO_CAMERA interessant, denn sie werden als Actions für Intents verwendet, um die Kamera-App mit startActivity() zu starten.

package com.thomaskuenneth.androidbuch.kamerademo1

import android.content.Intent
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class KameraDemo1Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
foto.setOnClickListener {
val intent = Intent(
MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
startActivity(intent)
}
video.setOnClickListener {
val intent = Intent(
MediaStore.INTENT_ACTION_VIDEO_CAMERA)
startActivity(intent)
}
}
}

Listing 11.7    Die Klasse »KameraDemo1Activity«

Eine auf diese Weise gestartete Kamera-App sendet allerdings keine Daten an den Aufrufer. Die beiden Intents INTENT_ACTION_STILL_IMAGE_CAMERA und INTENT_ACTION_VIDEO_CAMERA eignen sich deshalb vor allem für Fire-and-forget-Szenarien. Damit ist gemeint, dass Sie dem Anwender Ihrer App zwar den Komfort bieten, ein Foto oder Video aufzunehmen, aber die auf diese Weise entstandenen Dateien nicht unmittelbar integrieren oder weiterverarbeiten. Wie Sie das bewerkstelligen, möchte ich Ihnen nun zeigen.

Aufgenommene Fotos weiterverarbeiten

Um Daten von der Kamera-App in Ihrem Programm zu verwenden, rufen Sie startActivityForResult() auf. Das gesendete Intent enthält die Action ACTION_IMAGE_CAPTURE. Außerdem müssen Sie einen Uniform Resource Identifier (URI) übergeben, der das aufgenommene Foto in der Mediendatenbank repräsentiert. Wie das funktioniert, zeigt das Projekt KameraDemo2. Nach dem Auslösen in der Kamera-App erscheinen mehrere Buttons (Abbildung 11.10). Mit inline image verwerfen Sie die Aufnahme und beginnen von vorne. inline image bringt Sie zu KameraDemo2 zurück. Dort wird das aufgenommene Foto angezeigt (Abbildung 11.11). Mit inline image brechen Sie die Aufnahme ab.

Die Kamera-App nach dem Auslösen

Abbildung 11.10    Die Kamera-App nach dem Auslösen

KameraDemo2Activity überschreibt die vier Methoden onCreate(), onStart(), onRequestPermissionsResult() und onActivityResult(). In onCreate() wird nur die Benutzeroberfläche angezeigt und ein OnClickListener registriert. Ein Klick auf Foto aufnehmen ruft startCamera() auf. Diese erzeugt mit contentResolver.insert() einen neuen Eintrag in der systemweiten Mediendatenbank und feuert anschließend ein Intent mit der Action ACTION_IMAGE_CAPTURE. Titel, Beschreibung und der MIME-Type werden als ContentValues-Objekt übergeben. Dieses wird durch entsprechende put()-Aufrufe gefüllt. Den URI des angelegten Datensatzes erhält das Intent mittels putExtra().

Das aufgenommene Foto in »KameraDemo2«

Abbildung 11.11    Das aufgenommene Foto in »KameraDemo2«

Damit KameraDemo2 funktioniert, muss mit WRITE_EXTERNAL_STORAGE der Zugriff auf externe Medien angefordert werden. Da es sich hierbei um eine gefährliche Berechtigung handelt, ist neben dem obligatorischen Eintrag in der Manifestdatei auch die Behandlung zur Laufzeit nötig. In onStart() wird mit checkSelfPermission() geprüft, ob die App auf externe Medien schreiben darf. Falls nicht, wird die Berechtigung angefordert. Bis sie erteilt wurde, kann Foto aufnehmen nicht angeklickt werden.

package com.thomaskuenneth.androidbuch.kamerademo2

import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.graphics.*
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

private const val REQUEST_WRITE_EXTERNAL_STORAGE = 123
private const val REQUEST_IMAGE_CAPTURE = 1
private val TAG = KameraDemo2Activity::class.simpleName
class KameraDemo2Activity : AppCompatActivity() {
private lateinit var imageUri: Uri

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener { startCamera() }
}

override fun onStart() {
super.onStart()
if (checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_WRITE_EXTERNAL_STORAGE)
button.isEnabled = false
} else {
button.isEnabled = true
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
if (requestCode ==
REQUEST_WRITE_EXTERNAL_STORAGE &&
grantResults.isNotEmpty() && grantResults[0] ==
PackageManager.PERMISSION_GRANTED) {
button.isEnabled = true
}
}

override fun onActivityResult(requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_IMAGE_CAPTURE) {
if (resultCode == Activity.RESULT_OK) {
val source = ImageDecoder.createSource(this.contentResolver,
imageUri)
val bitmapSource = ImageDecoder.decodeBitmap(source)
// Größe des aufgenommenen Bildes
val wSource = bitmapSource.width
val hSource = bitmapSource.height
// auf eine Höhe von maximal 300 Pixel skalieren
val hDesti = if (hSource > 300) 300 else hSource
val wDesti = (wSource.toFloat() / hSource.toFloat()
* hDesti.toFloat()).toInt()
val bitmapDesti = Bitmap.createScaledBitmap(bitmapSource,
wDesti, hDesti, false)
imageView.setImageBitmap(bitmapDesti)
} else {
val rowsDeleted = contentResolver.delete(imageUri,
null, null)
Log.d(TAG, "$rowsDeleted rows deleted")
}
}
}

private fun startCamera() {
val values = ContentValues()
values.put(MediaStore.Images.Media.TITLE,
getString(R.string.app_name))
values.put(MediaStore.Images.Media.DESCRIPTION,
getString(R.string.descr))
values.put(MediaStore.Images.Media.MIME_TYPE,
"image/jpeg")
contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values)?.let {
imageUri = it
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE)
}
}

Listing 11.8    Die Klasse »KameraDemo2Activity«

Nach einer Aufnahme wird die Methode onActivityResult() aufgerufen. Hat die Kamera-App als resultCode den Wert RESULT_OK gemeldet (der Benutzer hat ein Foto geschossen), ist alles in Ordnung und das Foto kann angezeigt werden. Andernfalls muss der Eintrag mit delete() wieder aus der Mediendatenbank entfernt werden. Der Anwender hat die Aufnahme ja abgebrochen.

Mit den beiden Methoden createSource() und decodeBitmap() der Klasse ImageDecoder lässt sich ein Bild, das in der Mediendatenbank gespeichert wurde, sehr einfach als Bitmap zur Verfügung stellen. Sie wiederum kann bequem in ImageViews angezeigt werden. Rufen Sie einfach setImageBitmap() einer android.widget.ImageView-Instanz auf. Allerdings können die von der Kamera gelieferten Fotos zu groß sein. Um eine Ausnahme zur Laufzeit zu vermeiden, sollten Sie das Bild, wie im Beispiel gezeigt, mit createScaledBitmap() skalieren.

Mit der Galerie arbeiten

Die App Fotos oder Galerie zeigt alle Fotos und Videos der systemweiten Mediendatenbank an. Sie ist auf praktisch allen Geräten vorhanden. Wie Sie sie in Ihre eigenen Programme integrieren, zeige ich Ihnen anhand des Beispiels GalleryDemo. Unmittelbar nach dem Start ruft die App die Auswahlseite der Galerie auf (Abbildung 11.12).

Tippt der Benutzer ein Bild an, startet GalleryDemo abermals die Galerie. Diese zeigt die ausgewählte Datei in einer Art Detailansicht an, die in Abbildung 11.13 zu sehen ist.

Die Auswahlseite der App »Galerie«

Abbildung 11.12    Die Auswahlseite der App »Galerie«

Anzeige einer Datei in der »Galerie«

Abbildung 11.13    Anzeige einer Datei in der »Galerie«

Die Klasse GalleryDemoActivity erzeugt in onCreate() ein Intent mit der Aktion ACTION_PICK und dem URI EXTERNAL_CONTENT_URI und übergibt es an die Methode startActivityForResult().

package com.thomaskuenneth.androidbuch.gallerydemo

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity

private const val REQUEST_GALLERY_PICK = 1
class GalleryDemoActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, REQUEST_GALLERY_PICK)
}

override fun onActivityResult(requestCode: Int,
resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_GALLERY_PICK) {
if (resultCode == Activity.RESULT_OK) {
data?.let {
val intentView = Intent(Intent.ACTION_VIEW, it.data)
startActivity(intentView)
}
} else {
finish()
}
}
}
}

Listing 11.9    Die Klasse »GalleryDemoActivity«

In onActivityResult() wird mit resultCode == Activity.RESULT_OK geprüft, ob der Benutzer ein Bild ausgewählt hat. Falls ja, wurde der URI der Datei in den Extras eines Intents (data) übermittelt. Er kann dann mit it.data abgefragt werden. Um keine Fehler zur Laufzeit zu riskieren, sollten Sie auf jeden Fall mit data?.let { sicherstellen, dass nicht anstelle eines Intents null übergeben wurde. Um ein Bild anzuzeigen, packen Sie einfach dessen URI in ein Intent mit der Action ACTION_VIEW und rufen anschließend startActivity() auf.

 
Zum Seitenanfang

11.3.2    Die eigene Kamera-App Zur vorigen ÜberschriftZur nächsten Überschrift

Sie haben gesehen, wie schnell Sie Ihre Apps mit einer Aufnahmefunktion für Bilder und Videos versehen können. Bislang haben wir hierfür Teile der mitgelieferten Kamera-App verwendet. In diesem Abschnitt zeige ich Ihnen, wie Sie diese in einer sehr einfachen Version nachbauen. Schritt für Schritt lernen Sie anhand des in Abbildung 11.14 dargestellten Beispiels KameraDemo3, wie eine Live-Vorschau programmiert wird, wie Sie aus den unterschiedlichen Kameras eines Android-Geräts die gewünschte auswählen und wie die eigentliche Aufnahme funktioniert.

Die App »KameraDemo3« im Emulator

Abbildung 11.14    Die App »KameraDemo3« im Emulator

Die Layoutdatei von KameraDemo3 (Listing 11.10) enthält nur ein RelativeLayout mit einer in beiden Richtungen zentrierten android.view.SurfaceView als einziges Kind. Eine SurfaceView stellt einen dedizierten Zeichenbereich zur Verfügung, der zwar innerhalb einer View-Hierarchie angeordnet, aber von einem anderen Thread gezeichnet wird. Das hat den Vorteil, dass der Zeichen-Thread nicht auf die App warten muss, wenn diese mit anderen Aktionen beschäftigt ist. Der Zugriff auf die Oberfläche (engl. Surface) geschieht mithilfe von android.view.SurfaceHolder-Objekten. Diese Klasse enthält unter anderem die Methoden addCallback() und removeCallback(), mit denen Sie ein SurfaceHolder.Callback-Objekt registrieren oder entfernen.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>

Listing 11.10    Die Layoutdatei des Projekts »KameraDemo3«

Die Methoden des Interface SurfaceHolder.Callback (Listing 11.11) werden bei Änderungen an der Oberfläche aufgerufen, surfaceCreated() beispielsweise unmittelbar nach dem Erzeugen. surfaceChanged() informiert über strukturelle Änderungen. surfaceDestroyed() kündigt die unmittelbar bevorstehende Zerstörung einer Oberfläche an.

private val surfaceHolderCallback: SurfaceHolder.Callback =
object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "surfaceDestroyed()")
}

override fun surfaceCreated(holder: SurfaceHolder) {
Log.d(TAG, "surfaceCreated()")
try {
openCamera()
} catch (e: Exception) {
// SecurityException, CameraAccessException
Log.e(TAG, "openCamera()", e)
}
}

override fun surfaceChanged(
holder: SurfaceHolder,
format: Int, width: Int,
height: Int
) {
Log.d(TAG, "surfaceChanged()")
}
}

Listing 11.11    Verwendung von »SurfaceHolder.Callback«

Die Klasse KameraDemo3Activity überschreibt onCreate(), onPause(), onResume() und onRequestPermissionsResult(). onCreate() lädt die Benutzeroberfläche und zeigt sie an. Um später auf die Kamera zugreifen zu können, wird mit getSystemService(CameraManager::class.java) ein Objekt des Typs CameraManager ermittelt und der Variable manager zugewiesen. Außerdem muss die App in der Manifestdatei die gefährliche Berechtigung android.permission.CAMERA anfordern und im Code entsprechend behandeln. Die Prüfung findet in onResume() statt. Hat der Anwender zugestimmt, wird configureHolder() aufgerufen.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
manager = getSystemService(CameraManager::class.java)
setContentView(R.layout.activity_main)
camera = null
}

override fun onPause() {
super.onPause()
surfaceview.visibility = View.GONE
activeSession?.close()
activeSession = null
camera?.close()
camera = null
surfaceview.holder.removeCallback(surfaceHolderCallback)
}

override fun onResume() {
super.onResume()
if (checkSelfPermission(Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.CAMERA),
requestCamera
)
} else {
configureHolder()
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if (requestCode == requestCamera &&
grantResults.isNotEmpty() && grantResults[0] ==
PackageManager.PERMISSION_GRANTED
) {
configureHolder()
}
}

private fun configureHolder() {
surfaceview.holder.addCallback(surfaceHolderCallback)
val sizes = findCameraFacingBack()
if (sizes.isEmpty()) {
Log.d(TAG, "keine passende Kamera gefunden")
finish()
}
val metrics = resources.displayMetrics
for (size in sizes) {
if (size.width > metrics.widthPixels
|| size.height > metrics.heightPixels
) {
continue
}
surfaceview.setOnClickListener { takePicture() }
surfaceview.holder.setFixedSize(size.width, size.height)
surfaceview.visibility = View.VISIBLE
imageReader = ImageReader.newInstance(
size.width, size.height,
ImageFormat.JPEG, 2
)
imageReader.setOnImageAvailableListener(
{
Log.d(TAG, "setOnImageAvailableListener()")
val image = imageReader.acquireLatestImage()
val planes = image.planes
val buffer = planes[0].buffer
saveJPG(buffer)
image.close()
}, null
)
return
}
Log.d(TAG, "Zu groß")
finish()
}

Listing 11.12    Die üblichen Methoden des Activity-Lebenszyklus

In onPause() mache ich mit surfaceview.visibility die SurfaceView unsichtbar und blende sie in configureHolder() wieder ein. Das ist erforderlich, damit die Methoden meiner SurfaceHolder.Callback-Implementierung zuverlässig aufgerufen werden. Haben Sie die Aufrufe von addCallback() und removeCallback() bemerkt? Mit ihnen wird eine Instanz der Klasse SurfaceHolder.Callback registriert oder entfernt.

configureHolder() verzweigt nach findCameraFacingBack(). Dort wird nach einer vom Benutzer weg zeigenden Kamera gesucht und ihre Kennung der Variablen cameraId zugewiesen. findCameraFacingBack() liefert eine Liste von Ausgabegrößen. Sie wird in configureHolder() verwendet, um die Vorschau mit setFixedSize() zu konfigurieren. Steht keine passende Größe zur Verfügung, beendet sich die Activity mit finish(). Bestimmt fragen Sie sich, wann ich endlich etwas über Kameras schreibe. Bislang haben Sie zwar einiges über SurfaceView gelernt, aber das Ziel dieses Abschnitts ist doch die Implementierung einer Live-Vorschau, oder?

Kameras auswählen und verwenden

Der Zugriff auf eine Kamera erfolgt über Instanzen des Typs CameraDevice. In findCameraFacingBack() (Listing 11.13) wird mit cameraIdList über eine Liste der zur Verfügung stehenden Kameras iteriert. Die Methode getCameraCharacteristics() hilft Ihnen bei der Auswahl. Hierzu fragen Sie mit get() gewünschte Eigenschaften ab. Liefert beispielsweise get(CameraCharacteristics.LENS_FACING) den Wert LENS_FACING_BACK, haben Sie die Kamera an der Rückseite des Geräts gefunden. Wichtig ist, als Nächstes mithilfe einer StreamConfigurationMap die möglichen Ausgabegrößen des Geräts zu ermitteln. Die Klasse stellt hierfür die Methode getOutputSizes() zur Verfügung. Um eine StreamConfigurationMap zu ermitteln, rufen Sie cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) auf.

private fun findCameraFacingBack(): Array<Size> {
cameraId = ""
try {
val ids = manager.cameraIdList
for (id in ids) {
val cc = manager.getCameraCharacteristics(id)
Log.d(TAG, "$id: $cc")
val lensFacing = cc.get(CameraCharacteristics.LENS_FACING)
if (lensFacing != null &&
lensFacing ==
CameraCharacteristics.LENS_FACING_BACK
) {
cameraId = id
cc.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.run {
return getOutputSizes(SurfaceHolder::class.java)
}
}
}
} catch (e: CameraAccessException) {
Log.e(TAG, "findCameraFacingBack()", e)
} catch (e: NullPointerException) {
Log.e(TAG, "findCameraFacingBack()", e)
}
return emptyArray()
}

Listing 11.13    Eine geeignete Kamera suchen

Meine Implementierung in configureHolder() sucht das Element, dessen Breite und Höhe am besten zur Größe des einer Activity zur Verfügung stehenden Bereichs passt. Das muss vor der Inbetriebnahme der Kamera erfolgen. Eigentlich müssen Sie dazu nur die Methode openCamera() der Klasse CameraManager aufrufen, allerdings ist eine ganze Reihe von Callback-Objekten beteiligt. Diese sehen wir uns nun etwas genauer an (Listing 11.14). Die abstrakte Klasse CameraDevice.StateCallback erfordert die Implementierung von drei Methoden. onOpened() wird aufgerufen, nachdem das Öffnen (Hochfahren) der Kamera abgeschlossen ist. Ich verwende sie, um die nachfolgenden Schritte der Kamerakonfiguration zu beginnen. Die Methode onDisconnected() signalisiert, dass die Kamera nicht mehr verfügbar ist. Es bietet sich an, in dieser Methode Aufräumarbeiten durchzuführen. onError() wird bei schwerwiegenden Fehlern aufgerufen.

private fun openCamera() {
manager.openCamera(
cameraId,
object : CameraDevice.StateCallback() {
override fun onOpened(_camera: CameraDevice) {
Log.d(TAG, "onOpened()")
camera = _camera
createPreviewCaptureSession()
}

override fun onDisconnected(camera: CameraDevice) {
Log.d(TAG, "onDisconnected()")
}

override fun onError(
camera: CameraDevice,
error: Int
) {
Log.d(TAG, "onError()")
}
}, null
)
}

Listing 11.14    Kamera in Betrieb nehmen

Darüber hinaus müssen Sie zwei weitere Aktionen durchführen, damit eine Kamera verwendet werden kann. In createPreviewCaptureSession() (Listing 11.15) erzeuge ich mit createCaptureRequest() ein Objekt des Typs CaptureRequest.Builder und weise es der Instanzvariablen builderPreview zu. Dieser Builder erhält mit addTarget() eine Referenz auf das Surface, das die Vorschau repräsentiert.

private fun createPreviewCaptureSession() {
val outputs = mutableListOf<OutputConfiguration>()
outputs.add(OutputConfiguration(surfaceview.holder.surface))
outputs.add(OutputConfiguration(imageReader.surface))
val sessionConfiguration = SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
outputs, mainExecutor, captureSessionCallback
)
try {
camera?.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
)?.let {
builderPreview = it
it.addTarget(surfaceview.holder.surface)
camera?.createCaptureSession(sessionConfiguration)
}
} catch (e: Exception) {
Log.e(TAG, "createPreviewCaptureSession()", e)
}
}

Listing 11.15    Die Live-Vorschau vorbereiten

Nun folgt das letzte Puzzleteil: Ich erzeuge eine CameraCaptureSession, indem ich die Methode createCaptureSession() eines CameraDevice-Objekts aufrufe. Dabei wird ein – Sie ahnen es wahrscheinlich – Callback-Objekt übergeben. In meiner Beispielimplementierung (Listing 11.16) ist dies die Instanzvariable captureSessionCallback. Das ebenfalls übergebene SessionConfiguration-Objekt konfiguriert die Session. Vielleicht wundern Sie sich in diesem Zusammenhang über das Array outputs. Es enthält nämlich zwei OutputConfiguration-Objekte. Eines (dessen Konstruktor hatte ich surfaceview.holder.surface übergeben) sorgt dafür, dass ein kontinuierlicher Datenstrom in der SurfaceView angezeigt wird. Das ist die Live-Vorschau. Das zweite brauchen wir etwas später für die eigentliche Aufnahme.

private val captureSessionCallback = 
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
try {
session.setRepeatingRequest(builderPreview.build(), null, null)
activeSession = session
} catch (e: CameraAccessException) {
Log.e(TAG, "onConfigured()", e)
}
}

override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "onConfigureFailed()")
}
}

Listing 11.16    Ein »CameraCaptureSession.StateCallback«-Objekt

Die abstrakte Klasse CameraCaptureSession.StateCallback erwartet die Implementierung der beiden Methoden onConfigured() und onConfigureFailed(). Letztere wird aufgerufen, wenn die Session aufgrund eines Fehlers nicht genutzt werden kann. onConfigured() signalisiert, dass die Konfiguration der Session erfolgreich war und diese nun verwendet werden kann. In meiner Beispielimplementierung sorgt session.setRepeatingRequest() dafür, dass die Live-Vorschau aktiviert wird. Hierfür wird ein CaptureRequest-Objekt benötigt, das mit builderPreview.build() erzeugt wird.

[+]  Tipp

Wenn Sie Ihre App nicht nur für den Eigengebrauch entwickeln, sondern über Google Play vertreiben möchten, ist es wichtig, in der Manifestdatei zu vermerken, wenn Ihr Programm zwingend eine Kamera voraussetzt. Fügen Sie einfach die Zeile

<uses-feature android:name="android.hardware.camera" 
android:required="true" />

hinzu. Ihre App wird dann nur auf Geräten mit eingebauter Kamera zum Download angeboten. Dies bewahrt Anwender vor Frust und schützt Sie vor unnötigen schlechten Kommentaren. Wenn Ihre App auch ohne Kamera funktioniert, brauchen Sie den Eintrag natürlich nicht. In diesem Fall ist es aber wichtig, sauber auf das nicht Vorhandensein der Hardware zu reagieren, also beispielsweise einen entsprechenden Hinweis anzuzeigen.

Sicherlich fragen Sie sich, wie man eigentlich ein Foto aufnimmt. Eine Live-Vorschau ist zweifellos eine feine Sache, aber irgendwann möchte man schließlich den Auslöser drücken. Was Sie dazu tun müssen, zeige ich Ihnen im folgenden Abschnitt.

Fotos aufnehmen

Um eine Aufnahme auszulösen, soll der Anwender die Live-Vorschau antippen. In der Methode configureHolder() (Listing 11.12) wird mit setOnClickListener() ein Listener gesetzt, der die folgende Methode takePicture() aufruft.

private fun takePicture() {
try {
val builder = camera?.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
builder?.addTarget(imageReader.surface)
builder?.build()?.let {
activeSession?.capture(it, null, null)
}
} catch (e: CameraAccessException) {
Log.e(TAG, "takePicture()", e)
}
}

Listing 11.17    Auf das Antippen der Live-Vorschau reagieren

activeSession verweist auf ein CameraCaptureSession-Objekt. Wir haben es in der Implementierung der Methode onConfigured() von CameraCaptureSession.StateCallback verwendet, um mit setRepeatingRequest() die Live-Vorschau zu aktivieren. Nun rufen wir capture() auf, um das Foto zu schießen. Dafür ist ein CaptureRequest.Builder nötig, den wir mit createCaptureRequest() erzeugen. Das Ziel der Aufnahme (addTarget()) ist jetzt nicht der SurfaceHolder der Live-Vorschau, sondern ein Objekt, das ich über eine Instanzvariable namens imageReader referenziere. Das folgende Codefragment finden Sie in der Methode configureHolder().

imageReader = ImageReader.newInstance(
size.width, size.height,
ImageFormat.JPEG, 2
)
imageReader.setOnImageAvailableListener(
{
Log.d(TAG, "setOnImageAvailableListener()")
val image = imageReader.acquireLatestImage()
val planes = image.planes
val buffer = planes[0].buffer
saveJPG(buffer)
image.close()
}, null
)

Listing 11.18    Die Aufnahme auslesen

Nachdem der ImageReader mit newInstance() erzeugt wurde, muss mit setOnImageAvailableListener() ein OnImageAvailableListener gesetzt werden. Dessen Methode onImageAvailable() ermittelt mit acquireLatestImage() das zuletzt aufgenommene Foto. Objekte des Typs android.media.Image können im Prinzip mehrere Farbebenen haben. Bei ImageReader-Objekten mit dem Format ImageFormat.JPEG liefert getPlanes() aber immer ein Feld der Länge 1. Das Speichern der Bilddaten in eine Datei findet in der Methode saveJPG() statt. Ihr wird als einziges Argument ein ByteBuffer übergeben. Diesen liefert buffer des Objekts Image.Plane (planes[0]).

private fun saveJPG(data: ByteBuffer) {
getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.let {
if (it.mkdirs()) {
Log.d(TAG, "dirs created")
}
val f = File(it, "${TAG}_${System.currentTimeMillis()}.jpg")
try {
FileOutputStream(f).use { fos ->
BufferedOutputStream(fos).use { bos ->
while (data.hasRemaining()) {
bos.write(data.get().toInt())
}
Toast.makeText(
this, R.string.click,
Toast.LENGTH_SHORT
).show()
addToMediaProvider(f)
}
}
} catch (e: IOException) {
Log.e(TAG, "saveJPG()", e)
}
}
}

Listing 11.19    Die Methode »saveJPG()«

saveJPG() speichert Fotos im anwendungsspezifischen Verzeichnis für Bilder auf dem primären externen Medium. Dies geschieht byteweise, solange die ByteBuffer-Methode hasRemaining() den Wert true liefert. Dies ist dennoch effizient, da der Filterstrom BufferedOutputStream vor dem Schreiben genügend große Häppchen ansammelt. Die zum Schluss aufgerufene Methode addToMediaProvider() in Listing 11.20 fügt die Datei der systemweiten Mediendatenbank hinzu und zeigt das Bild an.

private fun addToMediaProvider(f: File) {
MediaScannerConnection.scanFile(
this,
arrayOf(f.toString()),
arrayOf("image/jpeg")
) { _, uri ->
val i = Intent(
Intent.ACTION_VIEW,
uri
)
startActivity(i)
}
}

Listing 11.20    Bild in der zentralen Mediendatenbank speichern

Mit zugegebenermaßen nicht ganz wenig Programmcode haben Sie eine einfache Kamera-App erhalten, die Bilder im JPEG-Format speichert. Zu tun gäbe es aber noch eine ganze Menge. Zum Beispiel ist es schade, dass das Foto keine Informationen bezüglich der Ausrichtung des Sensors speichert. Für Aufnahmen bei ungünstigen Lichtverhältnissen wäre die Ansteuerbarkeit des Blitzes sehr wichtig. Und wie stellt die Kamera eigentlich scharf? Der Weg zu diesen weiter fortgeschrittenen Techniken führt über CameraCaptureSession.CaptureCallback-Objekte.

CaptureCallback implementiert die Methoden onCaptureStarted(), onCapturePartial(), onCaptureProgressed(), onCaptureCompleted() sowie onCaptureFailed(). Mit ihnen wird ein komplexer Lebenszyklus beschrieben, der verschiedene Phasen eines Capture Requests repräsentiert. In den Methodenimplementierungen wird der aktuelle Zustand des Capture Requests geprüft und durch Umkonfigurieren des korrespondierenden Builders und Absetzen eines neuen Capture Requests modifiziert.

 
Zum Seitenanfang

11.3.3    Videos drehen Zur vorigen ÜberschriftZur nächsten Überschrift

In diesem Abschnitt zeige ich Ihnen anhand des Projekts KameraDemo4, wie Sie Videoclips aufzeichnen können. Die Dateien werden im Cacheverzeichnis der App gespeichert und mit dem zentralen Teilen-Dialog an einen beliebigen Empfänger weitergereicht. Dieses Mal verwende ich aber nicht die Klassen der Android-Plattform, sondern die Jetpack-Komponente CameraX. Um sie in Apps verwenden zu können, müssen Sie in der modulspezifischen build.gradle-Datei die folgenden Zeilen eintragen:

def camerax_version = "1.0.0-beta08"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-view:1.0.0-alpha15"

Listing 11.21    Jetpack CameraX hinzufügen

Die Namensbestandteile alpha und beta weisen darauf hin, dass sich die Komponente noch sehr schnell weiterentwickelt. Den jeweils aktuellen Stand finden Sie auf der Seite https://developer.android.com/jetpack/androidx/releases/camera. Leider ist auch die Programmierschnittstelle noch nicht fertig. Sie müssen deshalb damit rechnen, dass Versionswechsel brechende Änderungen enthalten, sie also Anpassungen an Ihrem Code vornehmen müssen, wenn Sie auf neuere Versionen aktualisieren. Allerdings gilt dies auch für andere Jetpack-Komponenten.

CameraX bringt ein einfaches UI-Element mit, das sich nicht nur um das Anzeigen der Vorschau kümmert, sondern auch gleich die ganze Logik mitbringt, um Fotos zu schießen und Videos zu drehen. Listing 11.22 zeigt, wie Sie die Komponente androidx.camera.view.CameraView verwenden.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:text="@string/start" />
<androidx.camera.view.CameraView
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/preview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@+id/button"
android:layout_alignParentTop="true"
app:captureMode="video"
app:flash="auto"
app:lensFacing="back"
app:pinchToZoomEnabled="false"
app:scaleType="fitCenter"
tools:context=".KameraDemo4Activity" />
</RelativeLayout>

Listing 11.22    Die Datei »activity_main.xml«

captureMode legt den Aufnahmemodus fest. flash konfiguriert den Blitz. lensFacing wählt die zu verwendende Kamera aus. Und mit scaleType beeinflussen Sie, ob die Vorschau skaliert und ggf. beschnitten wird. Es ist praktisch, die View schon in der Layoutdatei zu konfigurieren. Natürlich können Sie die Werte aber auch im Code setzen. Um die Kamera verwenden und Ton aufzeichnen zu können, müssen Sie in der Manifestdatei die Berechtigungen android.permission.RECORD_AUDIO und android.permission.CAMERA anfordern und zur Laufzeit prüfen. Die Klasse KameraDemo4Activity (Listing 11.23) überschreibt hierzu wie üblich onRequestPermissionsResult(). Die Prüfung habe ich in die private Methode checkPermissions() ausgelagert.

package com.thomaskuenneth.androidbuch.kamerademo4

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.VideoCapture.*
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import android.util.Log

private const val REQUEST_PERMISSIONS = 123
private val TAG = KameraDemo4Activity::class.simpleName
class KameraDemo4Activity : AppCompatActivity() {
private val requestCameraRecordAudio =
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
private var isRecording = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.isEnabled = false
button.setOnClickListener { toggleRecord() }
updateButton()
if (checkPermissions()) {
preview.post { startCamera() }
} else {
requestPermissions(
requestCameraRecordAudio, REQUEST_PERMISSIONS
)
}
}

override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_PERMISSIONS) {
if (checkPermissions()) {
preview.post { startCamera() }
} else {
Toast.makeText(
this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
finish()
}
}
}

private fun checkPermissions(): Boolean {
for (permission in requestCameraRecordAudio) {
if (checkSelfPermission(
permission
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}

@Throws(SecurityException::class)
private fun startCamera() {
button.isEnabled = true
preview.bindToLifecycle(this)
}

private fun toggleRecord() {
isRecording = !isRecording
updateButton()
if (isRecording) {
val dir = File(cacheDir, "videos")
dir.mkdirs()
val file = File(dir, "${System.currentTimeMillis()}.mp4")
preview.startRecording(file, mainExecutor,
object : OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults:
OutputFileResults) {
Toast.makeText(
this@KameraDemo4Activity,
file.absolutePath,
Toast.LENGTH_LONG
).show()
val uri = FileProvider.getUriForFile(
this@KameraDemo4Activity,
"com.thomaskuenneth.androidbuch.kamerademo4.fileprovider",
file
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = "video/*"
intent.putExtra(Intent.EXTRA_STREAM, uri)
val chooser = Intent.createChooser(intent,

getString(R.string.share))
val l = packageManager.queryIntentActivities(
chooser,
PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in l) {
val packageName = resolveInfo.activityInfo.packageName
grantUriPermission(
packageName,
uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
startActivity(chooser)
}

override fun onError(
videoCaptureError: Int,
message: String,
cause: Throwable?
) {
Log.e(TAG, message, cause)
}
})
} else
preview.stopRecording()
}

private fun updateButton() {
button.text = getString(if (isRecording) R.string.stop
else R.string.start)
}
}

Listing 11.23    Die Klasse »KameraDemo4Activity«

Das Hochfahren der Kamera findet in startCamera() statt. Hierzu ist nur der Aufruf der Methode bindToLifecycle() nötig. Um das Entfernen kümmert sich die Komponente selbstständig; Sie müssen nichts weiter tun. Um die Aufnahme zu starten oder zu beenden, rufen Sie startRecording() bzw. stopRecording() auf. Beides geschieht in toggleRecord(). Das Interface VideoCapture.OnVideoSavedCallback definiert die zwei Methoden onVideoSaved() und onError(). Sie werden im Erfolgs- bzw. im Fehlerfall aufgerufen. Letzteren ignoriert mein Beispiel geflissentlich. Konnte der Clip hingegen gespeichert werden, wird zunächst mit FileProvider.getUriForFile() ein Uri ermittelt und dieser in ein Intent mit der Aktion ACTION_SEND verpackt. startActivity() übergibt es an den Teilen-Dialog des Systems.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern