שליטה במצלמה

בשיעור הזה נסביר איך לשלוט בחומרה של המצלמה ישירות באמצעות ממשקי ה-API של המסגרת.

הערה: הדף הזה מתייחס לכיתה Camera, שהוצאה משימוש. מומלץ להשתמש ב-CameraX או, במקרים ספציפיים, ב-Camera2. גם CameraX וגם Camera2 תומכים ב-Android מגרסה 5.0 (רמת API‏ 21) ואילך.

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

כדאי לעיין במקורות המידע הבאים:

פתיחת אובייקט המצלמה

קבלת מופע של האובייקט Camera הוא השלב הראשון בתהליך של שליטה ישירה במצלמה. כמו באפליקציית המצלמה של Android, הדרך המומלצת לגשת למצלמה היא לפתוח את Camera בשרשור נפרד שמופעל מ-onCreate(). כדאי להשתמש בגישה הזו כי הפעולה הזו עשויה להימשך זמן מה ולעכב את פעילות ה-UI. בהטמעה בסיסית יותר, אפשר להעביר את פתיחת המצלמה לשיטה onResume() כדי להקל על שימוש חוזר בקוד ולשמור על זרימה פשוטה של הבקרה.

קריאה ל-Camera.open() מפעילה חריגה אם אפליקציה אחרת כבר משתמשת במצלמה, לכן אנחנו עוטפים אותה בבלוק try.

KotlinJava
private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        mCamera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(getString(R.string.app_name), "failed to open Camera")
        e.printStackTrace()
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    mCamera?.also { camera ->
        camera.release()
        mCamera = null
    }
}
private boolean safeCameraOpen(int id) {
    boolean qOpened = false;

    try {
        releaseCameraAndPreview();
        camera = Camera.open(id);
        qOpened = (camera != null);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }

    return qOpened;
}

private void releaseCameraAndPreview() {
    preview.setCamera(null);
    if (camera != null) {
        camera.release();
        camera = null;
    }
}

החל מרמת API 9, מסגרת המצלמה תומכת בכמה מצלמות. אם משתמשים ב-API הקודם ומפעילים את open() בלי ארגומנט, מקבלים את המצלמה האחורית הראשונה.

יצירת התצוגה המקדימה של המצלמה

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

תצוגה מקדימה של הכיתה

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

KotlinJava
class Preview(
        context: Context,
        val surfaceView: SurfaceView = SurfaceView(context)
) : ViewGroup(context), SurfaceHolder.Callback {

    var mHolder: SurfaceHolder = surfaceView.holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }
    ...
}
class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView surfaceView;
    SurfaceHolder holder;

    Preview(Context context) {
        super(context);

        surfaceView = new SurfaceView(context);
        addView(surfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        holder = surfaceView.getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
...
}

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

הגדרה והפעלה של התצוגה המקדימה

צריך ליצור מכונה של מצלמה ואת התצוגה המקדימה המשויכת לה בסדר מסוים, כאשר אובייקט המצלמה הוא הראשון. בקטע הקוד הבא, תהליך האיניציאליזציה של המצלמה הוא בתוך הקופסה, כך ש-Camera.startPreview() נקרא על ידי השיטה setCamera() בכל פעם שהמשתמש מבצע פעולה כלשהי כדי לשנות את המצלמה. צריך גם להפעיל מחדש את התצוגה המקדימה בשיטת ה-callback surfaceChanged() של הכיתה preview.

KotlinJava
fun setCamera(camera: Camera?) {
    if (mCamera == camera) {
        return
    }

    stopPreviewAndFreeCamera()

    mCamera = camera

    mCamera?.apply {
        mSupportedPreviewSizes = parameters.supportedPreviewSizes
        requestLayout()

        try {
            setPreviewDisplay(holder)
        } catch (e: IOException) {
            e.printStackTrace()
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        startPreview()
    }
}
public void setCamera(Camera camera) {
    if (mCamera == camera) { return; }

    stopPreviewAndFreeCamera();

    mCamera = camera;

    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        supportedPreviewSizes = localSizes;
        requestLayout();

        try {
            mCamera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
}

שינוי ההגדרות של המצלמה

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

KotlinJava
override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
    mCamera?.apply {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        parameters?.also { params ->
            params.setPreviewSize(previewSize.width, previewSize.height)
            requestLayout()
            parameters = params
        }

        // Important: Call startPreview() to start updating the preview surface.
        // Preview must be started before you can take a picture.
        startPreview()
    }
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // Now that the size is known, set up the camera parameters and begin
    // the preview.
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(previewSize.width, previewSize.height);
    requestLayout();
    mCamera.setParameters(parameters);

    // Important: Call startPreview() to start updating the preview surface.
    // Preview must be started before you can take a picture.
    mCamera.startPreview();
}

הגדרת הכיוון של התצוגה המקדימה

רוב אפליקציות המצלמה נועלות את המסך במצב לרוחב כי זוהי ההגדרה הטבעית של חיישן המצלמה. ההגדרה הזו לא מונעת מכם לצלם תמונות בפורמט לאורך, כי כיוון המכשיר מתועד בכותרת ה-EXIF. השיטה setCameraDisplayOrientation() מאפשרת לשנות את אופן הצגת התצוגה המקדימה בלי להשפיע על אופן הצילום של התמונה. עם זאת, ב-Android בגרסאות ישנות יותר מ-API ברמה 14, צריך להפסיק את התצוגה המקדימה לפני שמחליפים את הכיוון, ואז להפעיל אותה מחדש.

צילום תמונה

משתמשים ב-method‏ Camera.takePicture() כדי לצלם תמונה אחרי שהתצוגה המקדימה מתחילה. אפשר ליצור אובייקטים מסוג Camera.PictureCallback ו-Camera.ShutterCallback ולהעביר אותם אל Camera.takePicture().

אם רוצים לקבל תמונות באופן רציף, אפשר ליצור Camera.PreviewCallback שמטמיע את onPreviewFrame(). אם אתם רוצים לבחור משהו באמצע, תוכלו לצלם רק את הפריימים שנבחרו בתצוגה המקדימה, או להגדיר פעולה מושהית כדי להפעיל את takePicture().

הפעלה מחדש של התצוגה המקדימה

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

KotlinJava
fun onClick(v: View) {
    previewState = if (previewState == K_STATE_FROZEN) {
        camera?.startPreview()
        K_STATE_PREVIEW
    } else {
        camera?.takePicture(null, rawCallback, null)
        K_STATE_BUSY
    }
    shutterBtnConfig()
}
@Override
public void onClick(View v) {
    switch(previewState) {
    case K_STATE_FROZEN:
        camera.startPreview();
        previewState = K_STATE_PREVIEW;
        break;

    default:
        camera.takePicture( null, rawCallback, null);
        previewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
}

עצירת התצוגה המקדימה ושחרור המצלמה

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

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

KotlinJava
override fun surfaceDestroyed(holder: SurfaceHolder) {
    // Surface will be destroyed when we return, so stop the preview.
    // Call stopPreview() to stop updating the preview surface.
    mCamera?.stopPreview()
}

/**
 * When this function returns, mCamera will be null.
 */
private fun stopPreviewAndFreeCamera() {
    mCamera?.apply {
        // Call stopPreview() to stop updating the preview surface.
        stopPreview()

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        release()

        mCamera = null
    }
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    // Surface will be destroyed when we return, so stop the preview.
    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();
    }
}

/**
 * When this function returns, mCamera will be null.
 */
private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        mCamera.release();

        mCamera = null;
    }
}

מוקדם יותר בשיעור, התהליך הזה היה גם חלק מהשיטה setCamera(), כך שהפעלת המצלמה תמיד מתחילה בהפסקת התצוגה המקדימה.