צילום תמונות

הערה: הדף הזה מתייחס למחלקה 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. החל מ-Android 4.4, ההרשאה הזו כבר לא נדרשת כי לאפליקציות אחרות אין גישה לספרייה. לכן, אפשר להצהיר שצריך לבקש את ההרשאה רק בגרסאות ישנות יותר של Android, על ידי הוספת המאפיין maxSdkVersion:

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

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

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

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

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

הערה: אם שמרתם את התמונה בספרייה שסופקה על ידי 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);
}

פענוח תמונה מוקטנת

אם הזיכרון מוגבל, יכול להיות קשה לנהל כמה תמונות בגודל מלא. אם אתם מגלים שהאפליקציה שלכם נתקלת בבעיית אין זיכרון פנוי (OOM) אחרי הצגת כמה תמונות, אתם יכולים לצמצם באופן משמעותי את כמות הזיכרון הדינמי שנעשה בו שימוש על ידי הרחבת ה-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);
}