צילום תמונות

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

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

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

שליחת בקשה לשימוש בתכונה של המצלמה

אם אחת מהפונקציות המרכזיות של האפליקציה היא צילום תמונות, כדאי להגביל את החשיפה שלה ב-Google Play למכשירים שיש בהם מצלמה. כדי להודיע שהאפליקציה שלכם תלויה במצלמה, מוסיפים את התג <uses-feature> לקובץ המניפסט:

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

אם האפליקציה משתמשת במצלמה אבל לא נדרשת לה כדי לפעול, צריך להגדיר את android:required לערך false. כך, Google Play תאפשר למכשירים ללא מצלמה להוריד את האפליקציה. לאחר מכן, באחריותכם לבדוק את הזמינות של המצלמה במהלך זמן הריצה באמצעות קריאה ל-hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). אם המצלמה לא זמינה, צריך להשבית את תכונות המצלמה.

אחזור התמונה הממוזערת

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

אפליקציית המצלמה של Android מקודדת את התמונה בתשובה Intent שנשלחת אל onActivityResult() בתור Bitmap קטן בנתונים הנוספים, במפתח "data". הקוד הבא מאחזר את התמונה הזו ומציג אותה ב-ImageView.

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        val imageBitmap = data.extras.get("data") as Bitmap
        imageView.setImageBitmap(imageBitmap)
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Bundle extras = data.getExtras();
        Bitmap imageBitmap = (Bitmap) extras.get("data");
        imageView.setImageBitmap(imageBitmap);
    }
}

הערה: התמונה הממוזערת הזו מ-"data" עשויה להתאים לסמל, אבל לא הרבה יותר מזה. עבודה עם תמונה בגודל מלא דורשת קצת יותר מאמץ.

שמירת התמונה בגודל מלא

אפליקציית המצלמה של Android שומרת תמונה בגודל מלא אם מציינים לה קובץ לשמירה. עליכם לציין שם קובץ מלא שבו אפליקציית המצלמה תשמור את התמונה.

באופן כללי, כל התמונות שהמשתמש מצלם באמצעות מצלמת המכשיר צריכות להישמר במכשיר באחסון החיצוני הציבורי, כדי שכל האפליקציות יוכלו לגשת אליהן. הספרייה המתאימה לתמונות המשותפות מסופקת על ידי getExternalStoragePublicDirectory(), עם הארגומנט DIRECTORY_PICTURES. הספרייה שסיפקתם בשיטה הזו משותפת בין כל האפליקציות. ב-Android 9 (רמת API 28) וגרסאות ישנות יותר, כדי לקרוא ולכתוב בספרייה הזו נדרשות ההרשאות READ_EXTERNAL_STORAGE ו-WRITE_EXTERNAL_STORAGE, בהתאמה:

<manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

ב-Android 10 ואילך (רמת API 29 ואילך), הספרייה המתאימה לשיתוף תמונות היא הטבלה MediaStore.Images. אין צורך להצהיר על הרשאות אחסון, כל עוד לאפליקציה צריכה גישה רק לתמונות שהמשתמש צילם באמצעות האפליקציה.

עם זאת, אם אתם רוצים שהתמונות יישארו פרטיות רק לאפליקציה שלכם, תוכלו להשתמש בספרייה ש-Context.getExternalFilesDir() מספקת במקום זאת. ב-Android מגרסה 4.3 ומטה, כדי לכתוב בספרייה הזו נדרשת גם ההרשאה WRITE_EXTERNAL_STORAGE. החל מגרסה 4.4 של Android, ההרשאה הזו כבר לא נדרשת כי לאפליקציות אחרות אין גישה לספרייה. לכן, אפשר להצהיר שההרשאה צריכה להתבקש רק בגרסאות ישנות יותר של Android על ידי הוספת המאפיין maxSdkVersion:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="28" />
    ...
</manifest>

הערה: קבצים ששומרים בספריות שסופקו על ידי getExternalFilesDir() או getFilesDir() נמחקים כשהמשתמש מסיר את האפליקציה.

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

Kotlin

lateinit var currentPhotoPath: String

@Throws(IOException::class)
private fun createImageFile(): File {
    // Create an image file name
    val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val storageDir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile(
            "JPEG_${timeStamp}_", /* prefix */
            ".jpg", /* suffix */
            storageDir /* directory */
    ).apply {
        // Save a file: path for use with ACTION_VIEW intents
        currentPhotoPath = absolutePath
    }
}

Java

String currentPhotoPath;

private File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    File image = File.createTempFile(
        imageFileName,  /* prefix */
        ".jpg",         /* suffix */
        storageDir      /* directory */
    );

    // Save a file: path for use with ACTION_VIEW intents
    currentPhotoPath = image.getAbsolutePath();
    return image;
}

עכשיו, כשהשיטה הזו זמינה ליצירת קובץ של התמונה, אפשר ליצור את הפונקציה Intent ולהפעיל אותה כך:

Kotlin

private fun dispatchTakePictureIntent() {
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
        // Ensure that there's a camera activity to handle the intent
        takePictureIntent.resolveActivity(packageManager)?.also {
            // Create the File where the photo should go
            val photoFile: File? = try {
                createImageFile()
            } catch (ex: IOException) {
                // Error occurred while creating the File
                ...
                null
            }
            // Continue only if the File was successfully created
            photoFile?.also {
                val photoURI: Uri = FileProvider.getUriForFile(
                        this,
                        "com.example.android.fileprovider",
                        it
                )
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }
}

Java

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Ensure that there's a camera activity to handle the intent
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        // Create the File where the photo should go
        File photoFile = null;
        try {
            photoFile = createImageFile();
        } catch (IOException ex) {
            // Error occurred while creating the File
            ...
        }
        // Continue only if the File was successfully created
        if (photoFile != null) {
            Uri photoURI = FileProvider.getUriForFile(this,
                                                  "com.example.android.fileprovider",
                                                  photoFile);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
}

הערה: אנחנו משתמשים ב-getUriForFile(Context, String, File) שמחזיר URI מסוג content://. באפליקציות חדשות יותר שמטרגטות את Android מגרסה 7.0 (רמת API 24) ואילך, העברת URI של file:// מעבר לגבול החבילה גורמת ל-FileUriExposedException. לכן, אנחנו מציגים עכשיו דרך גנרית יותר לאחסון תמונות באמצעות FileProvider.

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

<application>
   ...
   <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.example.android.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"></meta-data>
    </provider>
    ...
</application>

מוודאים שהמחרוזת של הרשות תואמת לארגומנט השני של getUriForFile(Context, String, File). בקטע המטא-נתונים של הגדרת הספק, אפשר לראות שהספק מצפה שהנתיבים שעומדים בדרישות יוגדרו בקובץ משאב ייעודי, res/xml/file_paths.xml. זהו התוכן הנדרש לדוגמה הזו:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="my_images" path="Pictures" />
</paths>

רכיב הנתיב תואם לנתיב שמוחזר על ידי getExternalFilesDir() כשמפעילים אותו עם Environment.DIRECTORY_PICTURES. חשוב להחליף את com.example.package.name בשם החבילה בפועל של האפליקציה. בנוסף, כדאי לעיין במסמכי התיעוד של FileProvider כדי לקבל תיאור מפורט של מפרטי נתיב שאפשר להשתמש בהם בנוסף ל-external-path.

הוספת התמונה לגלריה

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

הערה: אם שמרתם את התמונה בספרייה שסיפק getExternalFilesDir(), לא תהיה לסורק המדיה גישה לקבצים כי הם פרטיים לאפליקציה.

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

Kotlin

private fun galleryAddPic() {
    Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
        val f = File(currentPhotoPath)
        mediaScanIntent.data = Uri.fromFile(f)
        sendBroadcast(mediaScanIntent)
    }
}

Java

private void galleryAddPic() {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(currentPhotoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

פענוח של תמונה מותאמת

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

Kotlin

private fun setPic() {
    // Get the dimensions of the View
    val targetW: Int = imageView.width
    val targetH: Int = imageView.height

    val bmOptions = BitmapFactory.Options().apply {
        // Get the dimensions of the bitmap
        inJustDecodeBounds = true

        BitmapFactory.decodeFile(currentPhotoPath, bmOptions)

        val photoW: Int = outWidth
        val photoH: Int = outHeight

        // Determine how much to scale down the image
        val scaleFactor: Int = Math.max(1, Math.min(photoW / targetW, photoH / targetH))

        // Decode the image file into a Bitmap sized to fill the View
        inJustDecodeBounds = false
        inSampleSize = scaleFactor
        inPurgeable = true
    }
    BitmapFactory.decodeFile(currentPhotoPath, bmOptions)?.also { bitmap ->
        imageView.setImageBitmap(bitmap)
    }
}

Java

private void setPic() {
    // Get the dimensions of the View
    int targetW = imageView.getWidth();
    int targetH = imageView.getHeight();

    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;

    BitmapFactory.decodeFile(currentPhotoPath, bmOptions);

    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.max(1, Math.min(photoW/targetW, photoH/targetH));

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
    imageView.setImageBitmap(bitmap);
}