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