Esta lição ensina como capturar uma foto delegando o trabalho a outro app de câmera no dispositivo. Caso prefira criar seu próprio recurso de câmera, consulte Como controlar a câmera.
Suponha que você esteja implementando um serviço meteorológico com colaboração coletiva, que cria um mapa meteorológico global misturando fotos do céu tiradas por dispositivos que executam seu app cliente. Integrar as fotos é só uma pequena parte do aplicativo. O objetivo é tirar fotos com o mínimo de complicações possível, e não reinventar a câmera. A maioria dos dispositivos Android já tem pelo menos um aplicativo de câmera instalado. Nesta lição, você aprenderá a fazer com que ele tire uma foto para você.
Solicitar o recurso de câmera
Se uma função essencial do seu aplicativo é tirar fotos, restrinja a visibilidade dele no Google Play para dispositivos que tenham câmera. Para anunciar que seu aplicativo depende de uma câmera, coloque uma tag <uses-feature>
no arquivo de manifesto:
<manifest ... > <uses-feature android:name="android.hardware.camera" android:required="true" /> ... </manifest>
Se seu aplicativo usa, mas não precisa de uma câmera para funcionar, configure android:required
como false
. Com isso, o Google Play permitirá que dispositivos sem câmera façam o download do seu aplicativo. Será sua responsabilidade verificar a disponibilidade da câmera no momento da execução, chamando hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
.
Se uma câmera não estiver disponível, os recursos da câmera precisarão ser desativados.
Tirar uma foto com um app de câmera
O Android delega ações a outros aplicativos invocando um Intent
que descreva o que você quer que seja feito. Esse processo envolve três partes: o próprio Intent
, uma chamada para iniciar a Activity
externa e um código para processar o vídeo quando o foco retorna à sua atividade.
Veja uma função que invoca uma intent para capturar uma foto.
Kotlin
val REQUEST_IMAGE_CAPTURE = 1 private fun dispatchTakePictureIntent() { Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent -> takePictureIntent.resolveActivity(packageManager)?.also { startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE) } } }
Java
static final int REQUEST_IMAGE_CAPTURE = 1; private void dispatchTakePictureIntent() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } }
O método startActivityForResult()
é protegido por uma condição que chama resolveActivity()
, que retorna o primeiro componente de atividade que pode processar a intent. Fazer essa verificação é importante porque, se você chamar startActivityForResult()
usando uma intent que nenhum app pode processar, seu app falhará. Portanto, desde que o resultado não seja nulo, é seguro usar a intent.
Ver a miniatura
Se o objetivo principal do seu app não é apenas tirar fotos, a ideia será recuperar a imagem do aplicativo da câmera e fazer algo com ela.
O aplicativo de Câmera do Android codifica a foto na Intent
de retorno entregue ao onActivityResult()
como um pequeno Bitmap
nos extras, sob a chave "data"
. O código a seguir recupera essa imagem e a exibe em 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); } }
Observação: essa imagem de miniatura de "data"
pode ser útil para um ícone, mas não é útil para outros fins. Processar uma imagem no tamanho original exige um pouco mais de trabalho.
Salvar a foto no tamanho original
O aplicativo de Câmera do Android salva uma foto em tamanho original se você fornecer um arquivo em que ela possa ser salva. Você precisa fornecer um nome de arquivo totalmente qualificado em que o app da câmera possa salvar a foto.
Em geral, as fotos tiradas pelo usuário com a câmera do dispositivo são salvas no armazenamento externo público do dispositivo, para que possam ser acessadas por todos os apps.
O diretório adequado para as fotos compartilhadas é fornecido pelo getExternalStoragePublicDirectory()
, com o argumento DIRECTORY_PICTURES
. Como o diretório fornecido por esse método é compartilhado por todos os apps, a leitura e gravação nele exigem as permissões READ_EXTERNAL_STORAGE
e WRITE_EXTERNAL_STORAGE
, respectivamente.
Implicitamente, a permissão de gravação permite a leitura. Portanto, se você precisar gravar no armazenamento externo, será necessário solicitar apenas uma permissão:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
Mas se você quiser que as fotos permaneçam privadas apenas para seu app, poderá usar o diretório fornecido por getExternalFilesDir()
.
No Android 4.3 e versões anteriores, a gravação nesse diretório também exige a permissão WRITE_EXTERNAL_STORAGE
. A partir do Android 4.4, a permissão não é mais necessária, porque o diretório não pode ser acessado por outros apps. Assim, você pode declarar que a permissão precisa ser solicitada apenas nas versões anteriores do Android ao adicionar o atributo maxSdkVersion
:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" /> ... </manifest>
Observação: os arquivos salvos nos diretórios fornecidos por getExternalFilesDir()
ou getFilesDir()
são excluídos quando o usuário desinstala o app.
Depois de decidir o diretório do arquivo, é necessário criar um nome de arquivo resistente a colisões. Você também pode salvar o caminho em uma variável de membro para uso posterior. Veja um exemplo de solução em um método que retorna um nome de arquivo exclusivo para uma nova foto, usando um carimbo de data e hora:
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; }
Com esse método disponível para criar um arquivo para a foto, você pode criar e invocar o Intent
da seguinte forma:
Kotlin
val REQUEST_TAKE_PHOTO = 1 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_TAKE_PHOTO) } } } }
Java
static final int REQUEST_TAKE_PHOTO = 1; 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_TAKE_PHOTO); } } }
Observação: estamos usando getUriForFile(Context, String, File)
, que retorna um URI content://
. Para apps mais recentes destinados ao Android 7.0 (API de nível 24) e versões mais recentes, passar um URI de file://
em um limite de pacote causa uma FileUriExposedException
.
Portanto, apresentamos agora uma maneira mais genérica de armazenar imagens usando um FileProvider
.
FileProvider
. No manifesto do app, adicione um provedor ao seu aplicativo:
<application> ... <provider android:name="android.support.v4.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>Verifique se a string de autoridades corresponde ao segundo argumento para
getUriForFile(Context, String, File)
.
Na seção de metadados da definição do provedor, é possível ver que o provedor espera que caminhos qualificados sejam configurados em um arquivo de recurso dedicado, <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-files-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" /> </paths>O componente do caminho corresponde ao caminho retornado por
getExternalFilesDir()
quando chamado com Environment.DIRECTORY_PICTURES
. Substitua com.example.package.name
pelo nome real do pacote do seu app. Além disso, confira a documentação de FileProvider
para ver uma descrição detalhada dos especificadores de caminho que podem ser usados além de external-path
.
Adicionar a foto a uma galeria
Ao criar uma foto por meio de uma intent, você saberá onde sua imagem está localizada, porque foi você que disse onde salvá-la. Para todas as outras pessoas, é provável que a maneira mais fácil de disponibilizar sua foto seja torná-la acessível pelo provedor de mídia do sistema.
Observação: se você salvou sua foto no diretório fornecido pelo getExternalFilesDir()
, o detector de mídia não pode acessar os arquivos, porque eles são particulares do seu app.
O exemplo de método a seguir demonstra como invocar o detector de mídia do sistema para adicionar sua foto ao banco de dados do provedor de mídia, disponibilizando-a no aplicativo Galeria do Android e em outros apps.
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); }
Decodificar uma imagem dimensionada
Gerenciar várias imagens em tamanho original com memória limitada pode ser complicado. Se o aplicativo estiver com memória insuficiente depois de exibir algumas imagens, é possível reduzir drasticamente a quantidade de heap dinâmico expandindo o JPEG em uma matriz de memória dimensionada para corresponder ao tamanho da visualização de destino. O exemplo de método a seguir demonstra essa técnica.
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 val photoW: Int = outWidth val photoH: Int = outHeight // Determine how much to scale down the image val scaleFactor: Int = 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; int photoW = bmOptions.outWidth; int photoH = bmOptions.outHeight; // Determine how much to scale down the image int scaleFactor = 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); }