Os smartphones são plataformas com forte apelo visual, fornecendo ao usuário uma experiência rica de uso. Consequentemente, o uso de imagens e vídeos nos aplicativos facilita seu uso e tem mais apelo entre os usuários. A plataforma Android parece saber desta questão e forneceu um conjunto amplo de API´s para criação e manipulação de mídia. A lista é enorme, mas podemos citar as mais importantes: uso de fotos, vídeos e sons.
Neste artigo objetivamos mostrar justamente este rico conjunto de APIs, focando na captura de áudio e vídeo e suas questões intrínsecas, como por exemplo, permissões do usuário, forma padrão ou customizada e meios de armazenamento. Para solidificar o conhecimento vamos construir um aplicativo simples fotos e vídeos ao longo do texto.
Em que situação o tema é útil
Devido à importância do uso de mídias em aplicativos para smartphones, é de grande valia que profissionais Android dominem estas técnicas, sendo assim, podem se diferenciar dentre as centenas de milhares de opções oferecidas na loja de aplicativos.
O mundo dos smartphones é fortemente baseado no apelo visual. A interface amigável e o refinamento no acabamento de ícones e aplicativos são considerados um grande diferencial. Além disso, este quesito passa a ser quase questão de sobrevivência na medida em que as lojas virtuais se enchem de possibilidades. Pesquisas feitas ainda no final de 2012 davam conta de que as principais lojas, a Google Play e a App Store, tinham cerca de 700.000 aplicativos em cada uma.
A questão visual começa na construção de um bom ícone, afinal, o usuário tem que se sentir atraído pela sua solução dentro de centenas de possibilidades. Segue com a construção de layouts ricos e que facilitem a utilização e navegação do aplicativo. E, para finalizar, pode incluir um intenso uso de imagens e vídeos. Afinal, sempre ouvimos falar que uma imagem vale mais do que mil palavras.
Um bom exemplo disso é o sucesso que aplicativos simples de edição de imagem estão tendo, como o Instagram. A princípio, uma solução fácil de ser implementada, mas com um forte apelo gráfico. E isso se deve também a modernização dos SDKs disponíveis para a grande maioria das plataformas móveis.
Nas plataformas modernas o desenvolvedor tem ao seu alcance um conjunto de bibliotecas que facilitam a integração com o sistema operacional, tornando trivial o processo de requisitar que a câmera capture uma foto e armazene em mídias de armazenamento externas, por exemplo. E mais do que isso, também possibilitam a customização deste processo, podendo utilizar uma camada com ligação direta com hardware juntamente com seu layout de interface gráfica desenvolvido pela equipe de criação.
Um dos exemplos é o Android, que é justamente o tema deste artigo. Nesta plataforma temos a opção de utilizar o conceito de intenções (será melhor detalhado no decorrer do texto), onde, com poucas linhas, requisitamos uma imagem e o próprio sistema operacional se encarrega de retornar o dado solicitado.
Outra possibilidade, que a referida plataforma também oferece, é mostrar uma pré-visualização da câmera e implementarmos o aplicativo de “tirar foto” que desejarmos, desde os mais simples até os mais complexos, com interfaces de usuário extremamente ricas e com utilização até mesmo de sensores.
Soma-se a isso a notável evolução que o hardware dos dispositivos móveis possui, estendendo-se a qualidade da imagem e vídeo que as câmeras dos mesmos conseguem capturar. Hoje em dia é perfeitamente normal abrirmos mão de dispositivos exclusivamente para fotos e em seu lugar utilizarmos o smartphone.
E mais uma vez as plataformas móveis se atentaram a este fato. É comum termos em mãos funções e/ou métodos que possibilitam configurações finas na forma de capturar a foto. O Android, por exemplo, permite reconhecimento facial em tempo real. E isso é apenas uma das possibilidades. Podemos também definirmos nível de zoom e modo de flash, para ficar nos mais conhecidos.
Ou seja, temos um cenário, para desenvolvedores, muito diferente daquele presente há 5, 6 anos. O JavaME, extensamente utilizado em anos anteriores, não oferecia nem metade das possibilidades que Android, iPhone e Windows Phone oferecem. Deixar de utilizá-las em nossos aplicativos nos fará perder tempo, espaço nas lojas e, como consequência final e mais impactante, clientes.
Sendo assim, o objetivo deste artigo é justamente preencher esta lacuna nos conhecimentos do leitor. Pretendemos mostrar de forma clara quais as principais APIs e suas formas de uso para captura de imagens e vídeo na plataforma Android.
Como dito anteriormente, faremos um aplicativo que mostra as quatro possibilidades discutidas ao longo do texto. Duas delas são a captura de fotos e vídeos utilizando os aplicativos nativos do aparelho. Desta forma ganhamos em tempo, porém, ficamos preso a interfaces destes aplicativos e não temos customizações. Por isso, também mostraremos como implementar nossa própria feature de captura de mídia. Consequentemente, teremos mais trabalho mas, em contrapartida, as possibilidades são muito maiores. Como bônus, o leitor poderá ver um pouco sobre a forma de armazenamento em mídias externas, como SDCard por exemplo. Boa leitura.
O projeto
O projeto a ser desenvolvido não tem nenhuma pretensão de se tornar uma aplicação comercial, mas sim, de demonstrar de maneira fácil e organizada as formas de captura de fotos e vídeos na plataforma Android. Na primeira tela (Figura 1) temos um menu com as quatro opções disponíveis.
A primeira opção (Intent-Foto) leva para a tela ilustrada na Figura 2. Depois que o botão for acionado e o aplicativo receber a foto como retorno da Intent populamos a região central. A mesma ideia vale para a opção Intent-Video, que levará o usuário para a tela mostrada na Figura 3. A única diferença é que a região central fica inicialmente preta porque estamos utilizando outro componente de interface gráfica.
Nas opções Professional-Foto e Professional-Video, o usuário será direcionado a uma tela conforme aquela mostrada na Figura 4. A diferença entre ambas é que quando a mídia for capturada a região de preview será substituída pela foto ou pelo vídeo. No caso de foto temos o exemplo na Figura 5.
Perceba que não foi feito um trabalho detalhado na interface de usuário, simplesmente porque o foco deste artigo não é este. Também utilizamos componentes de UI tradicionais do Android para não atrapalhar no entendimento do principal, a captura de mídia.
Codificando o projeto
Para melhor visualizar o conjunto de classes e arquivos .xml que iremos construir, observe a Figura 6.
Inicialmente, apenas crie as mesmas sete classes mostradas na figura dentro de um pacote também chamado de com.example.cameraapp. Além disso, também crie arquivos xml com os mesmos nomes vistos no diretório res->layout. Ao longo do artigo vamos populando e entendendo melhor cada um deles.
Como visto nas figuras do aplicativo, a tela inicial será uma lista com quatro opções que direcionam o usuário para outras Activities. Sendo assim, a primeira mudança que podemos efetuar é no AndroidManifest.xml. Observe a Listagem 1.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="com.example.cameraapp"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="16" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:screenOrientation="landscape"
android:name="com.example.cameraapp.ProfessionalVideo"
android:label="@string/app_name" >
</activity>
<activity
android:screenOrientation="landscape"
android:name="com.example.cameraapp.ProfessionalFoto"
android:label="@string/app_name" >
</activity>
<activity
android:name="com.example.cameraapp.IntentFoto"
android:label="@string/app_name" >
</activity>
<activity
android:name="com.example.cameraapp.IntentVideo"
android:label="@string/app_name" >
</activity>
<activity
android:name="com.example.cameraapp.Menu"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Na primeira linha temos o padrão do documento xml. Na linha 2 temos a tag raiz do manifesto, onde definimos o pacote da aplicação na linha 3, e o código e nome da versão do código que estamos desenvolvendo, isso nas linhas 4 e 5, respectivamente.
Na linha 7 temos a tag uses-sdk. Nela, estamos definindo que o aplicativo estará disponível a partir da api level 9 do Android (linha 8). Este número inteiro indica a versão do sistema operacional. Ele teve como base o Android 1.0 e, consequentemente, o api level 1. Depois, a cada nova versão disponibilizada este nível de API era acrescentado em 1. O 9 indica o android 2.3.3, ou, mais conhecido com o nome de Gingerbread.
Já na linha 9 especificamos que nossa versão de SDK alvo é aquela identificada com o api level 16, que se refere às versões 4.1 e 4.1.1 do Android, mais conhecidas como JellyBean.
A tag application, iniciada na linha 11, serve para definição de características comuns no aplicativo como o ícone (linha 12), rótulo (linha 13) e tema (linha 14). Perceba que em todos os casos estamos apontando para recursos da própria aplicação, isto é, estão dentro do diretório res. Por exemplo, o ic_launcher é uma imagem criada no momento da geração do projeto e, portanto, está dentro de res/drawable-ldi,mdpi,hdpi,xhdpi.
Em um aplicativo Android podemos ter quatro tipos de componentes que de forma resumida são:
- Activity: uma tela, especificada com a tag activity.
- Service: service que roda em background, especificada com a tag service;
- BroadcastReceiver: intercepta eventos oriundos do sistema operacional ou lançados por outro componente. É especificada com a tag receiver;
- Content Provider: forma de tornar dados da sua aplicação públicos. É especificada com a tag provider.
Sendo assim, declaramos todas as activities, ou seja, as telas que compõe nossa aplicação. Basicamente, todas tem duas propriedades: name, que indica o nome da classe que é responsável por esta atividade e, label, que é o rótulo que ficará na Action Bar, ao lado do ícone da aplicação. Porém, em alguns casos também temos a propriedade screenOrientation (linhas 16 e 21). Ela serve para indicar que esta tela sempre estará na orientação desejada e não sofrerá rotação.
Na Activity definida na linha 33 também temos algo a mais. Temos um intent-filter. Precisamos criar este filtro com uma action com o nome android.intent.action.MAIN para indicar que esta será a tela principal da aplicação, aquele onde será dado o ponto de partida. Perceba que estamos fazendo isso na tela Menu. Além disso, especificamos a categoria android.intent.category.LAUNCHER, na linha 38, para que este aplicativo apareça na bandeja de aplicações do smartphone.
Como vimos anteriormente, a tela que inicia a aplicação é o Menu. Então vamos dar uma olhada nesta classe. Observe Listagem 2.
public class Menu extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.menu);
String[] itens = new String[]{"Intent-Foto", "Intent-Video",
"Professional-Foto", "Professional-Vídeo"};
ArrayAdapter arrayAdapter = new ArrayAdapter<String>
(this, android.R.layout.simple_list_item_1, itens);
setListAdapter(arrayAdapter);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
witch (position){
case 0:
Intent intent = new Intent(this, IntentFoto.class);
startActivity(intent);
break;
case 1:
Intent intVideo = new Intent(this, IntentVideo.class);
startActivity(intVideo);
break;
case 2:
Intent intPFoto = new Intent
(this, ProfessionalFoto.class);
startActivity(intPFoto);
break;
case 3:
Intent intPVideo = new Intent
(this, ProfessionalVideo.class);
startActivity(intPVideo);
}
}
}
Na primeira linha encontramos a definição da classe que indica a herança com ListActviity. Esta, por sua vez, é uma especialização da classe Activity, que representa uma tela do aplicativo na arquitetura Android. Na linha 4 estamos sobrescrevendo o onCreate, que faz parte do ciclo de vida de uma Activity. Na linha estamos associando o conteúdo visual à interface, sendo que, o conteúdo desta está armazenado em um arquivo XML na pasta layout, dentro do diretório res. Por isso passamos o parâmetro R.layout.menu.
Na linha 8 criamos um array de Strings. O mesmo será passado como um parâmetro do construtor da classe ArrayAdapter. Além deste, passamos por parâmetro (linha 9) um contexto e o layout que será utilizado em cada índice da lista, respectivamente. Esta classe é passada no método setListAdapter, na linha 10. Este método irá associar a listagem de itens com a lista propriamente dita. A presença deste método se deve ao fato da classe herdar diretamente da classe ListActivity.
Devido à herança, também podemos sobrescrever o método onListItemClick, que será acionado quando o usuário clicar em um dos itens da lista. Na linha 16 estamos fazendo um switch com a informação de posição que foi clicada na lista. Dependendo deste valor, chamamos a tela associada. Todos os cases funcionam da mesma forma. Vamos tomar como exemplo o case 0, que inicia seu bloco de código na linha 18. Nesta linha estamos criando um Intent com Component Name. Ou seja, direcionamos o componente que irá trabalhar com esta intenção para fazer algo. E na linha 19 iniciamos a Intent simplesmente chamando o método startActivity.
Antes da mídia, seu repositório
Antes de começarmos a trabalhar com os códigos que capturam as fotos e vídeos, vamos trabalhar um pouquinho com a classe Util. Como seu nome indica, ela será utiliza pelas outras classes e um dos métodos que a mesma possui retorna o endereço do arquivo onde os dados das mídias serão armazenados. Observe Listagem 3.
public class Util {
public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
public static String localFoto;
public static Uri getOutputMediaFileUri(int type){
return Uri.fromFile(getOutputMediaFile(type));
}
public static File getOutputMediaFile(int type){
File mediaStorageDir = new File
(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "MyCameraApp");
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
return null;
}
}
String timeStamp = new SimpleDateFormat
("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE){
localFoto = mediaStorageDir.getPath()
+ File.separator +
"IMG_"+ timeStamp + ".jpg";
mediaFile = new File(mediaStorageDir.getPath()
+ File.separator +
"IMG_"+ timeStamp + ".jpg");
} else if(type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath()
File.separator +
"VID_"+ timeStamp + ".mp4");
} else {
return null;
}
return mediaFile;
}
}
A classe possui três variáveis estáticas. Na linha 3 e na linha 4 temos dois inteiros que serão utilizados para definir com qual tipo de mídia estamos trabalhando, foto ou vídeo. Na linha 4 temos uma String que armazena o último local utilizado para armazenar uma foto, o mesmo será utilizado pelas classes que farão uso da Util.java.
Na linha 7 temos o método getOutputMediaFileUri, que recebe um tipo por parâmetro e retorna uma instância de Uri. Para chegar a esta instância utilizamos o método fromFile da própria classe. E como parâmetro devemos passar uma instância de File que é justamente o retorno do próximo método a ser visto, o getOutputMediaFile.
Na linha 11 temos o getOutputMediaFile, que também recebe um tipo como parâmetro. Logo na linha 12 estamos criando uma instância de File. Como parâmetro estamos utilizando o retorno do método estático getExternalStoragePublicDirectory da classe Environment. E essa classe e método merecem um tratamento especial neste artigo.
A plataforma Android fornece diversas possibilidades de persistência de dados, uma delas é o armazenamento em mídias externas. Cada uma das formas apresenta seus prós e contras. Quando pensamos em mídia é muito indicado utilizar esta mídia externa, como um cartão SDCard por exemplo.
Basicamente, nós como desenvolvedores, devemos considerar duas localizações para colocarmos nossas mídias:
- Environment.getExternalStoragePublicDirectory: o Android fornece um diretório padrão e público, onde todos os aplicativos podem trabalhar. Este método recebe por parâmetro uma constante da classe Environment que categoriza alguns subdiretórios deste local compartilhado. É importante ressaltar que quando o aplicativo for removido as mídias criadas pelo mesmo continuam no aparelho, mais especificamente, na mídia externa.
- Context.getExternalFilesDir: retorna um diretório padrão associado ao aplicativo, porém, cabe ressaltar que não é garantido segurança para as mídias salvas neste local. A mudança aqui é que quando o aplicativo é removido, este local também é limpo, deletando qualquer arquivo criado.
Voltando à listagem de código, perceba que na linha 12 estamos utilizando o método getExternalStoragePublicDirectory e passando como segundo parâmetro o nome de uma pasta. Isso é altamente indicado pelo seguinte fato: estaremos trabalhando em um local aberto e compartilhado, é interessante marcarmos os arquivos que nosso aplicativo criou, colocando-os em um subdiretório específico.
Uma dica interessante para desenvolvimento de aplicativos profissionais é verificar a existência e a possibilidade de utilizar estas mídias externas.
Na linha 15 estamos verificando se a instância de File que acabamos de criar, nomeada como mediaStorageDir, já existe. Nosso aplicativo pode estar sendo executado pela, segunda, terceira, enésima vez. Caso não exista tentamos criar, através do método mkdirs da classe File. Caso a pasta não exista e não conseguimos criar, a linha 17 retornará um valor nulo para o método, porque não conseguiremos salvar mídias em algo inexistente.
Caso passemos por este teste, a linha 20 cria um String com a data daquele instante, formatada com uma máscara específica, através da classe SimpleDateFormat.
Na linha 22 verificamos se este método foi chamado com um tipo imagem. Se sim, inicializamos a variável localFoto com o path do File criado, mais o separador específico, acrescido do texto IMG_ com o timeStamp e a extensão .jpg (linha 23). Na linha 24 a variável mediafile recebe seu valor com o conteúdo da variável localFoto.
Caso este método tenha sido chamado com o tipo vídeo, entramos na linha 28. Aqui temos a mesma sequência de antes, apenas mudamos IMG_ por VID_ e a extensão para .mp4. Caso não foi nenhum dos dois tipos retornamos nulo (linha 31).
Se tudo deu certo retornamos a instância mediaFile na linha 34.
A opção IntentFoto
A primeira opção disponível ao usuário é o “Intent-Foto”, que direciona a aplicação para a tela e, consequentemente, para a classe que representa a Activity IntentFoto. Desta vez vamos começar analisando o intent_camera.xml que é a View da referida tela. Observe a Listagem 4.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Capturar"
android:onClick="capturar" />
<ImageView
android:scaleType="fitXY"
android:id="@+id/imgIntentCamera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
Este xml possui como tag root um LinearLayout. Na linha 3 e 4 configuramos o mesmo para ocupar todo o espaço disponível horizontalmente e verticalmente. Na linha 5 utilizamos a propriedade orientation para alinhar de forma vertical os filhos do container.
A primeira UI Widget é um Button. Ele tem um id definido na linha 8. Sua largura e altura são definimos com o valor wrap_content nas linhas 9 e 10. Com isso, ele ocupará o espaço retangular apenas para mostrar seu conteúdo e nada mais. Na linha 11 estamos orientando o botão ao centro horizontal em relação ao seu pai, que neste caso é o LinearLayout. Seu rótulo é definido na linha 12 e, finalmente, na linha 13 definimos a propriedade onClick, passando o nome do método que será chamado pela Activity que utilizar esta View quando o usuário clicar no UI Widget.
Já na linha 15 temos um UI Widget ImageView, que receberá a imagem capturada pela câmera do smartphone. Na linha 16 definimos o tipo de escala que a foto terá, passando fitXY e forçando a mesma a ocupar a tela toda. Para fomentar ainda mais nossa necessidade de preenchimento, definimos como match_parent a largura e a altura, nas linhas 18 e 19, respectivamente. Por fim, na linha 20 definimos seu peso como 1, sendo assim, qualquer espaço vertical restante sobrando na tela será usado pela imagem.
Visto a View vamos passar para o código da Activity. Observe Listagem 5.
public class IntentFoto extends Activity {
private ImageView img;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.intent_camera);
img = (ImageView) findViewById(R.id.imgIntentCamera);
}
public void capturar(View v) {}
}
A classe IntentFoto estende de Activity, logo será uma tela e deverá sobrescrever o método onCreate, que faz parte do ciclo de vida da sua classe pai e é onde ligamos a View a ela. Isso será feito na linha 9, com o método setContentView. Na linha 11 inicializamos a variável img, criada na linha 3. Estamos utilizando o método findViewById, de Activity. O que ele faz é procurar no xml que define a view se existe um UI Widget com o id passado por parâmetro. Caso encontre, ele retorna uma instância de View, que é a classe pai de ImageView, por isso o casting explícito.
Por fim, devemos criar o método capturar, porque quando definimos o UI Widget Image View no xml definimos sua propriedade onClick com capturar. Logo, somos obrigados a criar este método, sempre sendo público, sem retorno e recebendo como lista de parâmetros apenas uma instância de View.
Neste momento ainda não temos nenhum código que envolve captura de mídia, apenas o arcabouço preparado para receber esta codificação que vamos começar agora.
Capturando foto via Intent
A captura de mídia via Intent tem somo principal benefício a facilidade. Seu aplicativo não será responsável por esta tarefa. Apenas encaminha a intenção ao sistema operacional, que encontrará quem seja capaz de capturar uma imagem e lhe fornece o retorno pronto. A parte ruim é que, caso não achemos legal o aplicativo que foi usado para captura e desejemos mexer no layout do mesmo, estaremos de mãos amarradas.
Vamos modificar o método capturar da classe IntentFoto. Observe a Listagem 6.
public void capturar(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = Util.getOutputMediaFileUri(Util.MEDIA_TYPE_IMAGE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
}
Já utilizamos uma Intent para declarar a intenção de chamar uma tela diferente. Naquele cenário passamos dois parâmetros, sendo uma instância de Contexto e a classe destino. Porém, outro construtor que esta classe recebe é aceitando apenas um parâmetro: uma String que indica uma ação.
Na plataforma Android existem diversas Strings estáticas e públicas dentro de inúmeras classes, que permitem de forma fácil as entendermos e utilizarmos. Algumas delas estão dentro da própria classe Intent. Veja alguns exemplos:
- Para ver uma URL no browser padrão do aparelho: Intent.ACTION_VIEW;
- Para efetuar uma chamada telefônica: Intent.ACTION_CALL;
- Para enviar um e_mail: Intent.ACTION_SEND
Depois desta breve explanação, voltamos ao código da Listagem 6. No construtor de Intent estamos passando a ação MediaStore.ACTION_IMAGE_CAPTURE que tem objetivo auto explicado. Quando definimos esta Intent podemos passar alguns extras para a mesma. A classe MediaStore nos fornece algumas possibilidades. Uma delas é o EXTRA_OUPUT, que define a URI do arquivo onde a imagem será salva. É altamente indicado utilizarmos este apontamento, senão o Android salvará em um local padrão e, estamos seguindo esta boa prática na linha 6. Na linha 4 instanciamos a variável fileUri obtendo o retorno de uma chamada ao método Util.getOututMediaFileUri, passando o tipo imagem. A classe Util foi mostrada na Listagem 3.
Na linha 7 estamos lançando esta Intent de uma forma diferente. Ao invés de utilizarmos o startActivity, estamos usando o startActivityForResult. Temos um parâmetro a mais além da própria intenção, que é um código de requisição. Será necessário criar a variável CAPTUURE_IMAGE_ACTIVITY_REQUEST_CODE na classe IntentFoto:
private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
O startActivityForResult é muito importante e útil dentro da arquitetura da plataforma. Ele lança uma intenção ao sistema operacional, mas exige que o mesmo retorne um resultado identificado pelo request code que passamos por parâmetro. Tudo bem, mas como poderemos capturar esse retorno e utilizá-lo?
A resposta é sobrescrever o método onActivityResult. Observe Listagem 7.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
Bitmap vt = BitmapFactory.decodeFile
(Util.localFoto);
vt = Bitmap.createScaledBitmap
(vt, 480, 600, false);
Matrix matrix = new Matrix();
matrix.postRotate(90);
Bitmap resizedBitmap =
Bitmap.createBitmap(vt, 0, 0, 480, 600,
matrix, true);
img.setImageBitmap(resizedBitmap);
} else if (resultCode == RESULT_CANCELED) {
// usuário cancelou
} else {
// alguma falha ocorreeu
}
}
}
A resposta oriunda do aplicativo que tratou nossa Intent vem com três parâmetros, um request code, um response code e um uma Intent que pode ter dados extras. Por exemplo, se não passamos um endereço de File onde a imagem seria salva, poderemos saber onde isso foi feito através do parâmetro data.
Na linha 2 estamos verificando se o request code recebido pelo método foi igual àquele que nós mesmos utilizamos ao chamarmos a Intent. Caso afirmativo, deveremos tratar. Na linha 3 vamos verificar se o result code recebido foi uma constante RESULT_OK, RESULT_CANCELED ou outra coisa.
Se o usuário cancelou, o teste da linha 12 será true e deveremos ter algum tratamento específico na linha 13. Caso tenha ocorrido algum outro tipo de erro, o else da linha 14 será acionado e também é muito recomendado termos algum tratamento específico na linha 15. Caso tudo tenha ocorrido bem, entraremos no if da página 3.
Logo na linha 4 criamos uma instância de Bitmap. Existe uma classe na API Android chamada BitmapFactory, onde podemos decodificar uma imagem de diferentes fontes, uma delas é da uri de um arquivo no smartphone. Este método é o decodeFile. No seu parâmetro estamos passando a variável estática de Util, chamado de localFoto(veja a Listagem 3 para relembrar)
Depois de criada a instância de Bitmap, vamos escalar a mesma. Isso porque dependendo da qualidade do dispositivo e da câmera, a foto recebida pode ter mais de 2000 pixels, o que pode ocasionar em lentidão para mostrar a imagem ou nem mesmo mostrar.
A classe Bitmap tem um método chamado cerateScaledBitmap, onde passamos um bitmap existente, o novo tamanho desejado e um último parâmetro booleano indicando se a fonte original deve ser filtrada ou não.
Seguindo o código, mais especificamente na linha 7, estamos instanciando um objeto da classe Matrix que possui uma matriz 3x3 para transformação de coordenadas. Vamos nos valer desta classe para rotacionar a imagem. Precisamos fazer isso pelo seguinte: no aparelho que testamos o aplicativo, a foto é tirada sempre no modo landscape e estava utilizando o aplicativo no modo portrait. Claro que para aplicativos profissionais deveria haver um tratamento mais refinado para esta situação.
Sendo assim, na linha 8 estamos chamando o método postRotate que recebe como parâmetro os graus em float, no nosso caso 90. Depois, estamos criando um novo Bitmap passando como parâmetro a bitmap fonte, a posição x,y de recorte inicial, o tamanho desejado, a instância de Matrix e o boolean que define se a fonte será filtrada ou não.
E finalmente, na linha 11, passamos este bitmap como parâmetro para o método setImageBitmap da instância img, da classe ImageView. Com isso, a imagem capturada será visualizada pelo usuário.
A opção IntentVideo
A segunda opção disponível ao usuário é o “Intent-Video”, que direciona a aplicação para a tela e, consequentemente, para a classe que representa a Activity IntentVideo. Como anteriormente, vamos começar analisando o intent_video.xml que é a View da referida tela. Observe a Listagem 8.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Capturar"
android:onClick="capturar" />
<VideoView
android:scaleType="fitXY"
android:id="@+id/videoIntentCamera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
Até a linha 13 temos exatamente a mesma listagem que já tinha sido usada no IntentFoto, a grande diferença está na linha 14. Naquela ocasião, utilizamos uma UI WIDGET ImageView. Porém, não estamos mais tratando de foto, mas sim de vídeo. O Android possui a VideoView que serve para visualizar um vídeo. Porém, perceba que as propriedades que estamos utilizando aqui também foram todas aplicadas ao ImageView.
Devido à semelhança com o XML intente_camera, podemos passar para a classe IntentVideo que também é muito parecida com a IntentFoto. Observe a Listagem 9 com a estrutura inicial da mesma.
public class IntentVideo extends Activity {
private Uri fileUri;
private VideoView video;
private static final int CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE = 200;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.intent_video);
video = (VideoView) findViewById(R.id.videoIntentCamera);
}
public void capturar(View v) {}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {}
}
Também temos a vida facilitada neste código, porque é semelhante ao IntentFoto. Nas linhas 3, 4 e 5 estamos criando variáveis que serão utilizadas ao longo da classe. A fileUri servirá para armazenar o local onde o vídeo foi salvo e utilizar na VideoView. A instância desta mesma classe chamada de vídeo é inicializada na linha 12, utilizando o mesmo findViewById visto anteriormente. O método onCreate deve e é sobrescrito na linha 8. Na linha 10 ligamos view e activity com o método setContentView.
Na linha 15 temos a assinatura do método capturar, que será invocado com o clique no botão capturar e, na linha 17, sobrescrevemos o método onActivityResult para receber o retorno do sistema operacional e do aplicativo que atendeu nossa futura Intent.
Capturando vídeo via Intent
Vamos agora analisar a Listagem 10 que mostra o método capturar de IntentVideo:
public void capturar(View v) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
fileUri = Util.getOutputMediaFileUri(Util.MEDIA_TYPE_VIDEO);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 30);
startActivityForResult(intent, CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE);
}
Na segunda linha estamos criando uma Intent especificando sua ação, neste caso a ACTION_VIDEO_CAPTURE. Na linha 4 colocamos o retorno do método Util.getOutputMediaFileUri para a instância de URI chamada fileUri. Ela será usada para informar ao vídeo onde buscar a mídia a ser executada.
Assim como no caso da foto, também estamos passando alguns parâmetros para a intenção. Na linha 6 informamos onde os bytes que compõem o vídeo devem ser salvos. Na 7 fazemos uso da constante MediaStore.EXTRA_VIDEO_QUALITY para identificar a qualidade do vídeo desejada. Se o valor passado for 0 (zero), significa qualidade de vídeo baixa, passando valor 1, qualidade de vídeo alta.
Na linha 8 definimos o limite máximo do vídeo em segundos, utilizando para tanto a constante MediaStore.EXTRA_DURATION_LIMIT. Para ver todas as possibilidades de especialização da Intent, visite a página da classe MediaStore em developer.android.com/reference/android/provider/MediaStore.html.
E, finalmente, na linha 10 lançamos a intenção ao sistema operacional utilizando o método startActivityForResult passando por parâmetro a intent e o request code. Ao fazer uso desta chamada temos que sobrescrever o onActivityResult para tratar do retorno da aplicação que assumiu nossa intenção assim como fizemos na foto.
Veja na Listagem 11 como fica nosso método onAcitivityResult na IntentVideo.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
video.setVideoURI(fileUri);
video.start();
} else if (resultCode == RESULT_CANCELED) {
// usuário cancelou a captura
} else {
// falha na captura
}
}
}
Na linha 2 testamos se o resultado da chamada à startActivityForResult retornou o mesmo request code que passamos por parâmetro no referido método. Caso afirmativo, vamos testar qual foi o resultCode. Se o usuário cancelou a captura (linha 6) ou teve algum outro erro (linha 8) poderíamos criar um aviso personalizado.
Porém, se tudo deu certo, o teste if da linha 3 passará e a execução vai para a linha 4. Neste momento informamos à instância de ViewView qual a sua video uri. E, feito isso, basta apenas chamar o método start e o vídeo começa a ser exibido.
Conclusão
Neste artigo iniciamos a discussão sobre a manipulação de mídias em dispositivos Android. Foram apresentados alguns conceitos iniciais sobre as APIs necessárias e também o escopo do projeto que será desenvolvido. Nossa aplicação ainda não está completa, mas já demos um grande passo nesse sentido. No próximo artigo desta série finalizaremos o desenvolvimento da aplicação.
Dominando a captura de Midi – Parte 2
O mundo dos smartphones é fortemente baseado no apelo visual. A interface amigável e o refinamento no acabamento de ícones e aplicativos são considerados um grande diferencial. Além disso, este quesito passa a ser quase questão de sobrevivência na medida em que as lojas virtuais se enchem de possibilidades. Neste artigo daremos continuidade à apresentação do conjunto de APIs que focam na captura de áudio e vídeo. Para isso, finalizaremos o desenvolvimento de um aplicativo simples para manipulação de fotos e vídeos.
O entendimento sobre como manipular vídeos e fotos no Android é útil para todos os que têm interesse no desenvolvimento de aplicações ricas para esta plataforma e que queiram, de alguma forma, diferenciar sua aplicação de outras milhares já disponibilizadas no Google Play.
Uma interface amigável é considerada um grande diferencial para qualquer tipo de sistemas, e em especial, os aplicativos móveis. É essencial ter algo que chame a atenção rapidamente em um universo de aplicações que cresce muito rapidamente.
Ter uma interface amigável não significa apenas que a aplicação seja fácil de utilizar. No mundo mobile, significa também ter uma interface interativa e atraente para o usuário. Isto envolve muitas vezes a manipulação de arquivos de mídia como vídeo, imagem e áudio.
Plataformas modernas de desenvolvimento para dispositivos móveis como Android, iOS e Windows Phone possuem muitos recursos que facilitam a vida do desenvolvedor. Para isso, elas disponibilizam bibliotecas contendo uma série de facilidades para a manipulação da câmera, microfone, edição de imagens, dentre outros.
Toda esta facilidade aliada à evolução crescente do hardware presente nos smartphones aumenta consideravelmente a possibilidade de desenvolvimento de interfaces realmente inovadoras.
Neste artigo daremos continuidade ao desenvolvimento de um aplicativo para manipulação de mídias em Android. O projeto a ser desenvolvido não tem nenhuma pretensão de se tornar uma aplicação comercial, mas sim, de demonstrar de maneira fácil e organizada as formas de captura de fotos e vídeos na plataforma Android.
Capturando mídia profissionalmente – O Preview
Para o entendimento deste trecho do desenvolvimento, é necessária a leitura da parte 1 deste artigo publicada na edição 50 da Mobile Magazine.
Até aqui utilizamos a classe Intent para repassar o trabalho mais difícil a outros aplicativos. Como dito anteriormente, isso é perfeitamente viável e muito útil se a lógica de negócio do nosso software não exige uma tela de captura elabora e estilizada. Porém, em alguns casos teremos que customizar esse processo, atendendo aos requisitos do nosso aplicativo ou exigidos pelos nossos clientes.
Logo nos vem uma questão na mente: teremos que criar uma tela que mostrará um preview da câmera, ou seja, o usuário tem que ver no display do smartphone exatamente a mesma cena que o hardware de captura está visualizando. Graças a duas classes este trabalho não será muito difícil como pode parecer: SurfaceView e SurfaceHolder.Callback.
Vamos analisar a Listagem 1 com a classe CameraPreview. Esta será a classe responsável por este preview que será anexado aos nossos layouts xml.
public class CameraPreview extends SurfaceView
implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
private Camera mCamera;
public CameraPreview(Context context, Camera camera) {
super(context);
mCamera = camera;
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
}
}
public void surfaceDestroyed(SurfaceHolder holder) { }
public void surfaceChanged(SurfaceHolder holder,
int format, int w, int h) {
if (mHolder.getSurface() == null){
return;
}
try {
mCamera.stopPreview();
} catch (Exception e){
}
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
}
}
}
Logo na primeira linha percebemos que a classe estende SurfaceView. Como o nome indica, ela é uma superfície de desenho em cima de uma View. Ou seja, você tem uma Widget UI que pode ser personalizada exatamente do jeito que o programador quiser. Depois falaremos de sua interface.
Na linha 2 e 3 criamos duas variáveis, uma para a classe SurfaceHolder e outra para Camera. O construtor da CameraPreview recebe dois parâmetros, um contexto e a instância de Camera. Consequentemente, ligamos o segundo parâmetro à variável de mesmo tipo (linha 8). Na linha 9 recuperamos o SurfaceHolder associado ao SurfaceView e associamos o mesmo à variável da linha 2.
Cabe falar brevemente sobre o SurfaceHolder. Esta classe é um gerenciador de um display surface, ou seja, daquela região de desenho sobre a View. É ela que permite um controle refinado sobre mudanças de tamanho e formato no surface e pode editar os pixels do mesmo.
Na linha 10 estamos adicionando um callback a esta instância de SurfaceHolde. Como parâmetro no método estamos passando this. Isso porque a classe CameraPreview implementa SurfaceHolder.Callback. Logo vamos falar sobre isso, mas já podemos perceber que a classe tem três métodos que precisam ser sobrescritos quando estamos fazendo este implements, sendo eles: surfaceCreated, .surfaceDestroyed e surfaceChanged.
Ainda no construtor estamos chamando o método setType, de SurfaceHolder, que está depreciado nas versões mais novas do Android (para ser preciso, do Honeycomb em diante). Porém, é aconselhável deixar esta chamada caso seu aplicativo tenha como alvo versões anteriores ao Android 3.0.
Depois disso entramos nos métodos sobrescritos começando pelo surfaceCreated, na linha 14. Quando a surfasse display estiver pronta para uso, podemos passar a instância de SurfaceHolder no método setPreviewDisplay (linha 16) da instância de Camera. Com isso indicamos para a câmera onde ela deve “desenhar” o que ela enxergar através da sua lente. E, na linha 17, inicializamos esta pré-visualização.
Na linha 22 temos o método surfaceDestroyed, chamado quando a área de desenho for destruída. Não estamos implementando nenhum controle porque faremos isso na Activity que fará uso da CameraPreview.
Quando a surfasse view sofre alguma mudança de posição, rotação, tamanho etc., o método surfaceChanged será chamado. Perceba que nos parâmetros recebidos já temos formato, largura e altura. Ou seja, todas as informações necessárias para fornecer uma visualização perfeita para o usuário.
Na linha 26 estamos verificando se o SurfaceHolder ainda está nulo. Caso afirmativo não vamos mudar nada porque o controlador da surface não está pronto. Na linha 31 estamos tentando parar a visualização da câmera para preparar as mudanças necessárias. Esse procedimento é fortemente recomendado pela documentação do site de desenvolvedor do Android.
Estas alterações e customizações poderiam ser feitas antes da linha 35. Neste momento estamos recolocando o holder como saída da pré-visualização da câmera e a iniciamos na linha seguinte. Com isso chegamos ao final da CameraPreview. Agora podemos passar para o próximo ponto que é a sua utilização.
Já temos uma pista no nome da classe que estendemos, a SurfaceView. Perceba que o final dela é igual a ImageView, TextView dentre outros. Isso nos diz que podemos utilizar esta classe como se fosse um componente da interface gráfica do nosso aplicativo, como um Widget UI mesmo.
Vamos estudar o professional_camera.xml que fará uso futuramente da CameraPreview. Observe a Listagem 2.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1" />
<Button
android:id="@+id/button_capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Capture" />
</LinearLayout>
Na linha 2 temos um LinearLayout para organizar os elementos de UI horizontalmente (linha 5). Este gerenciador de layout ocupará toda a tela disponível, por isso está recebendo o valor fill_parent tanto para a largura (linha 3) quanto para a altura (linha 4).
Na linha 7 temos um novo gerenciador de layout, o FrameLayout. Este organiza seus filhos como cartas, sendo que o último inserido aparecerá sobre todos os outros semelhantes ao conceito de z-index utilizado extensivamente em jogos. Será nesse container que colocaremos nossa CameraPreview.
Na linha 8 definimos um id ao FrameLayout. Na linha 9 e 10 definimos sua largura e altura com fill_parent, respectivamente. E na linha 11 informamos que a propriedade layout_weight será igual a 1. Com isso, ele ocupará todo o espaço restante horizontalmente que sua view pai tiver. Assim, a parte do preview sempre ocupará um espaço generoso, independente da tela onde o aplicativo for rodar.
Na linha 13 temos mais uma Widget UI. Desta vez um Button. É ele que servirá como disparo para captura das mídias. Na linha 14 definimos seu id. Tanto sua largura como sua altura são definidos com wrap_content. Alinhamos o componente ao centro do seu pai na linha 17 e, por fim, na linha 18 mudamos seu rótulo através da propriedade text.
Método getCameraInstance na classe Util
Nos códigos mostrados nos últimos parágrafos falamos exaustivamente em uma instância de uma classe Camera. Por exemplo, para a SurfaceView funcionar corretamente e para que seu SurfaceHolder faça seu trabalho, temos que ligar o mesmo ao preview justamente de uma câmera. Para evitar a criação de instâncias de forma descontrolada e objetivando também uma melhor separação do código, decidimos criar um método na classe Util apresentada nas primeiras listagens deste artigo. O mesmo será chamado de getCamera e é mostrado na Listagem 3.
public static Camera getCamera(Context ctx){
Camera c = null;
try {
if (temCamera(ctx)){
c = Camera.open();
}
}
catch (Exception e){ }
return c;
}
private static boolean temCamera (Context context) {
if (context.getPackageManager().hasSystemFeature
PackageManager.FEATURE_CAMERA)){
return true;
} else {
return false;
}
}
Na primeira linha do método criamos a variável da classe Camera. Logo depois temos um bloco try-catch. Na linha 4 estamos fazendo um if com o retorno do método temCamera que nos responde se o smartphone possui o hardware ou não. Este último método é bem simples, o segredo está na linha 13, onde pegamos um PackateManager e indagamos ao mesmo se ele possui a feature identificada com a constante FEATURE_CAMERA.
Voltando ao método getCamera, se o teste if for bem sucedido passamos para linha 5, onde tentamos criar uma instância de Cameraatravés do método CameraOpen. Se algo der errado o método retornará o valor nulo.
Partindo para a opção Professional-Foto
Agora que já passamos pela classe de preview bem como pelo xml que incorporará e permitirá seu uso e pelas mudanças na classe Util, podemos analisar a terceira opção do menu principal do aplicativo que é a Professional-Foto. Observe agora a Listagem 4.
public class ProfessionalFoto extends Activity {
private Camera mCamera;
private CameraPreview mPreview;
private FrameLayout preview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.professional_camera);
mCamera = Util.getCamera(this);
mPreview = new CameraPreview(this, mCamera);
preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
Button captureButton = (Button) findViewById(R.id.button_capture);
captureButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
}
);
}
}
Logo nas primeiras linhas da classe temos três variáveis para classes de extrema importância para esta tela. Na linha 3 a Camera, logo em seguida a CameraPreview e na linha 5 o FrameLayout.
Na linha 8 temos o método sobrescrito onCreate, que já é comum nas nossas listagens e dispensa apresentações. Na linha 10 utilizamos também o já conhecido setContentView, passando o xml visto a pouco chamado de professional_camera.
Na linha 12, através do método da classe Util, capturamos a instância de classe Camera. Para um aplicativo profissional teríamos que fazer todo um controle de estados, por exemplo, não estamos verificando se o retorno deste método foi null e, caso afirmativo, mostrando uma mensagem personalizada ao usuário.
Na linha 14 estamos criando a instância de CameraPrevew, passando os parâmetros necessários que o construtor da referida classe exige. Na linha 15 recuperamos o FrameLayout criado no arquivo XML e na linha 16 acabamos o serviço inserindo o preview dentro do gerenciador de layout. Com isso, já arranjamos no display do smartphone o espaço e o componente que receberá o espelho do que está sendo capturado na câmera do aparelho.
Na linha 18 recuperamos o objeto Button, também um Widget UI que foi colocado no XML. Na linha seguinte, 19, inserimos um listener de clique ao mesmo. E já passamos como parâmetro uma instância da classe View.OnClickListener. Desta forma, sempre que o usuário interagir com o botão na ação de clicar, o método onClick (na linha 22) será chamado e podemos fazer as rotinas necessárias.
No método clique vamos colocar esta linha de código:
public void onClick(View v) {
mCamera.takePicture(null, null, foto);
}
A classe Camera tem um método takePicture onde podemos passar três parâmetros:
- Camera.ShutterCallback: chamado no exato momento que a imagem for capturada. Pode ser utilizado para informar o usuário com um sinal sonoro ou luminoso, por exemplo;
- Camera.PictureCallback (raw): chamado quando os dados crus da imagem já estão disponíveis;
- Camera.PictureCallback (jpeg): chamado quando a imagem comprimida está disponível.
No nosso caso estamos utilizando somente o último parâmetro, sendo uma instância de Camera.PictureCallback que chamamos de foto. Veja na Listagem 5 a implementação desta classe.
private PictureCallback foto = new PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
File pictureFile = Util.getOutputMediaFile(Util.MEDIA_TYPE_IMAGE);
if (pictureFile == null){
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
Bitmap vt = BitmapFactory.decodeFile(Util.localFoto);
vt = Bitmap.createScaledBitmap(vt, 600, 400, false);
ImageView img = new ImageView
(ProfessionalFoto.this);
img.setImageBitmap(vt);
preview.removeAllViews();
preview.addView(img);
} catch (FileNotFoundException e) { } catch (IOException e) { }
}
};
Na linha 4 estamos implementando o método abstrato onPictureTaken, chamado quando a foto estiver pronta. Perceba que o primeiro parâmetro que este método recebe é um vetor de bytes que compõe a imagem capturada. O segundo parâmetro é a Camera que originou a chamada a este método.
Na linha 6 criamos uma instância de File utilizando uma chamada ao método getOutputMediaFile da classe Util. Na linha 7 temos um teste para saber se o arquivo criado está nulo, caso afirmativo, o método retorna nulo e o processo é abortado.
Caso contrário, vamos para a linha 11 que está dentro de um bloco try-catch. Primeiramente criamos uma instância de FileOutputStream na linha 12 que é simplesmente um streaming de dados de saída para um arquivo especificado por parâmetro. Mandamos através deste canal de dados os bytes que compõem a imagem (linha 13) e na linha 14 fechamos o output stream.
Na linha 16 estamos criando uma instância de Bitmap, usando para isso a classe BitmapFactory e seu método decodeFile passando por parâmetro o local que a foto foi salva, armazenado na variável localFoto da classe Util. E na linha 17 escalamos a imagem para não levar tanto tempo para visualizar e evitar problemas em aparelhos com poder de processamento extremamente limitado.
Com o bitmap em mãos podemos criar uma instância da Widget UI Image View (linha 19) e já configurar seu recurso de imagem (linha 20). Na linha 21 estamos removendo todos os componentes gráficos do gerenciador de layout e adicionando o Image View recém criado. Isso fará com que a pré-visualização desapareça da tela e, em seu lugar, o usuário enxergue a foto que acabou de capturar.
Última opção: Professional-Video
Na opção de vídeo vamos ter basicamente o mesmo começo do professional-foto, porém, com mais códigos. Vamos ao esqueleto inicial da classe ProfessionalVideo mostrado na Listagem 6.
public class ProfessionalVideo extends Activity {
private Camera mCamera;
private CameraPreview mPreview;
private FrameLayout preview;
private MediaRecorder mMediaRecorder;
private String lastURIVideo;
private boolean isRecording;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.professional_camera);
mCamera = Util.getCameraInstance(this);
mPreview = new CameraPreview(this, mCamera);
preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
Button captureButton = (Button) findViewById(R.id.button_capture);
captureButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {}
}
);
}
}
Nas primeiras linhas temos algumas declarações de variáveis. Na linha 3 temos a Camera. Na linha 4 a CameraPreview. Sim, também teremos a pré-visualização aqui, aliás, é mais importante do que nunca. Na linha 5 temos o FrameLayout. Cabe lembrar que esta Activity utiliza a mesma view específica no mesmo xml que a IntentFoto utilizou. Na linha 6 temos a MediaRecorder. Na linha 7 temos uma simples string com o endereço do último vídeo gravado. Por fim, temos um valor booleano na linha 9 que indica que estamos gravando ou não.
Novamente temos o onCreate na linha 12 e utilizamos também a view professional_camera, na linha 14. Na linha 16 pegamos a instância de Camera da classe Util. Na linha 18 cria-se a instância de CameraPreview que será colocado no gerenciador de layout FrameLayout recuperado do xml na linha 19.
Na linha 22 recuperamos o botão com o id button_capture. Inserimos o mesmo listener utilizado na foto e, sendo assim, qualquer clique neste Widget UI chamará o método onClick da linha 26.
Antes de entrar de cabeça nos códigos que gravam, teremos que dar uma rápida olhada nos métodos auxiliares que serão utilizados em todo processo de gravação e descartar os recursos que estamos segurando enquanto a gravação estiver acontecendo (ver Listagem 7).
private boolean prepareVideoRecorder(){
mMediaRecorder = new MediaRecorder();
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
lastURIVideo = Util.getOutputMediaFile(Util.MEDIA_TYPE_VIDEO).toString();
mMediaRecorder.setOutputFile(lastURIVideo);
mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());
try {
mMediaRecorder.prepare();
} catch (IllegalStateException e) {
releaseMediaRecorder();
return false;
} catch (IOException e) {
releaseMediaRecorder();
return false;
}
return true;
}
private void releaseMediaRecorder(){
if (mMediaRecorder != null) {
mMediaRecorder.reset();
//clear recorder configuration
mMediaRecorder.release();
//release the recorder object
mMediaRecorder = null;
mCamera.lock();
//take camera access back from MediaRecorder
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
@Override
protected void onPause() {
super.onPause();
releaseMediaRecorder();
releaseCamera();
}
O método preparaVideoRecorder faz uma sequência de passos que será necessário sempre que quisermos preparar de forma correta um MediaRecorder. O primeiro passo é criar a instância da referida classe, que estamos fazendo na linha 2.
Na linha 4 desbloqueamos a câmera. Cabe lembrar que a partir do Android 4.0 esse controle de bloqueio e desbloqueio da câmara é feito de forma automática. Na linha 5 informamos ao MediaRecorder qual é a câmera que vai ser a origem do streaming de dados.
Na linha 6 configuramos a fonte de áudio que, para gravação de vídeo, é indicado utilizar a opção MediaRecorder.AudioSource.CAMCORDER. De forma similar temos a configuração da fonte de vídeo que indica o uso de MediaRecorder.VideoSource.CAMERA. Na linha 10 estamos configurando um perfil de gravação e passando por parâmetro o retorno do método get de CamcorderProfile que, por sua vez, recebeu como parâmetro CamcorderProfile.QUALITY_HIGH.
Também cabe citar que a forma de configuração deste perfil de qualidade de gravação era feita de outra forma antes da versão 2.2 do Android. Porém, como o market share das versões que vão da 1.0 até a 2.1 é muito pequeno, não vale a pena ficar discutindo estas diferenças. Vamos em frente.
Na linha 12 estamos utilizando a já conhecida classe Util para requisitar a URI do local onde o vídeo será gravado. Este mesmo texto será passado ao método setOutputFile ide MediaRecorder. E por fim, na linha 15, configuramos qual será o preview display que o recorder mostrará o vídeo que está capturando. No parâmetro estamos recuperando o surface da holder presente no CameraPreview.
Na linha 18 chamamos o método prepare, que basicamente faz toda a preparação necessária para que o MediaRecorder possa começar a capturar e codificar os dados do novo vídeo. Poderemos ter alguns erros nesses casos que estamos expondo com as cláusulas catch. Caso algum destes problemas ocorra, estamos chamando o método releaseMediaRecorder e retornando false. Caso contrário, retornamos true na linha 26.
O método releaseMediaRecorder começa na linha 29. Caso a instância de MediaRecorder não esteja nulo vamos começar na linha 31 onde chamamos o método reset, que reseta o MediaRecorder para o estado idle (ocioso). Na linha 32 chamamos o método release que libera todos os recursos que foram utilizados. Na linha 33 passamos null para a variável mMediaRecorder. Na linha 34 bloqueamos a câmera, paramos o preview logo em seguida e também liberamos os recursos utilizados pela classe Camera na linha 36.
Na linha 42 estamos sobrescrevendo o método onPause que faz parte do ciclo de vida da Activiyt por um motivo simples e muito importante. Se o aplicativo estiver capturando um vídeo e outro aplicativo assumir o controle do display, é altamente indicado que liberemos os recursos para não comprometer a performance do sistema operacional como um todo.
Com todos esses métodos auxiliares dissecados, vamos voltar para o evento de clique no botão. Analise a última listagem de código, a Listagem 8.
Button captureButton = (Button) findViewById(R.id.button_capture);
captureButton.setOnClickListener(
new View.OnClickListener() {
public void onClick(View v) {
if (isRecording) {
mMediaRecorder.stop();
releaseMediaRecorder();
VideoView video = new VideoView
(ProfessionalVideo.this);
video.setVideoURI(Uri.parse(lastURIVideo));
preview.removeAllViews();
preview.addView(video);
video.start();
isRecording = false;
} else {
if (prepareVideoRecorder()) {
mMediaRecorder.start();
isRecording = true;
}
}
}
});
A sequência de linhas da 1 até a 5 já estavam presentes no código anteriormente. A novidade é o que estamos fazendo dentro do método onClick. Na linha 6 verificamos se ele clicou no botão quando já iniciamos a gravação. Analisaremos primeiro o caso deste teste retornar falso. Neste caso vamos para a linha 17, onde chamaremos o método prepareVideoRecorder. Se o mesmo retornar true, logicamente não tivemos erro na sequência de passos necessários antes de chamar prepare e chamamos o método start na linha 18 mudando o valor de isRecording para true posteriormente.
Caso o isRecording esteja true, ele passará no teste if da linha 6. Observe então a linha 7. Nesse momento o usuário já gravou o vídeo e quer parar o processo e, consequentemente, visualizar o seu conteúdo. Então, paramos a gravação. Na linha 8 chamamos o método utilitário releaseMediaRecorder.
Já na linha 10 estamos criando uma instância de VideoView. Na linha 11 configuramos a uri do vídeo a ser exibido. Na linha 12 removemos todos componentes do FrameLayout e substituímos pelo videoview na linha 13. A linha 14 inicia a reprodução do vídeo no Widget UI. E, finalizando, na linha 15 alteramos para false a variável isRecording.
Conclusão
Neste artigo apresentamos as classes e interfaces da API Android para criação e manipulação de imagens e vídeos. Nada impede o leitor de seguir as instruções apresentadas e construir aplicativos realmente impressionantes.
Acreditamos ter deixado clara a importância de imagens nos aplicativos de hoje. Podemos pecar e perder muitos clientes por não dar a devida atenção a interfaces, a customizações de telas e processos e animações.
E por fim, o leitor deve ter fixado a diferença entre utilizar uma simples Intent para capturar foto e vídeo e personalizar este comportamento. De um lado temos simplicidade e rapidez na produção, porém, ficamos amarrados e não podemos garantir comportamento de interface do nosso aplicativo. Para lidar com isso, temos a customização da tela de captura, porém, teremos mais trabalho e consequentemente mais tempo e custo. Cabe a você leitor, decidir qual é o melhor caminho para o seu projeto.
- Página oficial da IDE Netbeans
- Portal oficial da plataforma Android, com ferramentas, dicas e tutoriais