העברת Camera1 ל- CameraX

אם באפליקציה נעשה שימוש בגרסה המקורית של Camera class ('camera1'), שהוצא משימוש מאז Android 5.0 (רמת API 21), מומלץ מאוד לעדכן ל-API מודרני של מצלמת Android. מבצעים ל-Android cameraX (מצלמת Jetpack סטנדרטית וחזקה API) ו-Camera2 (ממשק API של framework ברמה נמוכה). עבור ברוב המקרים, מומלץ להעביר את האפליקציה ל- CameraX. הנה תיאור הסיבות לכך:

  • קלות השימוש: CameraX מטפל בפרטים ברמה נמוכה, כדי שתוכל להתמקד פחות ביצירת חוויית צילום מאפס, להבדיל בין האפליקציה שלכם.
  • מצלמת CameraX מטפלת בפרגמנטציה: CameraX מפחיתה את הטווח הארוך עלויות התחזוקה וקוד ספציפי למכשיר, וכך לקבל חוויות ברמה גבוהה יותר למשתמשים. ניתן למצוא מידע נוסף בנושא תאימות טובה יותר למכשירים עם CameraX לפוסט בבלוג.
  • יכולות מתקדמות: CameraX תוכנן בקפידה כדי להפוך פונקציונליות שקל לשלב באפליקציה. לדוגמה, אתם יכולים לקבוע בקלות שימוש באפקט בוקה, ריטוש פנים, HDR (טווח דינמי גבוה) והבהרה בתאורה חלשה מצב צילום לילה בתמונות שלך באמצעות תוספי CameraX.
  • אפשרות לקבל עדכונים: ב-Android יש יכולות חדשות ותיקוני באגים ב- CameraX במהלך השנה. לאחר ההעברה אל CameraX, האפליקציה שלך תקבל את גרסת Android העדכנית ביותר טכנולוגיית מצלמה בכל גרסה של CameraX, לא רק גרסאות שנתיות של Android.

במדריך הזה תלמדו תרחישים נפוצים לאפליקציות מצלמה. כל אחד כולל הטמעה של Camera1 והטמעת CameraX עבור להשוואה.

בנושא העברה, לפעמים נדרשת גמישות נוספת כדי לבצע את השילוב עם Codebase קיים. כל קוד CameraX במדריך הזה כולל CameraController מצוין - אם אתם רוצים את הדרך הפשוטה ביותר להשתמש ב- CameraX - CameraProvider טוב יותר – אם אתם צריכים יותר גמישות. כדי לעזור לכם להחליט במיוחד בשבילך, הנה היתרונות של כל אחת מהן:

שלט רחוק למצלמה

CameraProvider

נדרש מעט קוד הגדרה שליטה רבה יותר
המשמעות של מתן הרשאה ל- CameraX לטפל בחלק גדול יותר של תהליך ההגדרה היא פונקציונליות כמו 'הקשה למיקוד' ו'צביטה לשינוי מרחק התצוגה' פועלות באופן אוטומטי מכיוון שמפתחי האפליקציות מטפלים בתהליך ההגדרה, יש יותר הזדמנויות כדי להתאים אישית את התצורה, למשל להפעיל סיבוב תמונה של פלט או הגדרת הפורמט של תמונת הפלט ב-ImageAnalysis
אם צריך PreviewView לתצוגה המקדימה של המצלמה, אפשר CameraX להציע שילוב חלק מקצה לקצה, כמו בערכת למידת המכונה שלנו שיכול למפות את קואורדינטות התוצאה של מודל למידת מכונה (כמו פנים) תיבות תוחמות) ישירות לקואורדינטות של התצוגה המקדימה היכולת להשתמש ב-'Surface' מותאם אישית לתצוגה המקדימה של המצלמה מאפשרת גמישות רבה יותר, כמו שימוש בקוד 'Surface' הקיים שלכם, יכול להיות קלט לחלקים אחרים באפליקציה

אם נתקעת במהלך ניסיון ההעברה, אפשר לפנות אלינו קבוצת דיון של CameraX.

לפני ההעברה

השוואת השימוש ב- CameraX ל- Camera1

הקוד עשוי להיראות שונה, אבל העקרונות הבסיסיים של Camera1 ו CameraX דומים מאוד. מצלמהX מפשטים את הפונקציונליות המשותפת של המצלמה בתרחישים לדוגמה. וכתוצאה מכך, משימות רבות שנותרו למפתח ב- Camera1 מטופל באופן אוטומטי על ידי CameraX. יש ארבעה UseCase ב- CameraX, שאותו אפשר משמשים למגוון משימות במצלמה: Preview, ImageCapture, VideoCapture, וגם ImageAnalysis.

דוגמה אחת לטיפול בפרטים ברמה נמוכה למפתחים של CameraX היא ViewPort שמשותף בין UseCase שנ' פעילות. כך כל הUseCase יראו בדיוק את אותם הפיקסלים. ב- Camera1, עליכם לנהל את הפרטים האלה בעצמכם, תוך התחשבות בשונות ביחסי גובה-רוחב במכשירים שונים חיישני המצלמה והמסכים, עלולים להיות מסובכים מוודאים שהתצוגה המקדימה תואמת לתמונות ולסרטונים שצולמו.

דוגמה נוספת, CameraX מטפל ב-Lifecycle קריאות חוזרות (callback) באופן אוטומטי מכונה אחת (Lifecycle) שמעבירים אותה. כלומר, CameraX מטפל חיבור למצלמה במשך כל מחזור החיים של הפעילות ב-Android, כולל המקרים הבאים: סגירת המצלמה כשהאפליקציה עוברת רקע; להסיר את התצוגה המקדימה של המצלמה כשאין יותר צורך במסך הצגתו, והשהיית התצוגה המקדימה של המצלמה כשמתבצעת פעילות אחרת קדימות בחזית, כמו שיחת וידאו נכנסת.

לבסוף, CameraX מטפל בסיבוב ובשינוי קנה מידה ללא צורך בקוד נוסף מצידכם. במקרה של Activity עם כיוון לא נעול, המאפיין ההגדרה UseCase מתבצעת בכל פעם שמסובבים את המכשיר, מכיוון שהמערכת משמיצה ויוצרת מחדש את Activity כשיש שינוי בכיוון. התוצאה של התהליך הזה על ידי UseCases להגדיר את סיבוב היעד כך שיתאים לכיוון המסך באמצעות כברירת מחדל בכל פעם. מידע נוסף על סיבובים ב- CameraX

לפני שנרד לפרטים, הנה מבט כללי על UseCase והקשר בין אפליקציית Camera1. (הקונספטים של CameraX כבר זמינים כחול ו- Camera1 שני מושגים ירוק.)

מצלמהX

הגדרת CameraController / CameraProvider
תצוגה מקדימה צילום תמונה צילום וידאו ניתוח תמונה
ניהול התצוגה המקדימה של המסך והפעלת המצלמה הגדרת PictureCallback ו-callTakePicture() במצלמה ניהול התצורה של המצלמה ושל MediaRecorder בסדר ספציפי קוד ניתוח מותאם אישית המבוסס על גרסת התצוגה המקדימה
קוד ספציפי למכשיר
ניהול של סבב מכשירים והתאמה לעומס (scaling)
ניהול סשן המצלמה (בחירת מצלמה, ניהול מחזור חיים)

מצלמה1

תאימות וביצועים ב- CameraX

CameraX תומך במכשירים פועלים Android 5.0 (רמת API 21) ואילך. הזה יותר מ-98% ממכשירי Android הקיימים. CameraX מיועד לטפל הבדלים אוטומטיים בין מכשירים, כך שיש פחות צורך במכשירים ספציפיים באפליקציה שלכם. בנוסף, אנחנו בודקים יותר מ-150 מכשירים פיזיים בכל מכשירי Android מגרסה 5.0 ואילך במעבדת בדיקת CameraX שלנו. שלך אפשר לעיין ברשימה המלאה של המכשירים שנמצאים כרגע ב-Test Lab.

CameraX משתמש ב-Executor כדי לכונן את מקבץ המצלמה. אפשר הגדרת קובץ הפעלה משלכם ב- CameraX אם לאפליקציה יש דרישות ספציפיות לגבי השרשורים. אם המדיניות לא מוגדרת, CameraX יוצר ומשתמש ב-Executor פנימי שעבר אופטימיזציה. הרבה מממשקי ה-API של הפלטפורמה מופעלים אילו מצלמות CameraX נבנתה דורשת חסימה של תקשורת בין תהליכים (IPC) עם חומרה שלפעמים לוקחת מאות אלפיות שנייה כדי להגיב. בשביל זה הסיבה לכך היא ש- CameraX קורא לממשקי ה-API האלה רק משרשורי רקע, דבר שמבטיח ה-thread הראשי לא חסום וממשק המשתמש נשאר גמיש. למידע נוסף על שרשורים

אם שוק היעד של האפליקציה שלך כולל מכשירים פשוטים, CameraX מספקת דרך לקצר את זמן ההגדרה באמצעות הגבלת המצלמה. מאז תהליך החיבור לרכיבי חומרה עשוי להימשך במיוחד במכשירים פשוטים, ניתן לציין את סדרת המצלמות שהאפליקציה לצרכים שלכם. CameraX מתחבר למצלמות האלה רק במהלך ההגדרה. לדוגמה, אם האפליקציה משתמשת רק במצלמות אחוריות, היא יכולה להגדיר את התצורה הזו באמצעות DEFAULT_BACK_CAMERA ולאחר מכן CameraX מונע אתחול קדמי מצלמות כדי לקצר את זמן האחזור.

מושגי פיתוח של Android

המדריך הזה מבוסס על ההנחה שיש לכם היכרות כללית עם נושא הפיתוח של Android. מעבר ל- הנה כמה מושגים שיכולים לעזור לכם להבין מעבר לקוד שלמטה:

העברת תרחישים נפוצים

הקטע הזה מסביר איך להעביר תרחישים נפוצים מ- 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. מצלמהX מאפשר להשתמש בקלות במצלמת ברירת המחדל. אפשר לציין אם ברירת המחדל של המצלמה הקדמית או המצלמה האחורית המוגדרת כברירת מחדל. בנוסף, באמצעות אובייקט 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 "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 = ...

    // 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, עליך לנהל את הקריאות החוזרות של מחזור החיים בצורה נכונה, צריך גם לקבוע את הסיבוב וההגדלה של התצוגה המקדימה.

בנוסף, ב- 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, אבל עדיין מפשט מאוד את הגדרת התצוגה המקדימה בהשוואה למצלמה1. להדגמה למטרות, הדוגמה הזו משתמשת ב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 "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()
    }
}

הקשה למיקוד

כשהתצוגה המקדימה של המצלמה מופיעה במסך, אחת הדרכים הנפוצות היא להגדיר את המיקוד כשהמשתמש מקיש על התצוגה המקדימה.

מצלמה1

כדי להטמיע את התכונה 'מצמידים ומשלמים' במצלמה1, צריך לחשב את המיקוד האופטימלי 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() ה-method מחזירה אובייקט 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:

  1. צריך להגדיר מזהה תנועות לטיפול באירועי הקשה.
  2. עם האירוע מסוג Tap, יוצרים MeteringPoint באמצעות MeteringPointFactory.createPoint().
  3. באמצעות MeteringPoint, יוצרים FocusMeteringAction.
  4. באמצעות האובייקט CameraControl ב-Camera (מוחזר מ- bindToLifecycle()), התקשרות startFocusAndMetering(), מעבירה את FocusMeteringAction
  5. (אופציונלי) מגיבים לFocusMeteringResult.
  6. איך מגדירים את מזהה התנועה להגיב לאירועי מגע ב- 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
// "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
}

צביטה לשינוי מרחק התצוגה

הגדלה והקטנה של התצוגה המקדימה היא עוד מניפולציה ישירה נפוצה של תצוגה מקדימה של המצלמה. עם המספר ההולך וגדל של מצלמות במכשירים, המשתמשים גם נצפה שהעדשה עם אורך המוקד הטוב ביותר תיבחר באופן אוטומטי בתור כתוצאה משינוי מרחק התצוגה.

מצלמה1

יש שתי דרכים לשנות את מרחק התצוגה באמצעות Camera1. השיטה Camera.startSmoothZoom() יוצר אנימציה, מרמת הזום הנוכחית ועד לרמת הזום שמעבירים. שיטת Camera.Parameters.setZoom() מדלגת ישירות לרמת הזום שמעבירים אינץ' לפני שמשתמשים באחד מהם, צריך להתקשר למספר isSmoothZoomSupported() או isZoomSupported(), בהתאמה, כדי לוודא את השיטות הקשורות לשינוי מרחק התצוגה זמינים במצלמה.

כדי ליישם תנועת צביטה לשינוי מרחק התצוגה, הדוגמה הזו משתמשת ב-setZoom() כי המגע ה-listener משטח התצוגה המקדימה מפעיל כל הזמן אירועים בתור הצביטה מתבצעת תנועה, ולכן רמת הזום מתעדכנת מיד בכל פעם. מחלקה ZoomTouchListener מוגדרת למטה, ויש להגדירה כקריאה חוזרת (callback) למאזינים במגע של משטח התצוגה המקדימה.

// 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() ה-method מחזירה אובייקט 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:

  1. הגדרת מזהה של תנועות שינוי גודל כדי לטפל באירועי צביטה.
  2. מקבלים את ZoomState מהאובייקט Camera.CameraInfo, שבו Camera המופע מוחזר כשקוראים bindToLifecycle()
  3. אם לZoomState יש ערך zoomRatio, צריך לשמור אותו כזום הנוכחי יחס הגובה-רוחב. אם אין zoomRatio ב-ZoomState, צריך להשתמש בברירת המחדל של המצלמה שיעור הזום (1.0).
  4. לוקחים את המכפלה של יחס הזום הנוכחי עם scaleFactor אל לקבוע את יחס הזום החדש, ולהעביר אותו ל-CameraControl.setZoomRatio().
  5. איך מגדירים את מזהה התנועה להגיב לאירועי מגע ב- 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-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

צילום תמונה

בקטע הזה נסביר איך להפעיל צילום תמונה, אם צריך לעשות זאת לאחר סיום הטיימר, או בכל אירוע אחר בחירה.

מצלמה1

במצלמה1, קודם אתם מגדירים 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. ל-method הזה של takePicture() יש שלושה סוגים שונים לסוגים שונים של נתונים. הפרמטר הראשון הוא ShutterCallback (שלא מוגדר בדוגמה הזו). הפרמטר השני הוא ל-PictureCallback כדי לטפל בנתוני המצלמה הגולמיים (לא דחוסים). השלישי הפרמטר הוא הפרמטר שבו משתמשת בדוגמה הזו, כי הוא PictureCallback שצריך לטפל בו. נתוני תמונה בפורמט JPEG.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraController של CameraX שומר על הפשטות של 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() הצגת הקוד CameraController בקטע הזה תופיע דוגמה מלאה לפונקציה takePhoto().

// 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, והשיטות חייבות לקרוא לפי סדר מסוים. חובה לעקוב אחר ההזמנה הזו עבור כדי שהאפליקציה תפעל כראוי:

  1. פותחים את המצלמה.
  2. הכנות והתחלת תצוגה מקדימה (אם האפליקציה מציגה את הסרטון המוקלט, וזה בדרך כלל המצב).
  3. צריך לבטל את נעילת המצלמה לשימוש על ידי MediaRecorder על ידי התקשרות אל Camera.unlock().
  4. מגדירים את ההקלטה על ידי שליחת קריאה ל-methods ב-MediaRecorder:
    1. מחברים את המכונה של Camera אל setCamera(camera).
    2. התקשרות אל setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. התקשרות אל setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. חיוג למספר setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) כדי להגדיר את האיכות. צפייה CamcorderProfile לכולם אפשרויות האיכות.
    5. התקשרות אל setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. אם באפליקציה יש תצוגה מקדימה של הסרטון, צריך להתקשר setPreviewDisplay(preview?.holder?.surface)
    7. התקשרות אל setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. התקשרות אל setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. התקשרות אל setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. קוראים לפונקציה prepare() כדי להשלים את ההגדרה של MediaRecorder.
  5. כדי להתחיל להקליט, צריך להתקשר למספר MediaRecorder.start().
  6. כדי להפסיק את ההקלטה, צריך להפעיל את השיטות האלה. שוב, הקפידו על הסדר המדויק הבא:
    1. התקשרות אל MediaRecorder.stop().
    2. אפשר להסיר את ההגדרה הנוכחית של MediaRecorder באמצעות התקשרות MediaRecorder.reset().
    3. התקשרות אל MediaRecorder.release().
    4. נעילת המצלמה כדי שאפשר יהיה להשתמש בה בסשנים עתידיים של MediaRecorder באמצעות בהתקשרות אל Camera.lock().
  7. כדי להפסיק את התצוגה המקדימה, צריך להתקשר אל Camera.stopPreview().
  8. לבסוף, כדי לשחרר את 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 UseCase בנפרד, כל עוד אפשר להשתמש ברשימת התרחישים לדוגמה בו-זמנית. האפליקציות ImageCapture ו-ImageAnalysis UseCase מופעלות כברירת מחדל, לכן לא צריך להתקשר אל setEnabledUseCases() כדי לצלם תמונה.

כדי להשתמש ב-CameraController לצילום וידאו, צריך קודם להשתמש setEnabledUseCases() כדי לאשר את VideoCapture UseCase.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

כשתרצו להתחיל בהקלטת וידאו, תוכלו להתקשר CameraController.startRecording() מותאמת אישית. הפונקציה הזו יכולה לשמור את הסרטון המוקלט ב-File, כמו שאפשר לראות בדוגמה שלמטה. בנוסף, עליך לעבור Executor בכיתה שמיישם OnVideoSavedCallback כדי לטפל בקריאות חוזרות (callback) ושגיאות הצלחה. כשההקלטה אמורה להסתיים, מבצעים קריאה CameraController.stopRecording()

הערה: אם אתם משתמשים ב- CameraX מגרסה 1.3.0-alpha02 ואילך, יש הפרמטר AudioConfig שמאפשר להפעיל או להשבית את הקלטת האודיו בסרטון. כדי להפעיל הקלטת אודיו, עליך לוודא שיש לך הרשאות לשימוש במיקרופון. בנוסף, ה-method 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: VideoCapture
private 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() כדי להתחיל להקליט, להעביר בהקשר האזנה לאירוע של 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 שלמות מאגר GitHub של דגימות מצלמה הדוגמאות הבאות מראות איך התרחישים במדריך הזה משתלבים אפליקציה ל-Android.

אם ברצונך לקבל תמיכה נוספת במעבר אל CameraX, או אם יש לך שאלות בנוגע לחבילת ממשקי ה-API של מצלמת Android, יש לפנות אלינו דיון CameraX קבוצה.