Jeśli Twoja aplikacja korzysta z pierwotnej klasy Camera
(„Camera1”), która została wycofana w wersji Androida 5.0 (poziom API 21), zdecydowanie zalecamy przejście na nowy interfejs API aparatu na Androida. Android oferuje CameraX (standardowy, niezawodny interfejs API Jetpack do obsługi aparatu) i Camera2 (interfejs API frameworku na niskim poziomie). W większości przypadków zalecamy przeniesienie aplikacji do CameraX. Przyczyna jest następująca:
- Łatwość obsługi: CameraX obsługuje szczegóły niskiego poziomu, dzięki czemu możesz mniej skupiać się na tworzeniu aparatu od podstaw, a bardziej na wyróżnieniu aplikacji.
- CameraX radzi sobie z fragmentacją: CameraX zmniejsza długoterminowe koszty konserwacji i kodu związanego z konkretnym urządzeniem, zapewniając użytkownikom wyższą jakość. Więcej informacji znajdziesz w poście na blogu Zwiększenie zgodności z urządzeniami dzięki CameraX.
- Funkcje zaawansowane: aplikacja CameraX została zaprojektowana tak, aby ułatwić Ci stosowanie zaawansowanych funkcji w aplikacji. Możesz na przykład łatwo stosować efekt bokeh, retusz twarzy, HDR (High Dynamic Range) i tryb nocny do swoich zdjęć za pomocą rozszerzeń CameraX.
- Możliwość aktualizacji: w ciągu roku Android wprowadza nowe funkcje i poprawki błędów w CameraX. Dzięki migracji na CameraX Twoja aplikacja będzie korzystać z najnowszej technologii aparatu Androida w każdej wersji CameraX, a nie tylko w rocznych wersjach Androida.
W tym przewodniku znajdziesz typowe scenariusze dotyczące aplikacji do obsługi aparatu. Każdy scenariusz zawiera implementację Camera1 i CameraX do porównania.
W przypadku migracji czasami potrzebujesz większej elastyczności, aby zintegrować się z istniejącą bazą kodu. Cały kod CameraX w tym przewodniku ma implementację CameraController
, która jest świetna, jeśli chcesz użyć CameraX w najprostszy sposób, a także implementację CameraProvider
, która jest świetna, jeśli potrzebujesz większej elastyczności. Aby ułatwić Ci wybór odpowiedniej opcji, przedstawiamy korzyści płynące z każdej z nich:
CameraController |
CameraProvider |
Wymaga niewielkiej ilości kodu konfiguracyjnego | Większa kontrola |
Przekazanie aplikacji CameraX większej części procesu konfiguracji oznacza, że funkcje takie jak skupianie przez dotknięcie czy powiększanie przez zbliżenie działają automatycznie. |
Ponieważ konfiguracją zajmuje się deweloper aplikacji, masz więcej możliwości dostosowania konfiguracji, na przykład włączenia funkcji obracania obrazu wyjściowego lub ustawienia formatu obrazu wyjściowego w pliku ImageAnalysis .
|
Wymaganie PreviewView w przypadku podglądu aparatu umożliwia CameraX oferowanie płynnej integracji end-to-end, podobnie jak w przypadku integracji z ML Kit, która może mapować współrzędne wyników modelu ML (np. ramki ograniczające twarz) bezpośrednio na współrzędne podglądu.
|
Możliwość używania niestandardowej „powierzchni” do podglądu aparatu daje większą elastyczność, np. możliwość użycia dotychczasowego kodu „powierzchni” jako danych wejściowych w innych częściach aplikacji. |
Jeśli napotkasz problemy podczas migracji, skontaktuj się z nami w grupie dyskusyjnej CameraX.
Zanim przeprowadzisz migrację
Porównanie wykorzystania aplikacji CameraX i Camera1
Chociaż kod może wyglądać inaczej, zastosowane w Camera1 i CameraX koncepcje są bardzo podobne. CameraX umieszcza wspólne funkcje aparatu w ramach konkretnych zastosowań, dzięki czemu wiele zadań, które w aplikacji Camera1 należało do dewelopera, jest wykonywanych automatycznie przez CameraX. W aplikacji CameraX są 4 UseCase
, które możesz stosować do różnych zadań związanych z aparatem: Preview
, ImageCapture
, VideoCapture
i ImageAnalysis
.
Przykładem obsługi przez CameraX szczegółów niskiego poziomu dla deweloperów jest ViewPort
, który jest udostępniany aktywnym UseCase
. Dzięki temu wszystkie UseCase
widzą dokładnie te same piksele.
W aplikacji Camera1 musisz samodzielnie zarządzać tymi szczegółami, a z uwagi na zmienność formatów obrazu na czujnikach i ekranach aparatów na różnych urządzeniach może być trudno zapewnić, aby podgląd pasował do zrobionych zdjęć i filmów.
Innym przykładem jest CameraX, który automatycznie obsługuje wywołania zwrotne Lifecycle
w przekazanej instancji Lifecycle
. Oznacza to, że CameraX obsługuje połączenie aplikacji z kamerą przez cały cykl życia aktywności Androida, w tym w takich przypadkach: gdy aplikacja przechodzi na drugi plan, gdy nie jest już potrzebne wyświetlanie podglądu kamery, oraz gdy inna aktywność ma pierwszeństwo, np. przychodzący połączenie wideo.
Na koniec CameraX obsługuje obracanie i skalowanie bez potrzeby dodawania dodatkowego kodu. W przypadku Activity
z odblokowaną orientacją konfiguracja UseCase
jest wykonywana za każdym razem, gdy urządzenie zostanie obrócone, ponieważ system niszczy i ponownie tworzy Activity
po zmianie orientacji. W rezultacie UseCases
ustawia domyślnie dopasowanie obrotu do orientacji wyświetlacza.
Więcej informacji o obrotach w CameraX
Zanim przejdziemy do szczegółów, przyjrzyjmy się ogólnie interfejsom API CameraXUseCase
i aplikacji Camera1. (Koncepcje dotyczące aparatu X są w kolorze niebieskim, a koncepcje dotyczące aparatu 1 – w kolorze zielonym.)
CameraX |
|||
Konfiguracja CameraController / CameraProvider | |||
↓ | ↓ | ↓ | ↓ |
Podgląd | ImageCapture | VideoCapture | ImageAnalysis |
⁞ | ⁞ | ⁞ | ⁞ |
Zarządzanie powierzchnią podglądu i jej ustawieniami w aparacie | Ustaw PictureCallback i wywołaj takePicture() w Aparacie | Zarządzanie konfiguracją aparatu i MediaRecorder w określonej kolejności | niestandardowy kod analizy utworzony na podstawie interfejsu Surface w wersji podglądu; |
↑ | ↑ | ↑ | ↑ |
Kod dla konkretnego urządzenia | |||
↑ | |||
Zarządzanie rotacją i skalowaniem urządzeń | |||
↑ | |||
Zarządzanie sesją kamery (wybór kamery, zarządzanie cyklem życia) | |||
Camera1 |
Zgodność i wydajność w aplikacji CameraX
CameraX obsługuje urządzenia z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. To ponad 98% dotychczasowych urządzeń z Androidem. Biblioteka CameraX została zaprojektowana tak, aby automatycznie obsługiwać różnice między urządzeniami, co zmniejsza potrzebę stosowania w aplikacji kodu specyficznego dla danego urządzenia. Ponadto w naszym Laboratorium testowym CameraX testujemy ponad 150 fizycznych urządzeń z Androidem we wszystkich wersjach od 5.0. Możesz przejrzeć pełną listę urządzeń obecnie używanych w Test Lab.
CameraX używa Executor
do obsługi zestawu kamer. Jeśli Twoja aplikacja ma określone wymagania dotyczące wątków, możesz ustawić własny wykonawca w CameraX. Jeśli nie jest ustawiony, CameraX tworzy i używa domyślnego wewnętrznego Executor
. Wiele interfejsów API platformy, na której opiera się CameraX, wymaga blokowania komunikacji międzyprocesowej (IPC) z urządzeniami, na odpowiedź których czasami trzeba czekać setki milisekund. Z tego powodu CameraX wywołuje te interfejsy API tylko z wątków w tle, co zapewnia, że wątek główny nie jest zablokowany i interfejs użytkownika pozostaje płynny.
Więcej informacji o wątkach
Jeśli na rynku docelowym Twojej aplikacji znajdują się urządzenia niskiej klasy, CameraX umożliwia skrócenie czasu konfiguracji za pomocą ogranicznika kamery. Proces łączenia z urządzeniami może zająć sporo czasu, zwłaszcza na urządzeniach niskiej klasy. Możesz więc określić zestaw kamer, których potrzebuje Twoja aplikacja. Aplikacja CameraX łączy się z tymi kamerami tylko podczas konfiguracji. Jeśli na przykład aplikacja korzysta tylko z tylnego aparatu, może ustawić tę konfigurację za pomocą DEFAULT_BACK_CAMERA
, a następnie CameraX uniknie inicjowania przednich aparatów, aby zmniejszyć opóźnienie.
Koncepcje związane z tworzeniem aplikacji na Androida
W tym przewodniku zakładamy, że znasz podstawy programowania aplikacji na Androida. Oprócz podstaw warto poznać kilka pojęć, które pomogą Ci zrozumieć kod poniżej:
- View Binding generuje klasę wiązania dla plików układu XML, co umożliwia łatwe odwoływanie się do widoków w aktywnościach, jak to zostało pokazane w kilku fragmentach kodu poniżej. Istnieją pewne różnice między view binding a
findViewById()
(poprzedni sposób odniesienia do widoków), ale w poniżej zamieszczonym kodzie możesz zastąpić wiersze view binding podobnym wywołaniemfindViewById()
. - Asynchroniczne coroutines to wzór projektowania współbieżności dodany w Kotlinie 1.3, który można wykorzystać do obsługi metod CameraX zwracających wartość
ListenableFuture
. Ułatwia to biblioteka Concurrent z Jetpacka w wersji 1.1.0. Aby dodać do aplikacji asynchroniczną coroutine:- Dodaj
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
do pliku Gradle. - Umieść kod CameraX, który zwraca
ListenableFuture
, w blokulaunch
lub w funkcji wstrzymywania. - Dodaj do wywołania funkcji, które zwraca wartość
ListenableFuture
, wywołanie funkcjiawait()
. - Aby dowiedzieć się więcej o tym, jak działają coroutine, zapoznaj się z przewodnikiem uruchamiania coroutine.
- Dodaj
Przenoszenie w typowych sytuacjach
Z tej sekcji dowiesz się, jak przenieść typowe scenariusze z Camera1 do CameraX.
Każdy scenariusz obejmuje implementację Camera1, implementację CameraXCameraProvider
i implementację CameraXCameraController
.
Wybieranie kamery
Jedną z pierwszych rzeczy, które warto dodać do aplikacji aparatu, jest możliwość wyboru różnych kamer.
Camera1
W Camera1 możesz wywołać metodę Camera.open()
bez parametrów, aby otworzyć pierwszy tylny aparat, lub możesz przekazać identyfikator całkowity aparatu, który chcesz otworzyć. Oto przykład, jak to może wyglądać:
// Camera1: select a camera from id.
// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.
private fun safeCameraOpen(id: Int): Boolean {
return try {
releaseCameraAndPreview()
camera = Camera.open(id)
true
} catch (e: Exception) {
Log.e(TAG, "failed to open camera", e)
false
}
}
private fun releaseCameraAndPreview() {
preview?.setCamera(null)
camera?.release()
camera = null
}
CameraX: CameraController
W CameraX wybór aparatu jest obsługiwany przez klasę CameraSelector
. CameraX ułatwia korzystanie z domyślnego aparatu. Możesz określić, czy chcesz użyć domyślnej przedniej kamery czy domyślnej tylnej kamery. Ponadto obiekt CameraControl
w CameraX umożliwia łatwe ustawienie poziomu powiększenia w aplikacji. Jeśli aplikacja działa na urządzeniu obsługującym logiczne aparaty, przełączy się na odpowiedni obiektyw.
Oto kod CameraX do korzystania z domyślnego tylnego aparatu w ramach CameraController
:
// CameraX: select a camera with CameraController
var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector
CameraX: CameraProvider
Oto przykład wyboru domyślnego przedniego aparatu za pomocą właściwości CameraProvider
(dostępne są opcje CameraController
lub CameraProvider
):
// CameraX: select a camera with CameraProvider.
// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
// Set up UseCases (more on UseCases in later scenarios)
var useCases:Array
Jeśli chcesz kontrolować, która kamera jest wybrana, możesz to zrobić w CameraX, jeśli używasz CameraProvider
, wywołując getAvailableCameraInfos()
, co daje Ci obiekt CameraInfo
do sprawdzania niektórych właściwości aparatu, takich jak isFocusMeteringSupported()
.
Następnie możesz przekształcić go w element CameraSelector
, który będzie używany w sposób podobny do pokazanego w powyższych przykładach z metodą CameraInfo.getCameraSelector()
.
Więcej informacji o poszczególnych kamerach znajdziesz w klasie Camera2CameraInfo
. Wywołaj funkcję getCameraCharacteristic()
z kluczem do wybranych danych z kamery. Sprawdź klasę CameraCharacteristics
, aby zobaczyć listę wszystkich kluczy, których możesz używać w zapytaniach.
Oto przykład użycia niestandardowej funkcji checkFocalLength()
, którą możesz zdefiniować samodzielnie:
// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().
val cameraInfo = cameraProvider.getAvailableCameraInfos()
.first { cameraInfo ->
val focalLengths = Camera2CameraInfo.from(cameraInfo)
.getCameraCharacteristic(
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
)
return checkFocalLength(focalLengths)
}
val cameraSelector = cameraInfo.getCameraSelector()
Wyświetlanie podglądu
Większość aplikacji do obsługi kamery musi w jakimś momencie wyświetlić obraz z kamery na ekranie. W przypadku Camera1 musisz prawidłowo zarządzać wywołaniami zwrotnymi cyklu życia, a także określić obrót i powiększenie podglądu.
Dodatkowo w Kamera1 musisz zdecydować, czy chcesz użyć jako powierzchni podglądu TextureView
czy SurfaceView
.
Oba rozwiązania mają swoje wady, a w obu przypadkach Camera1 wymaga prawidłowego obsługiwania obracania i powiększania. Z drugiej strony, interfejs PreviewView
w CameraX ma implementacje zarówno dla TextureView
, jak i SurfaceView
.
CameraX wybiera najlepszą implementację na podstawie takich czynników jak typ urządzenia i wersja Androida, na której działa aplikacja. Jeśli którakolwiek z implementacji jest zgodna, możesz określić preferowany format za pomocą parametru PreviewView.ImplementationMode
.
Opcja COMPATIBLE
używa TextureView
do podglądu, a wartość PERFORMANCE
używa SurfaceView
(jeśli to możliwe).
Camera1
Aby wyświetlić podgląd, musisz napisać własną klasę Preview
z implementacją interfejsu android.view.SurfaceHolder.Callback
, który służy do przekazywania danych obrazu z aparatu do aplikacji. Następnie, zanim rozpoczniesz podgląd obrazu na żywo, klasa Preview
musi zostać przekazana do obiektu Camera
.
// Camera1: set up a camera preview.
class Preview(
context: Context,
private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {
private val holder: SurfaceHolder = holder.apply {
addCallback(this@Preview)
setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
}
override fun surfaceCreated(holder: SurfaceHolder) {
// The Surface has been created, now tell the camera
// where to draw the preview.
camera.apply {
try {
setPreviewDisplay(holder)
startPreview()
} catch (e: IOException) {
Log.d(TAG, "error setting camera preview", e)
}
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// Take care of releasing the Camera preview in your activity.
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int,
w: Int, h: Int) {
// If your preview can change or rotate, take care of those
// events here. Make sure to stop the preview before resizing
// or reformatting it.
if (holder.surface == null) {
return // The preview surface does not exist.
}
// Stop preview before making changes.
try {
camera.stopPreview()
} catch (e: Exception) {
// Tried to stop a non-existent preview; nothing to do.
}
// Set preview size and make any resize, rotate or
// reformatting changes here.
// Start preview with new settings.
camera.apply {
try {
setPreviewDisplay(holder)
startPreview()
} catch (e: Exception) {
Log.d(TAG, "error starting camera preview", e)
}
}
}
}
class CameraActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var camera: Camera? = null
private var preview: Preview? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Create an instance of Camera.
camera = getCameraInstance()
preview = camera?.let {
// Create the Preview view.
Preview(this, it)
}
// Set the Preview view as the content of the activity.
val cameraPreview: FrameLayout = viewBinding.cameraPreview
cameraPreview.addView(preview)
}
}
CameraX: CameraController
W CameraX masz znacznie mniej elementów do zarządzania. Jeśli używasz reguły CameraController
, musisz też użyć reguły PreviewView
. Oznacza to, że Preview
UseCase
jest domyślnie ustawiony, co znacznie ułatwia konfigurację:
// CameraX: set up a camera preview with a CameraController.
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Create the CameraController and set it on the previewView.
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
val previewView: PreviewView = viewBinding.cameraPreview
previewView.controller = cameraController
}
}
CameraX: CameraProvider
Dzięki funkcji CameraProvider
w CameraX nie musisz używać funkcji PreviewView
, ale znacznie upraszcza to konfigurowanie podglądu w porównaniu z Camera1. W tym przykładzie na potrzeby demonstracji użyto funkcji PreviewView
, ale jeśli masz bardziej złożone potrzeby, możesz napisać niestandardową funkcję SurfaceProvider
i przekazać ją do funkcji setSurfaceProvider()
.
W tym przypadku Preview
UseCase
nie jest domyślnie ustawiony tak jak w przypadku CameraController
, więc musisz go skonfigurować:
// CameraX: set up a camera preview with a CameraProvider.
// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
val cameraProvider = ProcessCameraProvider.getInstance(this).await()
// Create Preview UseCase.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(
viewBinding.viewFinder.surfaceProvider
)
}
// Select default back camera.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind UseCases before rebinding.
cameraProvider.unbindAll()
// Bind UseCases to camera. This function returns a camera
// object which can be used to perform operations like zoom,
// flash, and focus.
var camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCases)
} catch(exc: Exception) {
Log.e(TAG, "UseCase binding failed", exc)
}
})
...
// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
lifecycleScope.launch {
startCamera()
}
}
Dotknij, aby wyostrzyć
Gdy podgląd kamery jest widoczny na ekranie, można ustawić punkt ostrości, gdy użytkownik kliknie podgląd.
Camera1
Aby w aplikacji Camera1 wdrożyć funkcję „dotknij, aby ustawić ostrość”, musisz obliczyć optymalny punkt ostrości Area
, aby wskazać, gdzie Camera
ma się skupić. Ten element Area
jest przekazywany do elementu setFocusAreas()
. Musisz też ustawić zgodny tryb fokusa na urządzeniu Camera
. Obszar skupienia ma zastosowanie tylko wtedy, gdy bieżący tryb skupienia to FOCUS_MODE_AUTO
, FOCUS_MODE_MACRO
, FOCUS_MODE_CONTINUOUS_VIDEO
lub FOCUS_MODE_CONTINUOUS_PICTURE
.
Każdy element Area
to prostokąt o określonej wadze. Waga to wartość z zakresu 1–1000, która służy do ustalania priorytetów Areas
, jeśli ustawiono kilka takich wartości. W tym przykładzie jest używana tylko jedna wartość Area
, więc wartość wagi nie ma znaczenia. Współrzędne prostokąta mieszczą się w zakresie od -1000 do 1000. Punkt w lewym górnym rogu ma współrzędne (-1000, -1000).
Punkt w prawym dolnym rogu ma współrzędne (1000, 1000). Kierunek jest określany względem orientacji czujnika, czyli tego, co widzi czujnik. Kierunek nie jest zależny od obracania ani lustrzanego odbicia Camera.setDisplayOrientation()
, więc musisz przekształcić współrzędne zdarzenia dotykowego w współrzędne czujnika.
// Camera1: implement tap-to-focus.
class TapToFocusHandler : Camera.AutoFocusCallback {
private fun handleFocus(event: MotionEvent) {
val camera = camera ?: return
val parameters = try {
camera.getParameters()
} catch (e: RuntimeException) {
return
}
// Cancel previous auto-focus function, if one was in progress.
camera.cancelAutoFocus()
// Create focus Area.
val rect = calculateFocusAreaCoordinates(event.x, event.y)
val weight = 1 // This value's not important since there's only 1 Area.
val focusArea = Camera.Area(rect, weight)
// Set the focus parameters.
parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
parameters.setFocusAreas(listOf(focusArea))
// Set the parameters back on the camera and initiate auto-focus.
camera.setParameters(parameters)
camera.autoFocus(this)
}
private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
// Define the size of the Area to be returned. This value
// should be optimized for your app.
val focusAreaSize = 100
// You must define functions to rotate and scale the x and y values to
// be values between 0 and 1, where (0, 0) is the upper left-hand side
// of the preview, and (1, 1) is the lower right-hand side.
val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000
// Calculate the values for left, top, right, and bottom of the Rect to
// be returned. If the Rect would extend beyond the allowed values of
// (-1000, -1000, 1000, 1000), then crop the values to fit inside of
// that boundary.
val left = max(normalizedX - (focusAreaSize / 2), -1000)
val top = max(normalizedY - (focusAreaSize / 2), -1000)
val right = min(left + focusAreaSize, 1000)
val bottom = min(top + focusAreaSize, 1000)
return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
}
override fun onAutoFocus(focused: Boolean, camera: Camera) {
if (!focused) {
Log.d(TAG, "tap-to-focus failed")
}
}
}
CameraX: CameraController
CameraController
nasłuchuje zdarzeń dotyku PreviewView
, aby automatycznie obsługiwać funkcję „dotknij, aby wyśrodkować”. Funkcję „dotknij, aby wyśrodkować” można włączyć i wyłączyć za pomocą metody setTapToFocusEnabled()
, a wartość można sprawdzić za pomocą odpowiedniej metody gettera isTapToFocusEnabled()
.
Metoda getTapToFocusState()
zwraca obiekt LiveData
, który służy do śledzenia zmian stanu fokusa na elemencie CameraController
.
// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.
val tapToFocusStateObserver = Observer
CameraX: CameraProvider
Aby korzystać z funkcji „dotknij, aby wyśrodkować”, musisz najpierw skonfigurować CameraProvider
. W tym przykładzie założono, że używasz tagu PreviewView
. Jeśli nie, musisz dostosować logikę do zastosowania w przypadku niestandardowego Surface
.
Oto, co należy zrobić, gdy używasz PreviewView
:
- Skonfiguruj detektor gestów, aby obsługiwał zdarzenia dotyku.
- W przypadku zdarzenia dotknięcia utwórz
MeteringPoint
za pomocą funkcjiMeteringPointFactory.createPoint()
. - Utwórz
FocusMeteringAction
za pomocąMeteringPoint
. - Gdy obiekt
CameraControl
jest dostępny w TwoimCamera
(zwrócony zbindToLifecycle()
), wywołaj funkcjęstartFocusAndMetering()
, przekazując jej argumentFocusMeteringAction
. - (Opcjonalnie) Odpowiedz na
FocusMeteringResult
. - W
PreviewView.setOnTouchListener()
ustaw, aby wykrywanie gestów reagowało na zdarzenia dotykowe.
// CameraX: implement tap-to-focus with CameraProvider.
// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
object : SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
val previewView = previewView ?: return
val camera = camera ?: return
val meteringPointFactory = previewView.meteringPointFactory
val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
val meteringAction = FocusMeteringAction
.Builder(meteringPoint).build()
lifecycleScope.launch {
val focusResult = camera.cameraControl
.startFocusAndMetering(meteringAction).await()
if (!result.isFocusSuccessful()) {
Log.d(TAG, "tap-to-focus failed")
}
}
}
}
)
...
// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
// See pinch-to-zooom scenario for scaleGestureDetector definition.
var didConsume = scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
didConsume = gestureDetector.onTouchEvent(event)
}
didConsume
}
Rozciągnij, aby powiększyć
Powiększanie i pomniejszanie podglądu to kolejna powszechna metoda bezpośredniej manipulacji podglądem kamery. Wraz ze wzrostem liczby aparatów na urządzeniach użytkownicy oczekują, że obiektyw o najlepszej ogniskowej zostanie automatycznie wybrany w wyniku przybliżenia.
Camera1
Zdjęcia można powiększać na 2 sposoby. Metoda Camera.startSmoothZoom()
animuje przejście od bieżącego poziomu powiększenia do podanego przez Ciebie poziomu powiększenia. Metoda Camera.Parameters.setZoom()
przechodzi bezpośrednio do poziomu powiększenia, który podasz. Zanim użyjesz którejkolwiek z nich, zadzwoń odpowiednio do isSmoothZoomSupported()
lub isZoomSupported()
, aby upewnić się, że potrzebne metody powiększenia są dostępne w aparacie.
Aby zaimplementować funkcję powiększania przez zbliżanie, przykład używa setZoom()
, ponieważ w miarę wykonywania przez użytkownika gestu zbliżania na powierzchni podglądu ciągle wywołuje on zdarzenia, które aktualizują poziom powiększenia natychmiast po każdym takim geście. Poniżej zdefiniowano klasę ZoomTouchListener
, która powinna być ustawiona jako wywołanie zwrotne dla interfejsu dotykowego powierzchni podglądu.
// Camera1: implement pinch-to-zoom.
// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
object : ScaleGestureDetector.OnScaleGestureListener {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val camera = camera ?: return false
val parameters = try {
camera.parameters
} catch (e: RuntimeException) {
return false
}
// In case there is any focus happening, stop it.
camera.cancelAutoFocus()
// Set the zoom level on the Camera.Parameters, and set
// the Parameters back onto the Camera.
val currentZoom = parameters.zoom
parameters.setZoom(detector.scaleFactor * currentZoom)
camera.setParameters(parameters)
return true
}
}
)
// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean =
scaleGestureDetector.onTouchEvent(event)
}
// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
view.setOnTouchListener(ZoomTouchListener())
}
CameraX: CameraController
Podobnie jak w przypadku funkcji „dotknij, aby wyostrzyć”, CameraController
reaguje na zdarzenia dotykowe w PreviewView, aby automatycznie obsługiwać powiększanie przez rozciąganie. Możesz włączać i wyłączać funkcję powiększania za pomocą palców za pomocą metody setPinchToZoomEnabled()
, a wartość sprawdzać za pomocą odpowiedniej metody gettera isPinchToZoomEnabled()
.
Metoda getZoomState()
zwraca obiekt LiveData
do śledzenia zmian w ZoomState
w CameraController
.
// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.
val pinchToZoomStateObserver = Observer
CameraX: CameraProvider
Aby funkcja powiększania za pomocą gestów działała w aplikacji CameraProvider
, musisz ją skonfigurować. Jeśli nie używasz funkcji PreviewView
, musisz dostosować logikę do zastosowania w przypadku niestandardowej funkcji Surface
.
Oto, co należy zrobić, gdy używasz PreviewView
:
- Skonfiguruj detektor gestów przesunięcia, aby obsługiwać gesty ściśnięcia.
- Pobierz
ZoomState
z obiektuCamera.CameraInfo
, gdzie zwracany jest obiektCamera
, gdy wywołaszbindToLifecycle()
. - Jeśli element
ZoomState
ma wartośćzoomRatio
, zapisz ją jako bieżący współczynnik powiększenia. Jeśli w elementachZoomState
nie ma wartościzoomRatio
, użyj domyślnego współczynnika powiększenia aparatu (1,0). - Aby określić nowy współczynnik powiększenia, należy pomnożyć bieżący współczynnik powiększenia przez
scaleFactor
i przekazać go doCameraControl.setZoomRatio()
. - W
PreviewView.setOnTouchListener()
ustaw, aby wykrywanie gestów reagowało na zdarzenia dotykowe.
// CameraX: implement pinch-to-zoom with CameraProvider.
// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
object : SimpleOnGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val camera = camera ?: return
val zoomState = camera.cameraInfo.zoomState
val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
camera.cameraControl.setZoomRatio(
detector.scaleFactor * currentZoomRatio
)
}
}
)
...
// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
var didConsume = scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
// See pinch-to-zooom scenario for gestureDetector definition.
didConsume = gestureDetector.onTouchEvent(event)
}
didConsume
}
Robienie zdjęcia
Z tej sekcji dowiesz się, jak wywołać zrobienie zdjęcia, niezależnie od tego, czy chcesz to zrobić po naciśnięciu przycisku migawki, po upływie czasu lub po innym zdarzeniu.
Camera1
W ramach usługi Camera1 najpierw definiujesz parametr Camera.PictureCallback
, aby zarządzać danymi obrazu, gdy zostanie to poproszone. Oto prosty przykład funkcji PictureCallback
do obsługi danych obrazu JPEG:
// Camera1: define a Camera.PictureCallback to handle JPEG data.
private val picture = Camera.PictureCallback { data, _ ->
val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
Log.d(TAG,
"error creating media file, check storage permissions")
return@PictureCallback
}
try {
val fos = FileOutputStream(pictureFile)
fos.write(data)
fos.close()
} catch (e: FileNotFoundException) {
Log.d(TAG, "file not found", e)
} catch (e: IOException) {
Log.d(TAG, "error accessing file", e)
}
}
Następnie, gdy chcesz zrobić zdjęcie, wywołujesz metodę takePicture()
w obiekcie Camera
. Ta metoda takePicture()
ma 3 różne parametry dla różnych typów danych. Pierwszy parametr to ShutterCallback
(niezdefiniowany w tym przykładzie). Drugi parametr to PictureCallback
, który obsługuje dane w postaci nieskompresowanych danych z kamery. Trzeci parametr jest używany w tym przykładzie, ponieważ jest to PictureCallback
do obsługi danych obrazu JPEG.
// Camera1: call takePicture on Camera instance, passing our PictureCallback.
camera?.takePicture(null, null, picture)
CameraX: CameraController
Funkcja CameraController
w CameraX zachowuje prostotę Camera1 w zakresie rejestrowania obrazu, ponieważ implementuje własną metodę takePicture()
. Tutaj zdefiniuj funkcję konfigurowania wpisu MediaStore
i zrób zdjęcie, które ma zostać zapisane.
// CameraX: define a function that uses CameraController to take a photo.
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private fun takePhoto() {
// Create time stamped name and MediaStore entry.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// Create output options object which contains file + metadata.
val outputOptions = ImageCapture.OutputFileOptions
.Builder(context.getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken.
cameraController.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(e: ImageCaptureException) {
Log.e(TAG, "photo capture failed", e)
}
override fun onImageSaved(
output: ImageCapture.OutputFileResults
) {
val msg = "Photo capture succeeded: ${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
}
CameraX: CameraProvider
Robienie zdjęć za pomocą CameraProvider
działa prawie tak samo jak w przypadku CameraController
, ale najpierw musisz utworzyć i powiązać ImageCapture
UseCase
, aby mieć obiekt, w którym można wywołać funkcję takePicture()
:
// CameraX: create and bind an ImageCapture UseCase.
// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null
...
// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()
...
// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
Gdy chcesz zrobić zdjęcie, możesz zadzwonić do ImageCapture.takePicture()
. Pełny przykład funkcji takePhoto()
znajdziesz w tym dziale w kodzie CameraController
.
// CameraX: define a function that uses CameraController to take a photo.
private fun takePhoto() {
// Get a stable reference of the modifiable ImageCapture UseCase.
val imageCapture = imageCapture ?: return
...
// Call takePicture on imageCapture instance.
imageCapture.takePicture(
...
)
}
Nagrywanie filmu
Nagrywanie filmu jest znacznie bardziej skomplikowane niż do tej pory analizowane scenariusze. Każdy etap procesu musi być prawidłowo skonfigurowany, zwykle w określonej kolejności. Może też być konieczne sprawdzenie, czy film i dźwięk są zsynchronizowane, lub rozwiązanie dodatkowych problemów z urządzeniem.
Jak zobaczysz, CameraX ponownie zadba o wiele z tych złożonych kwestii.
Camera1
Nagrywanie filmów za pomocą Camera1 wymaga ostrożnego zarządzania metodami Camera
i MediaRecorder
. Metody te muszą być wywoływane w określonej kolejności. Aby aplikacja działała prawidłowo, musisz postępować zgodnie z tym porządkiem:
- Otwórz aparat.
- Przygotuj i uruchom podgląd (jeśli aplikacja wyświetla nagrywany film, co zwykle ma miejsce).
- Odblokuj kamerę na potrzeby
MediaRecorder
, dzwoniąc na numerCamera.unlock()
. - Skonfiguruj nagrywanie, wywołując te metody na
MediaRecorder
:- Połącz instancję
Camera
zsetCamera(camera)
. - Będziesz dzwonić pod numer
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
. - Będziesz dzwonić pod numer
setVideoSource(MediaRecorder.VideoSource.CAMERA)
. - Zadzwoń pod numer
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
, aby ustawić jakość. Wszystkie opcje jakości znajdziesz w sekcjiCamcorderProfile
. - Będziesz dzwonić pod numer
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
. - Jeśli Twoja aplikacja ma podgląd wideo, zadzwoń pod numer
setPreviewDisplay(preview?.holder?.surface)
. - Będziesz dzwonić pod numer
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
. - Będziesz dzwonić pod numer
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
. - Będziesz dzwonić pod numer
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
. - Aby dokończyć konfigurowanie
MediaRecorder
, zadzwoń pod numerprepare()
.
- Połącz instancję
- Aby rozpocząć nagrywanie, zadzwoń pod numer
MediaRecorder.start()
. - Aby zatrzymać nagrywanie, wywołaj te metody. Ponownie postępuj zgodnie z tą samą kolejnością:
- Będziesz dzwonić pod numer
MediaRecorder.stop()
. - Opcjonalnie możesz usunąć bieżącą konfigurację
MediaRecorder
, wywołując funkcjęMediaRecorder.reset()
. - Będziesz dzwonić pod numer
MediaRecorder.release()
. - Zablokuj kamerę, aby w przyszłości sesje
MediaRecorder
mogły z niej korzystać, wywołującCamera.lock()
.
- Będziesz dzwonić pod numer
- Aby zatrzymać podgląd, zadzwoń pod numer
Camera.stopPreview()
. - Aby uwolnić
Camera
, aby inne procesy mogły z niego korzystać, wywołaj funkcjęCamera.release()
.
Oto wszystkie te czynności w połączeniu:
// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.
// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false
...
private fun prepareMediaRecorder(): Boolean {
mediaRecorder = MediaRecorder()
// Unlock and set camera to MediaRecorder.
camera?.unlock()
mediaRecorder?.run {
setCamera(camera)
// Set the audio and video sources.
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
setVideoSource(MediaRecorder.VideoSource.CAMERA)
// Set a CamcorderProfile (requires API Level 8 or higher).
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))
// Set the output file.
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
// Set the preview output.
setPreviewDisplay(preview?.holder?.surface)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
// Prepare configured MediaRecorder.
return try {
prepare()
true
} catch (e: IllegalStateException) {
Log.d(TAG, "preparing MediaRecorder failed", e)
releaseMediaRecorder()
false
} catch (e: IOException) {
Log.d(TAG, "setting MediaRecorder file failed", e)
releaseMediaRecorder()
false
}
}
return false
}
private fun releaseMediaRecorder() {
mediaRecorder?.reset()
mediaRecorder?.release()
mediaRecorder = null
camera?.lock()
}
private fun startStopVideo() {
if (isRecording) {
// Stop recording and release camera.
mediaRecorder?.stop()
releaseMediaRecorder()
camera?.lock()
isRecording = false
// This is a good place to inform user that video recording has stopped.
} else {
// Initialize video camera.
if (prepareVideoRecorder()) {
// Camera is available and unlocked, MediaRecorder is prepared, now
// you can start recording.
mediaRecorder?.start()
isRecording = true
// This is a good place to inform the user that recording has
// started.
} else {
// Prepare didn't work, release the camera.
releaseMediaRecorder()
// Inform user here.
}
}
}
CameraX: CameraController
Za pomocą interfejsu CameraController
w ramach CameraX możesz niezależnie przełączać ImageCapture
,
VideoCapture
i ImageAnalysis
UseCase
,
o ile tylko lista przypadków użycia może być używana jednocześnie.
Domyślnie funkcje ImageCapture
i ImageAnalysis
UseCase
są włączone, więc nie musisz dzwonić do setEnabledUseCases()
, aby zrobić zdjęcie.
Aby używać CameraController
do nagrywania filmów, musisz najpierw użyć setEnabledUseCases()
, aby zezwolić VideoCapture
na UseCase
.
// CameraX: Enable VideoCapture UseCase on CameraController.
cameraController.setEnabledUseCases(VIDEO_CAPTURE);
Aby rozpocząć nagrywanie filmu, możesz wywołać funkcję CameraController.startRecording()
. Ta funkcja umożliwia zapisanie nagranego filmu w File
, jak widać na przykładzie poniżej. Dodatkowo musisz przekazać Executor
i klasę, która implementuje OnVideoSavedCallback
, aby obsługiwać wywołania zwrotne powodzenia i błędu. Gdy nagranie ma się zakończyć, zadzwoń pod numer CameraController.stopRecording()
.
Uwaga: jeśli używasz CameraX 1.3.0-alpha02 lub nowszej wersji, dostępny jest dodatkowy parametr AudioConfig
, który umożliwia włączenie lub wyłączenie nagrywania dźwięku w filmie. Aby włączyć nagrywanie dźwięku, musisz mieć uprawnienia do korzystania z mikrofonu.
Dodatkowo w wersji 1.3.0-alpha02 została usunięta metoda stopRecording()
, a metoda startRecording()
zwraca obiekt Recording
, który można wykorzystać do wstrzymywania, wznawiania i zatrzymywania nagrywania filmu.
// CameraX: implement video capture with CameraController.
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: OutputFileResults) {
val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
override fun onError(videoCaptureError: Int, message: String,
cause: Throwable?) {
Log.d(TAG, "error saving video: $message", cause)
}
}
private fun startStopVideo() {
if (cameraController.isRecording()) {
// Stop the current recording session.
cameraController.stopRecording()
return
}
// Define the File options for saving the video.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val outputFileOptions = OutputFileOptions
.Builder(File(this.filesDir, name))
.build()
// Call startRecording on the CameraController.
cameraController.startRecording(
outputFileOptions,
ContextCompat.getMainExecutor(this),
VideoSaveCallback()
)
}
CameraX: CameraProvider
Jeśli używasz CameraProvider
, musisz utworzyć VideoCapture
UseCase
i przekazać obiekt Recorder
. W Recorder.Builder
możesz ustawić jakość filmu i opcjonalnie FallbackStrategy
, która umożliwia wyświetlanie treści w przypadku, gdy urządzenie nie może spełnić wymagań dotyczących jakości. Następnie powiązać instancję VideoCapture
z CameraProvider
za pomocą innych UseCase
.
// CameraX: create and bind a VideoCapture UseCase with CameraProvider.
// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
W tej chwili usługa Recorder
jest dostępna w usłudze videoCapture.output
. Użytkownik Recorder
może nagrywać filmy, które są zapisywane w urządzeniu File
, ParcelFileDescriptor
lub MediaStore
. W tym przykładzie użyto znacznika MediaStore
.
W przypadku Recorder
możesz użyć kilku metod, aby go przygotować. Wybierz opcję prepareRecording()
, aby skonfigurować opcje wyjściowe MediaStore
. Jeśli aplikacja ma uprawnienia do korzystania z mikrofonu urządzenia, zadzwoń też na numer withAudioEnabled()
.
Następnie wywołaj funkcję start()
, aby rozpocząć nagrywanie, przekazując kontekst i słuchacza zdarzeń Consumer<VideoRecordEvent>
, który będzie obsługiwać zdarzenia nagrywania wideo. Jeśli operacja się powiedzie, zwrócona wartość Recording
może służyć do wstrzymywania, wznawiania i zatrzymywania nagrywania.
// CameraX: implement video capture with CameraProvider.
private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private fun startStopVideo() {
val videoCapture = this.videoCapture ?: return
if (recording != null) {
// Stop the current recording session.
recording.stop()
recording = null
return
}
// Create and start a new recording session.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply {
text = getString(R.string.stop_capture)
isEnabled = true
}
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(
baseContext, msg, Toast.LENGTH_SHORT
).show()
Log.d(TAG, msg)
} else {
recording?.close()
recording = null
Log.e(TAG, "video capture ends with error",
recordEvent.error)
}
viewBinding.videoCaptureButton.apply {
text = getString(R.string.start_capture)
isEnabled = true
}
}
}
}
}
Dodatkowe materiały
W repozytorium GitHub z przykładowymi aplikacjami CameraX znajdziesz kilka kompletnych aplikacji. Te przykłady pokazują, jak scenariusze opisane w tym przewodniku pasują do pełnej aplikacji na Androida.
Jeśli potrzebujesz dodatkowej pomocy przy migracji do CameraX lub masz pytania dotyczące zestawu interfejsów CameraX API na Androida, skontaktuj się z nami w grupie dyskusyjnej CameraX.