Chụp ảnh

Lưu ý: Trang này đề cập đến lớp Camera (không dùng nữa). Bạn nên dùng CameraX hoặc Camera2 (trong một số trường hợp sử dụng cụ thể). Cả CameraX và Camera2 đều hỗ trợ Android 5.0 (API cấp 21) trở lên.

Bài học này sẽ hướng dẫn bạn chụp ảnh bằng cách uỷ quyền công việc cho một ứng dụng máy ảnh khác trên thiết bị. (Nếu bạn muốn tự tạo chức năng máy ảnh, hãy xem bài viết Điều khiển máy ảnh.)

Giả sử bạn đang triển khai một dịch vụ thời tiết sử dụng nguồn lực cộng đồng giúp tạo bản đồ thời tiết toàn cầu bằng cách kết hợp các bức ảnh bầu trời được chụp bằng các thiết bị chạy ứng dụng khách. Việc tích hợp ảnh chỉ là một phần nhỏ trong ứng dụng. Bạn muốn chụp ảnh mà không tốn nhiều công sức và không phải đổi mới máy ảnh. Thật may là hầu hết thiết bị chạy Android đều đã cài đặt ít nhất một ứng dụng máy ảnh. Trong bài học này, bạn sẽ tìm hiểu cách chụp ảnh bằng ứng dụng.

Yêu cầu tính năng máy ảnh

Nếu chức năng thiết yếu của ứng dụng là chụp ảnh, hãy hạn chế chế độ hiển thị của chức năng này trên Google Play đối với những thiết bị có máy ảnh. Để quảng cáo rằng ứng dụng này phụ thuộc vào việc có máy ảnh, hãy đặt thẻ <uses-feature> vào tệp kê khai của bạn:

<manifest ... >
    <uses-feature android:name="android.hardware.camera"
                  android:required="true" />
    ...
</manifest>

Nếu ứng dụng của bạn dùng máy ảnh nhưng không cần phải có máy ảnh mới hoạt động được, hãy đặt android:required thành false. Khi làm như vậy, Google Play sẽ cho phép những thiết bị không có máy ảnh tải ứng dụng của bạn xuống. Sau đó, bạn có trách nhiệm kiểm tra để biết khả năng hoạt động của máy ảnh trong thời gian chạy bằng cách gọi hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Nếu máy ảnh không sử dụng được, bạn nên tắt các tính năng của máy ảnh.

Xem hình thu nhỏ

Nếu mục đích thiết kế ứng dụng không chỉ dừng lại ở việc chụp ảnh đơn giản, thì bạn có thể muốn lấy ảnh từ ứng dụng máy ảnh rồi thao tác trên ảnh đó.

Ứng dụng Máy ảnh trên Android mã hoá ảnh trong Intent trả về được gửi đến onActivityResult() dưới dạng Bitmap nhỏ trong phần bổ sung, dưới khoá "data". Mã sau đây truy xuất hình ảnh này và hiển thị trong 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);
    }
}

Lưu ý: Hình thu nhỏ từ "data" có thể phù hợp với một biểu tượng, nhưng không phù hợp với nhiều loại biểu tượng nữa. Việc xử lý hình ảnh ở kích thước đầy đủ sẽ mất nhiều công sức hơn.

Lưu ảnh ở kích thước đầy đủ

Ứng dụng Máy ảnh trên Android sẽ lưu ảnh ở kích thước đầy đủ nếu bạn cung cấp tệp để lưu ảnh đó. Bạn phải cung cấp tên tệp đủ điều kiện để ứng dụng máy ảnh lưu ảnh.

Nhìn chung, mọi ảnh mà người dùng chụp bằng máy ảnh của thiết bị phải được lưu trong bộ nhớ ngoài công khai của thiết bị để tất cả ứng dụng đều có thể truy cập vào những ảnh đó. Thư mục thích hợp cho ảnh được chia sẻ do getExternalStoragePublicDirectory() cung cấp, cùng đối số DIRECTORY_PICTURES. Tất cả các ứng dụng đều dùng chung thư mục mà phương thức này cung cấp. Trên Android 9 (API cấp 28) trở xuống, việc đọc và ghi vào thư mục này lần lượt cần có quyền READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE:

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

Trên Android 10 (API cấp 29) trở lên, thư mục thích hợp để chia sẻ ảnh là bảng MediaStore.Images. Bạn không cần khai báo bất kỳ quyền nào về bộ nhớ, miễn là ứng dụng của bạn chỉ cần truy cập vào các ảnh mà người dùng đã chụp bằng ứng dụng của bạn.

Tuy nhiên, nếu chỉ muốn giữ lại ảnh trong ứng dụng, bạn có thể dùng thư mục do Context.getExternalFilesDir() cung cấp. Trên Android 4.3 trở xuống, việc ghi vào thư mục này cũng yêu cầu quyền WRITE_EXTERNAL_STORAGE. Kể từ Android 4.4, quyền này không cần thiết nữa vì các ứng dụng khác không thể truy cập vào thư mục này, vì vậy, bạn có thể khai báo quyền phải được yêu cầu chỉ trên các phiên bản Android thấp hơn bằng cách thêm thuộc tính maxSdkVersion:

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

Lưu ý: Những tệp bạn lưu trong các thư mục do getExternalFilesDir() hoặc getFilesDir() cung cấp sẽ bị xoá khi người dùng gỡ cài đặt ứng dụng của bạn.

Sau khi quyết định thư mục cho tệp, bạn cần tạo tên tệp chống xung đột. Bạn cũng nên lưu đường dẫn trong biến thành phần để sử dụng sau này. Dưới đây là một giải pháp mẫu trong phương thức trả về tên tệp duy nhất cho một ảnh mới bằng dấu ngày giờ. (Ví dụ này giả định bạn đang gọi phương thức từ trong 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;
}

Với phương thức có thể dùng để tạo tệp cho ảnh, giờ đây, bạn có thể tạo và gọi Intent như sau:

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);
        }
    }
}

Lưu ý: Chúng tôi đang sử dụng getUriForFile(Context, String, File) để trả về URI content://. Đối với những ứng dụng mới đây nhắm đến Android 7.0 (API cấp 24) trở lên, việc chuyển URI file:// qua ranh giới gói sẽ tạo ra FileUriExposedException. Do đó, hiện chúng tôi trình bày cách lưu trữ hình ảnh chung chung hơn bằng cách sử dụng FileProvider.

Bây giờ, bạn cần định cấu hình FileProvider. Trong tệp kê khai của ứng dụng, hãy thêm nhà cung cấp vào ứng dụng của bạn:

<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>

Đảm bảo rằng chuỗi authorities (tổ chức uỷ quyền) khớp đối số thứ hai với getUriForFile(Context, String, File). Trong phần siêu dữ liệu của định nghĩa về nhà cung cấp, bạn có thể thấy rằng nhà cung cấp muốn có đường dẫn đủ điều kiện được định cấu hình trong một tệp tài nguyên chuyên dụng, res/xml/file_paths.xml. Sau đây là nội dung cần thiết cho ví dụ cụ thể này:

<?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>

Thành phần đường dẫn tương ứng với đường dẫn mà getExternalFilesDir() trả về khi được gọi bằng Environment.DIRECTORY_PICTURES. Hãy nhớ thay thế com.example.package.name bằng tên gói thực tế của ứng dụng. Ngoài ra, hãy xem tài liệu về FileProvider để biết nội dung mô tả bao quát về các thông số đường dẫn mà bạn có thể sử dụng cùng với external-path.

Thêm ảnh vào thư viện

Khi tạo ảnh thông qua một ý định, bạn nên biết vị trí của hình ảnh, vì bạn đã nói nơi lưu ảnh đầu tiên. Đối với những người khác, có lẽ cách dễ nhất để giúp họ truy cập được vào ảnh của bạn là cho phép họ truy cập vào ảnh từ Nhà cung cấp nội dung nghe nhìn của hệ thống.

Lưu ý: Nếu bạn đã lưu ảnh vào thư mục do getExternalFilesDir() cung cấp, thì trình quét nội dung nghe nhìn sẽ không truy cập được vào các tệp đó vì những tệp này chỉ dành riêng cho ứng dụng của bạn.

Phương thức mẫu sau minh hoạ cách gọi trình quét nội dung nghe nhìn của hệ thống để thêm ảnh vào cơ sở dữ liệu của Nhà cung cấp nội dung nghe nhìn, đưa ảnh đó vào ứng dụng Thư viện Android và các ứng dụng khác.

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);
}

Giải mã hình ảnh được điều chỉnh theo tỷ lệ

Bạn có thể gặp khó khăn khi quản lý nhiều hình ảnh ở kích thước đầy đủ trong bộ nhớ bị hạn chế. Nếu thấy ứng dụng hết bộ nhớ sau khi chỉ hiển thị một vài hình ảnh, bạn có thể giảm đáng kể dung lượng vùng nhớ khối xếp động được dùng bằng cách mở rộng JPEG thành một mảng bộ nhớ đã được điều chỉnh tỷ lệ cho phù hợp với kích thước của khung hiển thị đích. Phương thức mẫu sau đây minh hoạ kỹ thuật này.

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);
}