צילום תמונות

הערה: הדף הזה מתייחס למחלקה מצלמה, שהוצאה משימוש. מומלץ להשתמש ב-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.

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

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

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