אם האפליקציה שלכם משתמשת במחלקה המקורית Camera (Camera1), שהוצאה משימוש מאז Android 5.0 (רמת API 21), מומלץ מאוד לעדכן ל-API מודרני של מצלמת Android. Android מציעה את CameraX (ממשק API סטנדרטי, חזק של מצלמה מ-Jetpack) ואת Camera2 (ממשק API ברמת מסגרת נמוכה).
ברוב המקרים, מומלץ להעביר את האפליקציה ל-CameraX. אלו הסיבות לכך:
- קלות השימוש: CameraX מטפלת בפרטים ברמה הנמוכה, כך שאתם יכולים להתמקד פחות בבניית חוויית מצלמה מאפס ויותר בבידול האפליקציה שלכם.
- CameraX מטפל בפיצול בשבילכם: CameraX מפחית את עלויות התחזוקה לטווח ארוך ואת הקוד הספציפי למכשיר, וכך מאפשר למשתמשים ליהנות מחוויה איכותית יותר. מידע נוסף זמין בפוסט בבלוג תאימות טובה יותר של מכשירים עם CameraX.
- יכולות מתקדמות: CameraX תוכננה בקפידה כדי להקל על שילוב פונקציונליות מתקדמת באפליקציה. לדוגמה, אפשר להשתמש ב-CameraX Extensions כדי להוסיף בקלות לתמונות אפקטים כמו בוקה, ריטוש פנים, HDR (טווח דינמי גבוה) ומצב צילום לילה להבהרת תמונות בתנאי תאורה חלשים.
- אפשרות עדכון: במהלך השנה, מערכת Android משיקה יכולות חדשות ותיקוני באגים ל-CameraX. כשמעבירים את האפליקציה ל-CameraX, מקבלים את טכנולוגיית המצלמה העדכנית ביותר של Android עם כל גרסה של CameraX, ולא רק עם הגרסאות השנתיות של Android.
במדריך הזה מפורטים תרחישים נפוצים לשימוש באפליקציות מצלמה. כל תרחיש כולל הטמעה של Camera1 והטמעה של CameraX לצורך השוואה.
כשמבצעים מיגרציה, לפעמים צריך גמישות נוספת כדי לבצע אינטגרציה עם בסיס קוד קיים. כל הקוד של CameraX במדריך הזה הוא הטמעה של CameraController – פתרון מצוין אם אתם רוצים דרך פשוטה יותר להשתמש ב-CameraX – וגם הטמעה של CameraProvider – פתרון מצוין אם אתם צריכים גמישות רבה יותר. כדי לעזור לכם להחליט מה מתאים לכם, הנה היתרונות של כל אחת מהאפשרויות:
CameraController |
CameraProvider |
| נדרש קוד הגדרה קצר | מאפשר שליטה רבה יותר |
| אם מאפשרים ל-CameraX לטפל בחלק גדול יותר של תהליך ההגדרה, תכונות כמו הקשה למיקוד וצביטה לשינוי גודל פועלות באופן אוטומטי. |
מכיוון שמפתח האפליקציה מטפל בהגדרה, יש יותר אפשרויות להתאמה אישית של ההגדרה, כמו הפעלת סיבוב של תמונת הפלט או הגדרת פורמט תמונת הפלט ב-ImageAnalysis
|
הדרישה ל-PreviewView לתצוגה המקדימה של המצלמה מאפשרת ל-CameraX להציע שילוב חלק מקצה לקצה, כמו בשילוב עם ML Kit שלנו, שיכול למפות את קואורדינטות התוצאה של מודל למידת המכונה (כמו תיבות תוחמות של פנים) ישירות לקואורדינטות של התצוגה המקדימה.
|
האפשרות להשתמש ב-`Surface` מותאם אישית לתצוגה מקדימה של המצלמה מאפשרת גמישות רבה יותר, למשל שימוש בקוד `Surface` קיים שיכול להיות קלט לחלקים אחרים באפליקציה |
אם נתקלתם בבעיה במהלך ההעברה, אתם יכולים לפנות אלינו בקבוצת הדיון של CameraX.
לפני ההעברה
השוואה בין השימוש ב-CameraX לבין השימוש ב-Camera1
למרות שהקוד עשוי להיראות שונה, המושגים הבסיסיים ב-Camera1 וב-CameraX דומים מאוד. CameraX מפשטת את הפונקציונליות הנפוצה של המצלמה לתרחישי שימוש, וכתוצאה מכך, הרבה משימות שהיו מוטלות על המפתח ב-Camera1 מטופלות באופן אוטומטי על ידי CameraX. יש ארבעה רכיבי UseCase ב-CameraX, שאפשר להשתמש בהם למגוון משימות שקשורות למצלמה: Preview, ImageCapture, VideoCapture ו-ImageAnalysis.
דוגמה אחת לאופן שבו CameraX מטפל בפרטים ברמה נמוכה עבור מפתחים היא ViewPort שמשותף בין UseCase פעילים. כך מוודאים שכל UseCase יראו בדיוק את אותם פיקסלים. ב-Camera1, אתם צריכים לנהל את הפרטים האלה בעצמכם. בגלל יחסי הגובה-רוחב המשתנים במכשירים שונים, קשה להתאים את התצוגה המקדימה למדיה שצולמה.
דוגמה נוספת: CameraX מטפל בLifecycle קריאות חוזרות באופן אוטומטי במופע Lifecycle שסיפקתם. באמצעות הארכיטקטורה הזו, CameraX מטפלת בחיבור של האפליקציה למצלמה במהלך מחזור החיים של הפעילות ב-Android, כולל במקרים הבאים: סגירת המצלמה כשהאפליקציה עוברת לרקע, הסרת התצוגה המקדימה של המצלמה כשהמסך כבר לא צריך להציג אותה והשהיית התצוגה המקדימה של המצלמה כשפעילות אחרת מקבלת עדיפות בחזית, כמו שיחת וידאו נכנסת.
לבסוף, CameraX מטפל בסיבוב ובשינוי הגודל בלי שתצטרכו להוסיף קוד נוסף. במקרה של Activity עם כיוון תצוגה לא נעול, ההגדרה של UseCase מתבצעת בכל פעם שמסובבים את המכשיר, כי המערכת משמידה ויוצרת מחדש את Activity בשינויים בכיוון התצוגה. כתוצאה מכך, ההגדרה UseCases תגרום לכך שסיבוב היעד יותאם לכיוון התצוגה בכל פעם כברירת מחדל. מידע נוסף על סיבובים ב-CameraX
לפני שנעמיק בפרטים, הנה סקירה כללית של UseCase של CameraX ואיך אפליקציה של Camera1 קשורה אליהם. (המושגים שקשורים ל-CameraX מופיעים בכחול והמושגים שקשורים ל-Camera1 מופיעים בירוק).
CameraX |
|||
| הגדרות של CameraController / CameraProvider | |||
| ↓ | ↓ | ↓ | ↓ |
| תצוגה מקדימה | ImageCapture | VideoCapture | ImageAnalysis |
| ⁞ | ⁞ | ⁞ | ⁞ |
| ניהול משטח התצוגה המקדימה והגדרתו במצלמה | הגדרת PictureCallback והפעלת takePicture() במצלמה | ניהול ההגדרה של Camera ו-MediaRecorder בסדר מסוים | קוד ניתוח מותאם אישית שנבנה על גבי אזור התצוגה המקדימה |
| ↑ | ↑ | ↑ | ↑ |
| קוד ספציפי למכשיר | |||
| ↑ | |||
| ניהול סיבוב והתאמה לעומס (scaling) של מכשירים | |||
| ↑ | |||
| ניהול סשנים של מצלמה (בחירת מצלמה, ניהול מחזור חיים) | |||
מצלמה1 |
|||
תאימות וביצועים ב-CameraX
CameraX תומכת במכשירים עם Android 5.0 (רמת API 21) ומעלה. זה מייצג יותר מ-98% ממכשירי Android הקיימים. CameraX נועד לטפל בהבדלים בין מכשירים באופן אוטומטי, וכך מצמצם את הצורך בקוד ספציפי למכשיר באפליקציה. בנוסף, אנחנו בודקים יותר מ-150 מכשירים פיזיים בכל גרסאות Android החל מגרסה 5.0 ב-CameraX Test Lab שלנו. אפשר לעיין ברשימה המלאה של המכשירים ב-Test Lab.
CameraX משתמשת ב-Executor כדי להפעיל את מחסנית המצלמה. אם לאפליקציה שלכם יש דרישות ספציפיות לגבי שרשורים, אתם יכולים להגדיר מפעיל משלכם ב-CameraX.
אם לא מגדירים את המדיניות, CameraX יוצרת ומשתמשת ב-Executor פנימי אופטימלי שמוגדר כברירת מחדל.
הרבה ממשקי API של הפלטפורמה שעליהם מבוסס CameraX דורשים חסימה של תקשורת בין תהליכים (IPC) עם חומרה, שלפעמים לוקח לה מאות אלפיות השנייה להגיב. לכן, CameraX קורא לממשקי ה-API האלה רק מ-threads ברקע, כדי לוודא שה-thread הראשי לא נחסם ושממשק המשתמש נשאר חלק. מידע נוסף על שרשורים
אם שוק היעד של האפליקציה שלכם כולל מכשירים ברמת כניסה, CameraX מספקת דרך לקצר את זמן ההגדרה באמצעות מגביל מצלמה. תהליך החיבור לרכיבי חומרה יכול לקחת זמן, במיוחד במכשירים פשוטים. לכן, אפשר לציין את קבוצת המצלמות שהאפליקציה צריכה. CameraX מתחבר למצלמות האלה רק במהלך ההגדרה. לדוגמה, אם האפליקציה משתמשת רק במצלמות שפונות לאחור, היא יכולה להגדיר את התצורה הזו באמצעות DEFAULT_BACK_CAMERA, ואז CameraX לא יאתחל מצלמות שפונות קדימה כדי לצמצם את זמן האחזור.
מושגים בפיתוח ל-Android
המדריך הזה מיועד למי שמכיר באופן כללי את פיתוח Android. בנוסף למושגים הבסיסיים, כדאי להבין את שני המושגים הבאים לפני שמתעמקים בקוד שבהמשך:
- View Binding יוצר מחלקה של View Binding עבור קובצי פריסת ה-XML, וכך מאפשר לכם להפנות לתצוגות בפעילויות, כמו שמוצג בכמה קטעי קוד בהמשך. יש כמה הבדלים בין view binding לבין
findViewById()(השיטה הקודמת להפניה לתצוגות), אבל בקוד הבא אמורה להיות אפשרות להחליף את השורות של view binding בקריאה דומה שלfindViewById(). - שגרות המשך (coroutine) אסינכרוניות הן דפוס עיצוב של בו-זמניות (concurrency) שנוסף ב-Kotlin 1.3, שאפשר להשתמש בו כדי לטפל בשיטות של CameraX שמחזירות
ListenableFuture. החל מגרסה 1.1.0, ספריית Jetpack Concurrent מקלה על התהליך הזה. כדי להוסיף קורוטינה אסינכרונית לאפליקציה:- מוסיפים את
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")לקובץ Gradle. - מציבים כל קוד CameraX שמחזיר
ListenableFutureבבלוקlaunchאו בפונקציית השהיה. - מוסיפים קריאה ל-
await()לבקשה להפעלת פונקציה שמחזירהListenableFuture. - להבנה עמוקה יותר של האופן שבו קורוטינות פועלות, אפשר לעיין במדריך הפעלת קורוטינה.
- מוסיפים את
העברת תרחישים נפוצים
בקטע הזה מוסבר איך להעביר תרחישים נפוצים מ-Camera1 ל-CameraX.
כל תרחיש כולל הטמעה של Camera1, הטמעה של CameraX CameraProvider
והטמעה של CameraX CameraController.
בחירת מצלמה
באפליקציית המצלמה, אחת האפשרויות הראשונות שמומלץ להציע היא אפשרות לבחור מצלמות שונות.
מצלמה1
ב-Camera1, אפשר להתקשר אל Camera.open() בלי פרמטרים כדי לפתוח את המצלמה האחורית הראשונה, או להעביר מזהה של מספר שלם למצלמה שרוצים לפתוח. דוגמה לאופן שבו זה יכול להיראות:
// 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
ב-CameraX, בחירת המצלמה מתבצעת על ידי המחלקה CameraSelector. CameraX
מפשטת את המקרה הנפוץ של שימוש במצלמה שמוגדרת כברירת מחדל. אתם יכולים לציין אם אתם רוצים שהמצלמה הקדמית או האחורית יהיו ברירת המחדל. בנוסף, אובייקט CameraControl של CameraX מאפשר להגדיר את רמת הזום באפליקציה. לכן, אם האפליקציה פועלת במכשיר שתומך במצלמות לוגיות, היא תעבור לעדשה המתאימה.
זהו קוד CameraX לשימוש במצלמה האחורית שמוגדרת כברירת מחדל עם 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
דוגמה לבחירת המצלמה הקדמית כברירת מחדל באמצעות CameraProvider
(אפשר להשתמש במצלמה הקדמית או האחורית באמצעות CameraController או
CameraProvider):
// CameraX: select a camera with CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the preceding "Android development concepts" // section. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Set up UseCases (more on UseCases in later scenarios) var useCases:Array= ... // Set the cameraSelector to use the default front-facing (selfie) // camera. val cameraSelector = CameraSelector.DEFAULT_FRONT_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() } }
אם רוצים לשלוט במצלמה שנבחרה, אפשר לעשות זאת גם ב-CameraX באמצעות CameraProvider על ידי קריאה ל-getAvailableCameraInfos(), שמחזירה אובייקט CameraInfo לבדיקת מאפיינים מסוימים של המצלמה, כמו isFocusMeteringSupported(). אחר כך אפשר להמיר אותו ל-CameraSelector כדי להשתמש בו כמו בדוגמאות הקודמות עם השיטה CameraInfo.getCameraSelector().
אפשר לקבל פרטים נוספים על כל מצלמה באמצעות המחלקה Camera2CameraInfo. מתקשרים אל getCameraCharacteristic() עם מפתח לנתוני המצלמה הרצויים. אפשר לעיין בכיתה CameraCharacteristics כדי לראות רשימה של כל המפתחות שאפשר לשלוח לגביהם שאילתות.
דוגמה לשימוש בפונקציה מותאמת אישית checkFocalLength() שאפשר להגדיר בעצמכם:
// 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()
הצגת תצוגה מקדימה
ברוב אפליקציות המצלמה, פיד המצלמה צריך להופיע על המסך בשלב מסוים. ב-Camera1, צריך לנהל את הקריאות החוזרות (callback) של מחזור החיים בצורה נכונה, וגם לקבוע את הסיבוב והשינוי של גודל התצוגה המקדימה.
בנוסף, ב-Camera1 צריך להחליט אם להשתמש ב-TextureView או ב-SurfaceView כמשטח התצוגה המקדימה. לשתי האפשרויות יש חסרונות, ובכל מקרה, ב-Camera1 צריך לטפל בסיבוב ובשינוי הגודל בצורה נכונה. לעומת זאת, ל-PreviewView של CameraX יש הטמעות בסיסיות גם ל-TextureView וגם ל-SurfaceView. CameraX מחליט איזו הטמעה היא הטובה ביותר בהתאם לגורמים כמו סוג המכשיר וגרסת Android שבה האפליקציה פועלת. אם אחת מההטמעות תואמת, אפשר להצהיר על ההעדפה באמצעות PreviewView.ImplementationMode. האפשרות
COMPATIBLE משתמשת ב-TextureView לתצוגה המקדימה, והערך PERFORMANCE
משתמש ב-SurfaceView (כשאפשר).
מצלמה1
כדי להציג תצוגה מקדימה, צריך לכתוב מחלקה משלכם Preview עם הטמעה של הממשק android.view.SurfaceHolder.Callback, שמשמש להעברת נתוני תמונה מחומרת המצלמה לאפליקציה.
לאחר מכן, לפני שמתחילים את התצוגה המקדימה של התמונה בשידור חי, צריך להעביר את המחלקה Preview לאובייקט 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
ב-CameraX, יש הרבה פחות דברים שהמפתחים צריכים לנהל. אם משתמשים ב-CameraController, צריך להשתמש גם ב-PreviewView. המשמעות היא שהערך
Preview UseCase מוגדר כברירת מחדל, ולכן ההגדרה דורשת הרבה פחות עבודה:
// 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
עם CameraProvider של CameraX, לא צריך להשתמש ב-PreviewView, אבל הוא עדיין מפשט מאוד את הגדרת התצוגה המקדימה בהשוואה ל-Camera1. לצורך ההדגמה, בדוגמה הזו נעשה שימוש ב-PreviewView, אבל אפשר לכתוב SurfaceProvider מותאם אישית כדי להעביר אותו אל setSurfaceProvider() אם יש לכם צרכים מורכבים יותר.
במקרה הזה, לא מניחים מראש את הערך של Preview UseCase כמו בפונקציה CameraController, ולכן צריך להגדיר אותו:
// 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 preceding "Android development concepts" // section. 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() } }
הקשה להתמקדות
כשהתצוגה המקדימה של המצלמה מופיעה במסך, אחת מהפעולות הנפוצות היא הגדרת נקודת המיקוד כשמשתמש מקיש על התצוגה המקדימה.
מצלמה1
כדי להטמיע את התכונה 'הקשה לפוקוס' ב-Camera1, צריך לחשב את הפוקוס האופטימלי
Area כדי לציין איפה Camera צריך לנסות להתמקד. הערך Area מועבר אל setFocusAreas(). בנוסף, צריך להגדיר מצב התמקדות תואם בתג Camera. לאזור ההתמקדות יש השפעה רק אם מצב ההתמקדות הנוכחי הוא FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO או FOCUS_MODE_CONTINUOUS_PICTURE.
כל Area הוא מלבן עם משקל מוגדר. המשקל הוא ערך בין 1 ל-1,000, והוא משמש לקביעת סדר העדיפויות של המיקוד Areas אם מוגדרים כמה אזורים. בדוגמה הזו נעשה שימוש רק ב-Area אחד, ולכן ערך המשקל לא משנה. הקואורדינטות של טווח המלבן הן מ--1000 עד 1000. הנקודה השמאלית העליונה היא (-1000, -1000).
הנקודה השמאלית התחתונה היא (1000, 1000). הכיוון הוא יחסי לכיוון החיישן, כלומר למה שהחיישן רואה. הכיוון לא מושפע מהסיבוב או מהשיקוף של Camera.setDisplayOrientation(), ולכן צריך להמיר את הקואורדינטות של אירוע המגע לקואורדינטות של החיישן.
// 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 מאזין לאירועי מגע של PreviewView כדי לטפל באופן אוטומטי בהקשה להגדרת פוקוס. אפשר להפעיל ולהשבית את ההגדרה 'הקשה למיקוד' באמצעות setTapToFocusEnabled(), ולבדוק את הערך באמצעות הפונקציה המתאימה לאחזור ערכים isTapToFocusEnabled().
השיטה getTapToFocusState() מחזירה אובייקט LiveData למעקב אחרי שינויים במצב המיקוד ב-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{ state -> when (state) { CameraController.TAP_TO_FOCUS_NOT_STARTED -> Log.d(TAG, "tap-to-focus init") CameraController.TAP_TO_FOCUS_STARTED -> Log.d(TAG, "tap-to-focus started") CameraController.TAP_TO_FOCUS_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focus successful)") CameraController.TAP_TO_FOCUS_NOT_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focused unsuccessful)") CameraController.TAP_TO_FOCUS_FAILED -> Log.d(TAG, "tap-to-focus failed") } } cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX: CameraProvider
כשמשתמשים ב-CameraProvider, צריך לבצע הגדרה מסוימת כדי שהתכונה 'הקשה לפוקוס' תפעל. בדוגמה הזו נניח שאתם משתמשים ב-PreviewView. אם לא, צריך להתאים את הלוגיקה כך שתחול על Surface מותאם אישית.
אלה השלבים לשימוש ב-PreviewView:
- מגדירים זיהוי תנועות כדי לטפל באירועי הקשה.
- בעזרת אירוע ההקשה, יוצרים
MeteringPointבאמצעותMeteringPointFactory.createPoint(). - בעזרת
MeteringPoint, יוצריםFocusMeteringAction. - עם האובייקט
CameraControlב-Camera(שהוחזר מ-bindToLifecycle()), מתקשרים אלstartFocusAndMetering()ומעבירים אתFocusMeteringAction. - (אופציונלי) מגיבים להודעה של
FocusMeteringResult. - מגדירים את הכלי לזיהוי תנועות כך שיגיב לאירועי מגע ב-
PreviewView.setOnTouchListener().
// 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 // preceding "Android development concepts" section. 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-zoom scenario for scaleGestureDetector definition. var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { didConsume = gestureDetector.onTouchEvent(event) } didConsume }
שינוי מרחק התצוגה באמצעות צביטה
שינוי התצוגה של התצוגה המקדימה של המצלמה הוא עוד מניפולציה ישירה נפוצה. בגלל שמספר המצלמות במכשירים הולך וגדל, המשתמשים מצפים שהעדשה עם אורך המוקד הטוב ביותר תיבחר באופן אוטומטי כתוצאה של שימוש בזום.
מצלמה1
יש שתי דרכים לעשות זום באמצעות Camera1. השיטה Camera.startSmoothZoom() יוצרת אנימציה מרמת הזום הנוכחית לרמת הזום שמעבירים. השיטה
Camera.Parameters.setZoom() עוברת ישירות לרמת הזום שמעבירים לה. לפני שמשתמשים באחת מהשיטות האלה, צריך להתקשר אל isSmoothZoomSupported() או אל isZoomSupported() כדי לוודא ששיטות הזום שרוצים להשתמש בהן זמינות במצלמה.
כדי להטמיע את התכונה 'צביטה לשינוי גודל התצוגה', נעשה שימוש בדוגמה הזו ב-setZoom() כי ה-touch listener במשטח התצוגה המקדימה מפעיל אירועים באופן רציף בזמן הצביטה, ולכן הוא מעדכן את רמת הזום באופן מיידי בכל פעם. המחלקות ZoomTouchListener מוגדרות בהמשך הקטע הזה, וצריך להגדיר אותן כקריאה חוזרת למאזין המגע של משטח התצוגה המקדימה.
// 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
בדומה להקשה למיקוד, CameraController מאזין לאירועי המגע של PreviewView כדי לטפל בזום באמצעות צביטה באופן אוטומטי. אפשר להפעיל ולהשבית את התכונה 'צביטה לזום' באמצעות setPinchToZoomEnabled(), ולבדוק את הערך באמצעות הפונקציה המתאימה isPinchToZoomEnabled().
השיטה getZoomState() מחזירה אובייקט LiveData למעקב אחרי שינויים ב-ZoomState ב-CameraController.
// CameraX: track the state of pinch-to-zoom over the Lifecycle of // a PreviewView, logging the linear zoom ratio. val pinchToZoomStateObserver = Observer{ state -> val zoomRatio = state.getZoomRatio() Log.d(TAG, "ptz-zoom-ratio $zoomRatio") } cameraController.getZoomState().observe(this, pinchToZoomStateObserver)
CameraX: CameraProvider
כדי להשתמש בתנועת צביטה לשינוי גודל התצוגה ב-CameraProvider, צריך לבצע הגדרה מסוימת. אם אתם לא משתמשים ב-PreviewView, אתם צריכים להתאים את הלוגיקה כך שתחול על Surface בהתאמה אישית.
אלה השלבים לשימוש ב-PreviewView:
- מגדירים ScaleGestureDetector כדי לטפל באירועי צביטה.
- מקבלים את
ZoomStateמהאובייקטCamera.CameraInfo, כאשר מופעלת קריאה ל-bindToLifecycle()ומוחזרת דוגמה שלCamera. - אם ל-
ZoomStateיש ערךzoomRatio, שומרים אותו כיחס הזום הנוכחי. אם איןzoomRatioב-ZoomState, משתמשים בשיעור ברירת המחדל של הזום במצלמה (1.0). - כדי לקבוע את יחס הזום החדש, מכפילים את יחס הזום הנוכחי ב-
scaleFactorומעבירים את התוצאה אלCameraControl.setZoomRatio(). - מגדירים את הכלי לזיהוי תנועות כך שיגיב לאירועי מגע ב-
PreviewView.setOnTouchListener().
// 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-zoom scenario for gestureDetector definition. didConsume = gestureDetector.onTouchEvent(event) } didConsume }
צילום תמונה
בקטע הזה מוסבר איך להפעיל צילום תמונה, בין אם צריך לעשות את זה בלחיצה על כפתור הצילום, אחרי שהטיימר הסתיים או בכל אירוע אחר שתבחרו.
מצלמה1
ב-Camera1, קודם מגדירים Camera.PictureCallback כדי לנהל את נתוני התמונה כשמתבצעת בקשה. הנה דוגמה פשוטה ל-PictureCallback
לטיפול בנתוני תמונות 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) } }
אחר כך, בכל פעם שרוצים לצלם תמונה, קוראים לשיטה takePicture() במופע Camera. לשיטה takePicture() הזו יש שלושה פרמטרים שונים לסוגי נתונים שונים. הפרמטר הראשון הוא של ShutterCallback (שלא מוגדר בדוגמה הזו). הפרמטר השני הוא PictureCallback לטיפול בנתוני המצלמה הגולמיים (לא דחוסים). הפרמטר השלישי הוא זה שמשמש בדוגמה הזו, כי הוא PictureCallback לטיפול בנתוני תמונות בפורמט JPEG.
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX: CameraController
CameraX CameraController שומרת על הפשטות של Camera1 לצילום תמונות באמצעות הטמעה של שיטה משלה takePicture(). כאן מגדירים פונקציה ליצירת רשומה של MediaStore ולצילום תמונה שתשמר בה.
// 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
הצילום באמצעות CameraProvider מתבצע כמעט באותו אופן כמו הצילום באמצעות CameraController, אבל קודם צריך ליצור ולקשור ImageCapture
UseCase כדי שיהיה אובייקט שאפשר להפעיל עליו את 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)
אחר כך, בכל פעם שרוצים לצלם תמונה, אפשר להפעיל את הפונקציה
ImageCapture.takePicture(). בדוגמה המלאה של הפונקציה takePhoto() אפשר לעיין בקוד 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( ... ) }
הקלטת סרטון
הקלטת סרטון היא תהליך מורכב בהרבה מהתרחישים שבדקנו עד עכשיו. צריך להגדיר כל חלק בתהליך בצורה נכונה, בדרך כלל בסדר מסוים. בנוסף, יכול להיות שתצטרכו לוודא שהסרטון והאודיו מסונכרנים או לטפל בחוסר עקביות נוסף במכשיר.
כפי שניתן לראות, CameraX מטפל שוב בהרבה מהמורכבות הזו בשבילכם.
מצלמה1
צילום וידאו באמצעות Camera1 מחייב ניהול קפדני של Camera ושל MediaRecorder, והקריאה למתודות צריכה להתבצע בסדר מסוים. חשוב לפעול לפי הסדר הזה כדי שהאפליקציה תפעל בצורה תקינה:
- פותחים את המצלמה.
- מכינים תצוגה מקדימה ומתחילים אותה (אם האפליקציה מציגה את הסרטון בזמן ההקלטה, וזה בדרך כלל המצב).
- כדי לבטל את הנעילה של המצלמה לשימוש על ידי
MediaRecorder, צריך להתקשר אלCamera.unlock(). - כדי להגדיר את ההקלטה, מתקשרים לשיטות האלה ב-
MediaRecorder:- מקשרים את מופע
CameraאלsetCamera(camera). - רוצה להתקשר ל-
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)? - רוצה להתקשר ל-
setVideoSource(MediaRecorder.VideoSource.CAMERA)? - מתקשרים למספר
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))כדי להגדיר את האיכות.CamcorderProfileמציג את כל האפשרויות של איכות. - רוצה להתקשר ל-
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())? - אם באפליקציה יש תצוגה מקדימה של הווידאו, מתקשרים ל
setPreviewDisplay(preview?.holder?.surface). - רוצה להתקשר ל-
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)? - רוצה להתקשר ל-
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)? - רוצה להתקשר ל-
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)? - מתקשרים אל
prepare()כדי לסיים את ההגדרה שלMediaRecorder.
- מקשרים את מופע
- כדי להתחיל להקליט, מתקשרים אל
MediaRecorder.start(). - כדי להפסיק את ההקלטה, קוראים לשיטות האלה. שוב, צריך לפעול בדיוק לפי הסדר הזה:
- רוצה להתקשר ל-
MediaRecorder.stop()? - אפשר להסיר את ההגדרה הנוכחית של
MediaRecorderבאמצעות הקריאה ל-MediaRecorder.reset(). - רוצה להתקשר ל-
MediaRecorder.release()? - כדי לנעול את המצלמה כך שיהיה אפשר להשתמש בה בסשנים עתידיים של
MediaRecorder, צריך להתקשר אלCamera.lock().
- רוצה להתקשר ל-
- כדי להפסיק את התצוגה המקדימה, צריך להתקשר אל
Camera.stopPreview(). - לבסוף, כדי לשחרר את
Cameraכך שתהליכים אחרים יוכלו להשתמש בו, קוראים ל-Camera.release().
אלה כל השלבים יחד:
// 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
באמצעות CameraController של CameraX, אפשר להפעיל או להשבית את ImageCapture, VideoCapture ו-ImageAnalysis UseCases באופן עצמאי, כל עוד אפשר להשתמש ברשימת UseCases במקביל. ההרשאות ImageCapture ו-ImageAnalysis UseCaseמופעלות כברירת מחדל, ולכן לא היית צריך להתקשר אל setEnabledUseCases() כדי לצלם תמונה.
כדי להשתמש בCameraController לצילום סרטונים, קודם צריך להשתמש בsetEnabledUseCases() כדי לאפשר את VideoCapture UseCase.
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
כדי להתחיל לצלם סרטון, אפשר לקרוא לפונקציה CameraController.startRecording(). הפונקציה הזו יכולה לשמור את הסרטון המוקלט ב-File, כמו שרואים בדוגמה הבאה.
בנוסף, צריך להעביר Executor וגם מחלקה שמטמיעה את OnVideoSavedCallback כדי לטפל בקריאות חוזרות להצלחה ולשגיאה. כשרוצים להפסיק את ההקלטה, לוחצים על CameraController.stopRecording().
הערה: אם אתם משתמשים ב-CameraX בגרסה 1.3.0-alpha02 ואילך, יש פרמטר נוסף, AudioConfig, שמאפשר להפעיל או להשבית את הקלטת האודיו בסרטון. כדי להפעיל הקלטת אודיו, צריך לוודא שיש לכם הרשאות גישה למיקרופון. בנוסף, השיטה stopRecording() הוסרה בגרסה 1.3.0-alpha02, והשיטה startRecording() מחזירה אובייקט Recording שאפשר להשתמש בו להשהיה, להפעלה מחדש ולהפסקת צילום הווידאו.
// 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
אם אתם משתמשים ב-CameraProvider, אתם צריכים ליצור VideoCapture
UseCase ולהעביר אובייקט Recorder. ב-Recorder.Builder, אפשר להגדיר את איכות הווידאו, ואם רוצים, גם FallbackStrategy, שמטפל במקרים שבהם המכשיר לא עומד במפרט האיכות הרצוי. לאחר מכן, מקשרים את מופע VideoCapture ל-CameraProvider עם שאר 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: VideoCaptureprivate var recording: Recording? = null ... // Create a Recorder instance to set on a VideoCapture instance (can be // added with other UseCase definitions). val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() videoCapture = VideoCapture.withOutput(recorder) ... // Bind UseCases to camera (adding videoCapture along with preview here, but // preview is not required to use videoCapture). 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, videoCapture)
בשלב הזה, אפשר לגשת אל Recorder בנכס videoCapture.output. Recorder יכולים להתחיל הקלטות של שיחות וידאו שנשמרות ב-File, ParcelFileDescriptor או MediaStore. בדוגמה הזו נשתמש ב-MediaStore.
ב-Recorder, יש כמה שיטות להכנה של שיחה. מתקשרים אל
prepareRecording() כדי להגדיר את אפשרויות הפלט של MediaStore. אם לאפליקציה יש הרשאה להשתמש במיקרופון של המכשיר, צריך גם להתקשר אל withAudioEnabled().
לאחר מכן, מתקשרים אל start() כדי להתחיל את ההקלטה, ומעבירים הקשר ורכיב event listener Consumer<VideoRecordEvent> כדי לטפל באירועי הקלטת וידאו. אם הפעולה בוצעה ללא שגיאות, אפשר להשתמש בערך Recording שמוחזר כדי להשהות, להמשיך או להפסיק את ההקלטה.
// 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 } } } } }
מקורות מידע נוספים
יש לנו כמה אפליקציות מלאות של CameraX במאגר הדוגמאות של Camera ב-GitHub. בדוגמאות האלה אפשר לראות איך התרחישים שבמדריך הזה משתלבים באפליקציית Android מלאה.
אם אתם רוצים לקבל תמיכה נוספת במעבר ל-CameraX או שיש לכם שאלות לגבי חבילת Android Camera APIs, אתם יכולים לפנות אלינו בקבוצת הדיון של CameraX.