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