หมายเหตุ: หน้านี้อ้างอิงถึงคลาส Camera ซึ่งเลิกใช้งานแล้ว เราขอแนะนำให้ใช้ CameraX หรือ Camera2 สำหรับกรณีการใช้งานที่เฉพาะเจาะจง ทั้ง CameraX และ Camera2 รองรับ Android 5.0 (API ระดับ 21) ขึ้นไป
ดูวิธีย้ายข้อมูลจาก Camera1 ไปยัง CameraX ได้ที่หัวข้อย้ายข้อมูล Camera1 ไปยัง CameraX
หรือจะใช้ทักษะ Agentic สำหรับการย้ายข้อมูลจาก Camera1 ไปยัง CameraX ก็ได้
บทเรียนนี้จะสอนวิธีจับภาพโดยมอบสิทธิ์การทำงานให้แอปกล้องอื่นในอุปกรณ์ (หากต้องการสร้างฟังก์ชันการทำงานของกล้องเอง โปรดดูการควบคุมกล้อง)
สมมติว่าคุณกำลังใช้บริการสภาพอากาศที่รวบรวมข้อมูลจากมวลชนซึ่งสร้างแผนที่สภาพอากาศทั่วโลกโดยการผสมผสานภาพท้องฟ้าที่ถ่ายโดยอุปกรณ์ที่ใช้แอปไคลเอ็นต์ของคุณ การผสานรวมรูปภาพเป็นเพียงส่วนเล็กๆ ของแอปพลิเคชันเท่านั้น คุณต้องการถ่ายภาพโดยไม่ต้องยุ่งยาก ไม่ใช่สร้างกล้องขึ้นมาใหม่ โชคดีที่อุปกรณ์ที่ทำงานด้วยระบบ Android ส่วนใหญ่มีแอปพลิเคชันกล้องอย่างน้อย 1 แอปติดตั้งไว้อยู่แล้ว ในบทเรียนนี้ คุณจะได้เรียนรู้วิธีทำให้ Gemini ถ่ายภาพให้คุณ
ขอฟีเจอร์กล้อง
หากฟังก์ชันที่สำคัญของแอปพลิเคชันคือการถ่ายภาพ ให้จำกัดระดับการเข้าถึงใน Google Play ไว้เฉพาะอุปกรณ์ที่มีกล้อง หากต้องการโฆษณาว่าแอปพลิเคชันของคุณต้องใช้กล้อง ให้ใส่แท็ก <uses-feature> ในไฟล์ Manifest ดังนี้
<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 ในไฟล์ Manifest ของแอป ให้เพิ่มผู้ให้บริการลงในแอปพลิเคชัน ดังนี้
<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>
ตรวจสอบว่าสตริงหน่วยงานตรงกับอาร์กิวเมนต์ที่ 2 ของ 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); }
ถอดรหัสรูปภาพที่ปรับขนาดแล้ว
การจัดการรูปภาพขนาดเต็มหลายรูปอาจเป็นเรื่องยากเมื่อมีหน่วยความจำจำกัด หากพบว่าแอปพลิเคชันมีหน่วยความจำไม่เพียงพอหลังจากแสดงรูปภาพเพียงไม่กี่รูป คุณสามารถลดปริมาณฮีปแบบไดนามิกที่ใช้ได้อย่างมากโดยขยาย 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); }