Leitor de Códigos no Android com Barcode Scanner API - ZXing

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Leitor de Códigos no Android com Barcode Scanner API - ZXing

Leitor de Códigos no Android com Barcode Scanner API - ZXing

Vinícius Thiengo
(2023) (2) (187)
Go-ahead
"Se você está interessado de coração no que você faz, se concentre em construir coisas, em vez de somente falar sobre elas."
Ryan Freitas
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, tudo bem?

Neste artigo vamos, passo a passo, trabalhar o popular leitor de códigos ZXing (Zebra Crossing), por meio da simples interface pública oferecida pela biblioteca Barcode Scanner.

Desta vez o projeto de exemplo é inteiramente dependente da API em estudo. Iremos construir um aplicativo Android completo que terá como responsabilidade oferecer aos usuários a possibilidade de leitura de todas as simbologias de códigos de barras suportados pelo ZXing:

Animação da leitura de QR Code pelo app BarCodeLeitor

Antes de prosseguir, não esqueça de se inscrever 📩 na lista de emails do Blog para receber os conteúdos exclusivos e em primeira mão.

Abaixo os tópicos que estaremos abordando:

Proposta Barcode Scanner API

Você provavelmente já deve ter tido a necessidade de algum aplicativo leitor de código de barras, certo? Ao menos um leitor de QR Code. Em termos de "programação" temos dois famosos projetos open source para que possamos criar rapidamente nossos próprios aplicativos leitores de códigos:

O "porém" para uso de ambos os projetos é que a implementação não é tão trivial como deveria ser, tendo em vista que muitas bibliotecas Android populares precisam de poucas linhas de código, como, por exemplo, a library Android-State.

A biblioteca Barcode Scanner, que atende a partir do Android 8 (Froyo), inicialmente desenvolvida por Dushyanth, tem a proposta de fornecer a rápida integração com qualquer um dos projetos, ZXing ou ZBar, e o fácil uso destes, com uma interface pública simples e que atende às principais funcionalidades.

Caso você ainda não tenha integrado "na unha" nenhuma das duas APIs de leitura de código, logo após o estudo deste conteúdo veja um antigo artigo, com vídeo, que há aqui no Blog sobre como utilizar o ZXing no Android. Nesta época o Eclipse ainda era o principal IDE: Integrando o Leitor de QRCode ZXing no Android.

Por que a escolha do ZXing ao invés do ZBar?

Ambos os projetos são excelentes para a comunidade open source. O ZBar tem uma proposta boa e uma documentação completa sobre o funcionamento interno do projeto.

Porém, ao menos pela interface oferecida pelo Barcode Scanner, quando em produção, mesmo prometendo a possibilidade de leitura de exatos 16 tipos de simbologias de códigos, o ZBar respondeu bem somente para a leitura de QR Codes.

Já o ZXing, mesmo tendo uma interface pública via Barcode Scanner menos trivial do que a do ZBar, não falhou em nenhum dos códigos testados, isso com as mesmas condições: aparelho e sistema Android.

Devido a está discrepância em eficácia entre os dois projetos, optei por manter o estudo somente do ZXing via Barcode Scanner. A seguir a tabela de simbologias de códigos de leitura possível quando utilizando o ZXing:

1D produto1D industrial2D
UPC-ACODE 39QR Code
UPC-ECODE 93Data Matrix
EAN-8CODE 128Aztec (beta)
EAN-13CODABARPDF417 (beta)
 ITFMaxiCode
 RSS-14 
 RSS-Expanded 

Então vamos aos códigos.

Instalação da API

Como estaremos utilizando somente a API ZXing via Barcode Scanner, a referência a seguir no Gradle App Level, ou build.gradle (Module: app), é a necessária:

...
dependencies {
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
}

 

Lembre de utilizar implementation ao invés de compile, o compile está depreciado no Gradle. Lembre também de sempre utilizar a última versão estável da API, na época da construção deste conteúdo a última versão era a 1.9.8. Ao final sincronize o projeto.

Resolvendo problemas de conflitos de versões de bibliotecas

Pode ser que você tenha problemas de incompatibilidade de versão das bibliotecas já em uso em seu projeto com as referenciadas pelo me.dm7.barcodescanner:zxing, neste caso, uma maneira de resolver é excluindo as bibliotecas antigas e compilando o projeto com as mais atuais.

Na época da construção deste artigo o Android Studio apontou que a referência a com.android.support:support-v4 em me.dm7.barcodescanner:zxing era de versão inferior à já referenciada por outras bibliotecas do projeto.

Neste caso optei como solução a "exclusão do módulo antigo" no momento da compilação, ou seja, o módulo support-v4 da biblioteca me.dm7.barcodescanner:zxing:

...
dependencies {
implementation('me.dm7.barcodescanner:zxing:1.9.8') {
exclude module: 'support-v4'
}
}

 

Depois da configuração de exclusão de API é necessário realizar novos testes no aplicativo, isso para garantir que não haverá problemas de crash. Caso você tenha problemas, tente o inverso, excluindo a versão mais atual. Não deixe de também avaliar outras opções de remoção de APIs em conflito.

Lembrando, como o conflito foi com a referência com.android.support:support-v4, temos que:

  • group = com.android.support;
  • module = support-v4.

A versão da API não entra em exclude.

Definindo permissões

Apesar de na documentação apontar somente a necessidade de permissão de câmera, é prudente também solicitar a permissão de uso da luz de flash, mesmo sabendo que está última não é uma dangerous permission.

No AndroidManifest, adicione as duas <uses-permission> em destaque:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.barcodeleitor">

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
...
</manifest>

 

Como a permissão de câmera é uma dangerous permission, é necessário ter em código a solicitação desta permissão em tempo de execução. Você pode entender mais sobre como realizar essa solicitação no conteúdo a seguir: Sistema de Permissões em Tempo de Execução, Android M.

Aqui, como uma breve solução, podemos utilizar a biblioteca EasyPermissions, que é muito bem aceita pela comunidade de desenvolvedores Android, é simples de utilizar e atende a todas as versões em mercado do Android.

No Gradle App Level, ou build.gradle (Module: app), adicione a seguinte referência e sincronize o projeto:

...
dependencies {
implementation 'pub.devrel:easypermissions:1.2.0'
}

 

Já na primeira atividade, ou fragmento, que utiliza a câmera para leitura de código, coloque os algoritmos de solicitação e recebimento (ou não recebimento) de permissão. Algoritmos junto a Interface EasyPermissions.PermissionCallbacks:

class MainActivity : AppCompatActivity(),
EasyPermissions.PermissionCallbacks {

val REQUEST_CODE_CAMERA = 182 /* O INTEIRO DEFINIDO AQUI É ALEATÓRIO */

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

askCameraPermission()
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

/* Encaminhando resultados para EasyPermissions API */
EasyPermissions.onRequestPermissionsResult(
requestCode,
permissions,
grantResults,
this )
}

override fun onPermissionsDenied(
requestCode: Int,
perms: MutableList<String>) {

/* TODO */
}

private fun askCameraPermission(){
EasyPermissions.requestPermissions(
PermissionRequest.Builder( this, REQUEST_CODE_CAMERA, Manifest.permission.CAMERA )
.setRationale( "A permissão de uso de câmera é necessária para que o aplicativo funcione." )
.setPositiveButtonText( "Ok" )
.setNegativeButtonText( "Cancelar" )
.build() )
}

override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

/* AQUI SE INICIA A EXECUÇÃO DA BARCODE SCANNER API - ABERTURA DA CÂMERA */
}
}

 

Note que o código anterior é apenas um exemplo, simples, para conseguirmos a solicitação de permissão de câmera em tempo de execução, logo, não deixe de ler também todo o artigo sobre permissões, aqui do Blog, indicado anteriormente.

Uma ressalva sobre a permissão de acesso ao LED de flash do device: coloque ela se parte de seu domínio do problema exigir também o trabalho com flash, caso contrário, pode descartar essa permissão.

Permitindo a instalação somente em devices com câmera 

É prudente permitir que seu aplicativo apareça na Play Store somente para devices que têm câmera disponível. Para isso adicione ao AndroidManifest.xml a tag em destaque, <uses-feature>:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.barcodeleitor">

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

Definição de ZXingScannerView

No arquivo XML de layout da atividade ou fragmento que terá a responsabilidade de abrir o leitor de código, coloque:

...
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/z_xing_scanner"
android:layout_width="match_parent"
android:layout_height="match_parent" />
...

 

Note que os valores dos atributos são definidos por ti, acima temos apenas um exemplo.

Há a possibilidade de também definir o ZXingScannerView via código de programação, como a seguir:

...
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
val xXingScannerView = ZXingScannerView( this )
setContentView( xXingScannerView )
}
...

Definindo o manipulador de resultados de leitura

Com a Interface ZXingScannerView.ResultHandler vamos conseguir ter o método que permitirá a obtenção do resultado da leitura de código:

class MainActivity : AppCompatActivity(),
ZXingScannerView.ResultHandler,
... {
...

override fun onResume() {
super.onResume()
/*
* Registrando a entidade para que ela possa
* trabalhar os resultados de scan. Seguindo a
* documentação, o código entra no onResume().
* */
z_xing_scanner.setResultHandler( this )

...
}

override fun handleResult(result: Result?) {
Log.i("LOG", "Conteúdo do código lido: ${result!!.text}")
Log.i("LOG", "Formato do código lido: ${result.barcodeFormat.name}")

z_xing_scanner.resumeCameraPreview( this )

/* CÓDIGO DE PROCESSAMENTO DE SIMBOLOGIA LIDA */
}
...
}

 

No código acima o z_xing_scanner é uma referência a View ZXingScannerView. Como estamos trabalhando com Kotlin e o plugin kotlin-android-extensions, não há necessidade de findViewById().

O método setResultHandler() requer uma instância de ZXingScannerView.ResultHandler como argumento. Além do mais, segundo a documentação, o lugar ideal de invocação deste método é no onResume() do fragmento ou atividade em uso.

Se o método resumeCameraPreview() não for invocado posteriormente a leitura de algum código, a tela de leitura fica travada. resumeCameraPreview() também espera uma instância de ZXingScannerView.ResultHandler como argumento.

Lembrando que todo o código apresentado aqui é passível de ser utilizado também em fragmentos.

Iniciando a câmera para leitura de código

Ainda em onResume() invoque o método startCamera():

...
override fun onResume(){
...

/*
* Sem nenhum parâmetro definido em startCamera(),
* digo, o parâmetro idCamera, a câmera de ID 0
* será a utilizada, ou seja, a câmera de tras
* (rear-facing) do device. A câmera da frente
* (front-facing) é a de ID 1.
* */
z_xing_scanner.startCamera()
}
...

 

Como informado em comentário no código anterior:

  • startCamera() sem argumento é equivalente a câmera principal do device, que tende a ser a câmera de tras, rear-facing;
  • startCamera(0) é também equivalente a câmera principal do device, a câmera de tras, rear-facing;
  • startCamera(1) tende a invocar a câmera de frente do device, front-facing.

A partir deste ponto de seu código, digo, do ponto de invocação do método startCamera(), a atenção deve ser redobrada, pois aqui se iniciam os principais problemas caso você não saiba a regra de negócio para invocação deste método:

  • Invoque o startCamera() somente se os recursos de câmera estiverem todos disponíveis, ou seja:
    • Na abertura da entidade que contém o startCamera(), onde nenhum recurso de câmera ainda foi alocado;
    • Depois do stopCamera() completo. Este é crítico, pois a liberação de recursos não é síncrona. No próximo tópico estudaremos melhor o "stopCamera() completo".

Caso não respeite os itens acima, acredite, ou a câmera nem mesmo vai funcionar, ou caso funcione trabalhará de modo inconsistente.

Parando a câmera e liberando recursos

Exatamente no método onPause() da atividade ou fragmento em uso, invoque o algoritmo de stop e liberação de recursos de câmera:

...
override fun onPause() {
super.onPause()

z_xing_scanner.stopCamera()

val camera = CameraUtils.getCameraInstance()
if( camera != null ){
(camera as Camera).release()
}
}
...

 

O trecho de código a partir de CameraUtils.getCameraInstance() é necessário para devices com versões mais antigas do Android, anteriores a API 22, segundo meus testes. Caso o release() não seja invocado e haja alguma estratégia de abertura de câmera em uma outra atividade, por exemplo, a câmera não funcionará nesta nova abertura e travará a anterior.

Em caso de reconstrução de atividade (ou fragmento), sem o release() a câmera também deixará de funcionar.

Ok, mas este código de release() trabalha também para versões do Android que não necessitam dele?

Sim, não terá algum efeito negativo nestes aparelhos.

Muito cuidado com o uso de CameraUtils.getCameraInstance(), isso, pois a invocação deste método, mesmo quando utilizando o argumento de ID de câmera, termina o funcionamento da câmera em tela. Logo, somente utilize CameraUtils.getCameraInstance() depois do stopCamera() ou quando não houver a necessidade de trabalho com câmera em tela.

Isso é um bug?

Provavelmente, pois a interface pública, nativa a Barcode Scanner API, para verificação de recurso de flash depende de CameraUtils.getCameraInstance(). Assim acaba que não podemos utilizar o isFlashSupported() devido a este bug.

Em todos os testes onde necessitei de utilizar algum recurso de câmera, a câmera em tela parava de responder. Logo podemos assumir que esta é uma limitação da API nativa do Android, Camera API. Pois os projetos Barcode Scanner e ZXing ainda utilizam a Camera API ao invés da Camera2 API.

De qualquer forma não deixa de ser um bug em Barcode Scanner, pois nem mesmo na documentação tem algo sobre está limitação. Caso não possa contornar o problema, informe na doc sobre a limitação.

E a verificação de null, if( camera != null ), é necessária?

Sim. Pois, por exemplo, se o usuário entra em uma atividade que tem o uso da barcode Scanner API, porém tendo em mente que ele já veio de outra atividade que também fazia uso desta API... e assumindo que na nova abertura ele rapidamente volte a atividade anterior.

Este "rapidamente" pode ser o suficiente para que a câmera não tenha ainda sido alocada em tela, ou seja, todos os recursos estão sendo liberados da primeira atividade. Desta forma o código CameraUtils.getCameraInstance() retorna null.

Resumo: caso não tenha a verificação, uma exceção é gerada.

Trabalho com luz de flash

Caso você queira oferecer está funcionalidade aos usuários de seu aplicativo Android, o algoritmo é simples. Primeiro a definição da permissão no AndroidManifest:

...
<uses-permission android:name="android.permission.FLASHLIGHT" />
...

 

Depois o código de verificação de existência de luz de flash no device:

...
/*
* Como alguns devices não têm a luz de flash, é
* necessária a verificação para a não geração de
* exception.
* */
fun ZXingScannerView.isFlashSupported(context: Context) =
context
.packageManager
.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
...

 

E enfim o algoritmo que contém, por exemplo, a alternância de status da luz de flash:

...
fun enableFlash(
context: Context,
zXing: ZXingScannerView,
status: Boolean) {

if( zXing.isFlashSupported( context ) ){
zXing.flash = status
}
}
...

 

Note que desta vez o context não é uma instância de ZXingScannerView.ResultHandler e sim uma atividade, facilmente acessível também via fragmento.

O código de verificação de recurso de flash é essencial, caso contrário excessões serão geradas em devices que não têm o flash, e acredite: alguns não têm.

Ok, mas nós temos um recurso de verificação de flash direto na API Barcode Scanner, certo?

Sim, temos, o CameraUtils.isFlashSupported(). Mas como informado em tópico anterior, ele acaba não tendo valor algum, pois necessita do objeto câmera como argumento e sabemos que se acessarmos este objeto a câmera para de funcionar.

Permitindo mudança de orientação de tela

É comum que você queira permitir que o usuário mude a orientação da tela caso, por exemplo, o leitor de código ocupe toda a extensão dela.

Porém, como informado em tópicos anteriores: há um sério problema com a liberação de recursos de câmera. Esta liberação é assíncrona, ou seja, caso haja a invocação do startCamera(), logo na reconstrução de atividade, antes da total liberação de recursos de câmera utilizados na antiga instância da atividade, neste cenário a câmera para de funcionar.

A melhor solução então é utilizar para está atividade o ConfigChanges:

...
<activity
android:configChanges="orientation|screenSize"
android:name=".NameActivity" />
...

 

Assim ela não será reconstruída e não haverá a possiblidade de conflito de chamadas ao startCamera().

AutoFocus

Por padrão o AutoFocus é true. A vantagem de ter o AutoFocus ativo é que a câmera é utilizada em sua maior capacidade para conseguir ler o código em teste:

z_xing_scanner.setAutoFocus(true)

 

A desvantagem é que caso o método resumeCameraPreview() seja invocado logo depois de uma leitura e o código lido esteja ainda em tela, neste cenário várias leituras continuam ocorrendo. Mas isso não tira a qualidade da biblioteca, nem mesmo se compara ao problema de colocar setAutoFocus(false).

Problema?

Sim. A câmera deixa de ser utilizada com maior desempenho para leitura de código... e em devices mais antigos, com câmeras menos robustas, segundo meus testes, somente QR Code é passível de ser lido.

Verificação de câmera em funcionamento

Caso seja uma necessidade de sua lógica de negócio saber quando a câmera já está funcionando na interface do usuário, utilize o método isShow() para isso:

while( z_xing_scanner.isShown() ){
/* TODO */
}

Configuração para devices HUAWEI

Para o correto funcionamento da API Barcode Scanner em devices HUAWEI, a documentação solicita que o seguinte código seja definido antes do startCamera():

/*
* Deve ser utilizado somente para o correto funcionamento
* do CameraPreview em devices HUAWEI.
* */
z_xing_scanner.setAspectTolerance(0.5F)

 

Porém a invocação a setAspectTolerance(0.5F) faz com que em devices de outras marcas a leitura de código fique dificultada se o ZXingScannerView não estiver ocupando toda a tela do device.

Ou seja, para utilizar o método setAspectTolerance() sem problemas em aparelhos diversos, coloque também um algoritmo de verificação de marca de aparelho:

val brand = Build.MANUFACTURER
if( brand.equals("HUAWEI", true) ){
this.setAspectTolerance(0.5F)
}

Outras configurações

Caso queira mudar as cores de borda, utilize o método setBorderColor(), como a seguir:

z_xing_scanner.setBorderColor(Color.RED)

 

Como resultado do código anterior:

Câmera preview com bordas vermelhas

Para mudar a cor do laser, ou linha central, utilize o método setLaserColor(), como a seguir:

z_xing_scanner.setLaserColor(Color.YELLOW)

 

Como resultado do código anterior:

Câmera preview com cor amarela de laser

Para mudar a cor de máscara, fade, utilize o método setMaskColor(), como a seguir:

z_xing_scanner.setMaskColor(Color.BLUE)

 

Como resultado do código anterior:

Câmera preview com cor de máscara azul

Caso queira por algum motivo rotacionar a caixa de leitura da câmera, utilize rotation como a seguir:

z_xing_scanner.rotation = 45.0F

 

Somente valores Float são aceitos. Como resultado do código anterior:

Câmera preview em 45º

Somente trabalhe com rotation se houver real necessidade, pois segundo meus testes a eficiência de leitura de código cai consideravelmente se o valor de rotation não for o padrão: 0.0F.

Se por algum motivo você necessite definir as simbologias de códigos que podem ser lidas, utilize o método setFormats():

z_xing_scanner.setFormats( listOf(BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX) )

 

O código acima informa a API que somente QR Codes e Data Matrix é que podem ser interpretados, os outros formatos devem ser ignorados.

Simples código de exemplo

A seguir um algoritmo funcional e com as definições base para uso do ZXing via Barcode Scanner API:

class MainActivity : AppCompatActivity(),
ZXingScannerView.ResultHandler,
EasyPermissions.PermissionCallbacks {

val REQUEST_CODE_CAMERA = 182

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

askCameraPermission()
}

override fun onResume() {
super.onResume()
z_xing_scanner.setResultHandler( this )
startCamera()
}

override fun onPause() {
super.onPause()
z_xing_scanner.stopCamera()

val camera = CameraUtils.getCameraInstance()
if( camera != null ){
(camera as Camera).release()
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

EasyPermissions.onRequestPermissionsResult(
requestCode,
permissions,
grantResults,
this )
}

override fun onPermissionsDenied(
requestCode: Int,
perms: MutableList<String>) {

askCameraPermission()
}

private fun askCameraPermission(){
EasyPermissions.requestPermissions(
PermissionRequest.Builder(this, REQUEST_CODE_CAMERA, Manifest.permission.CAMERA)
.setRationale("A permissão de uso de câmera é necessária para que o aplicativo funcione.")
.setPositiveButtonText("Ok")
.setNegativeButtonText("Cancelar")
.build() )
}

override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

startCamera()
}

private fun startCamera(){
if( EasyPermissions.hasPermissions( this, Manifest.permission.CAMERA ) ){
z_xing_scanner.startCamera()
}
}

override fun handleResult(result: Result?) {
Log.i("LOG", "Conteúdo do código lido: ${result!!.text}")
Log.i("LOG", "Formato do código lido: ${result.barcodeFormat.name}")
z_xing_scanner.resumeCameraPreview( this )
}
}

Pontos negativos

  • A API Camera1 ainda é utilizada sem o suporte da API Camera2. Mesmo assim funcionou sem problemas em um device Android API 26;
  • A documentação tanto da biblioteca Barcode Scanner quanto do projeto ZXing não são completos em relação a codificação no Android. Muitas das regras de negócio foram descobertas durante os testes;
  • A versão ZBar ainda não está boa para ser liberada e infelizmente, ao menos na referência a API Barcode Scanner no Android-Arsenal, é exatamente o código do ZBar que é utilizado;
  • Não há uma maneira trivial de trabalharmos o zoom e nem mesmo planos para adicionar esta funcionalidade;
  • Não é possível redimensionar, com uma interface simples, o box de leitura de código ou: CameraPreview. O redimensionamento é somente automático, respeitando as dimensões disponíveis para a câmera;
  • Não há uma interface pública segura para realizarmos a conexão com a câmera somente depois de recursos liberados;
  • Se é preciso criar um objeto Result de ZXing, temos de utilizar um construtor mínimo que ainda exige argumentos que não serão utilizados, deveria ter também um construtor esperando somente os dados: conteúdo e formato de código de barra;
  • A Barcode Scanner deveria ter uma mesma interface pública para trabalho com o ZXing ou com o ZBar, assim teríamos de escolher, via constante, por exemplo, com qual trabalhar;
  • Está faltando uma interface simples para corrigir o problema de orientação de câmera, pois em alguns devices, alguns poucos, a câmera fica invertida em relação a posição do device.

Pontos positivos

  • Interface pública muito simples de ser utilizada em relação as interfaces públicas dos projetos ZXing e ZBar;
  • Permite que de maneira trivial tenhamos um leitor de código em apenas parte da tela, com o restante contendo outros conteúdos complementares;
  • Os métodos de customização dos componentes visuais da API facilitam que todo o projeto fique com o mesmo aspecto de design;
  • Quando utilizando o projeto ZXing para leitura de códigos, tudo é bem rápido independente do formato da simbologia de código.

Considerações finais

Apesar dos vários problemas citados, junto a eles também foram expostas soluções que atendem bem a qualquer domínio de problema que necessite de leitura de simbologias de códigos de barras.

De todas as limitações citadas, a que realmente preocupa é o ainda uso somente da Camera API, é preciso que a Camera 2 API seja também trabalhada. Logo, em seu projeto, não deixe de realizar todos os testes possíveis com a interface pública da API Barcode Scanner.

De qualquer forma, principalmente para aqueles que têm conhecimento de causa com o domínio de leitura de simbologias de códigos, a Barcode Scanner API adianta em muito a inclusão desta funcionalidade ao aplicativo e pelo o que vi na comunidade: é a mais bem aceita.

Projeto Android

Desta vez vamos construir um aplicativo completo utilizando a API Barcode Scanner junto ao projeto ZXing. Será um aplicativo leitor de simbologias de códigos de barras que terá uma tela inicial simples, com área de câmera e conteúdo e uma tela fullscreen.

O projeto também está disponível no GitHub a seguir: https://github.com/viniciusthiengo/bar-code-leitor.

Não deixe de acompanhar a construção do aplicativo aqui no artigo, pois haverá explicações de alguns trechos importantes para o funcionamento do app.

Requisitos do app

Misturados, requisitos funcionais e não funcionais, o aplicativo deverá:

  • Permitir que em uma mesma tela seja possível:
    • Ler qualquer simbologia de código de barras suportada pelo ZXing;
    • Mostrar o conteúdo lido, acompanhado do tipo de código lido.
  • Permitir que a luz de flash seja utilizada;
  • Permitir que a leitura de código seja travada e destravada;
  • Permitir que uma tela em fullscreen seja aberta, com possibilidade de mudança de orientação;
  • Permitir que conteúdos lidos como url, email e telefone abram uma opção de ativação, exemplo:
    • Um conteúdo obtido que é identificado como email deverá apresentar alguma forma de invocar os aplicativos de email do device.
  • Persistir localmente o último conteúdo lido;
  • Dar a opção de apagar o último conteúdo lido;
  • Atender a mais de 99% do mercado de devices Android.

Protótipo estático

A seguir as imagens do protótipo estático, desenvolvido antes mesmo de iniciar um novo projeto no Android Studio e baseando-se nos requisitos listados:

 Tela de entrada

 Tela de entrada 

 Tela de leitura de código - vazia

 Tela de leitura de código - vazia

Tela de leitura de código - texto livre

Tela de leitura de código - texto livre

Tela de leitura de código - url

Tela de leitura de código - url

Tela de leitura de código - email

Tela de leitura de código - email

Tela de leitura de código - telefone

Tela de leitura de código - telefone

Tela em fullscreen

Tela em fullscreen

 

Assim podemos partir para a criação e codificação do projeto Android.

Iniciando o projeto

Com o Android Studio aberto, inicie um novo projeto Android Kotlin. Como nome do projeto coloque "BarCodeLeitor". Como API mínima escolha a 16, Jelly Bean - atendendo a mais de 99% dos devices Android no mercado.

A atividade inicial será uma "Empty Activity" com o nome sugerido: MainActivity. Ao final teremos a seguinte arquitetura de projeto:

Arquitetura Android Studio do projeto BarCodeLeitor

Configurações Gradle

A seguir as configurações do Gradle Project Level, ou build.gradle (Project: BarCodeLeitor):

buildscript {
ext.kotlin_version = '1.2.31'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Então as configurações do Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 27
defaultConfig {
applicationId "thiengo.com.br.barcodeleitor"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'

/* Barcode Scanner API com ZXing */
implementation('me.dm7.barcodescanner:zxing:1.9.8') {
exclude module: 'support-v4'
}

/* Para permissões em tempo de execução */
implementation 'pub.devrel:easypermissions:1.2.0'
}

 

Note que desta vez já estamos prosseguindo com todas as definições necessárias do projeto final, pois não haverá primeira e segunda parte, somente a parte de construção do aplicativo.

Logo, já incluímos a referência a biblioteca Barcode Scanner com ZXing e também a library EasyPermissions.

Configurações AndroidManifest

Abaixo as configurações do AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.barcodeleitor">

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />

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

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".FullscreenActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/FullscreenTheme" />
</application>
</manifest>

 

Note que como já temos conhecimento do problema que é manter a câmera ativa via Barcode Scanner API quando a mudança de orientação de tela está desbloqueada, para a atividade MainActivity optei por travar a tela em portrait:

...
<activity
android:name=".MainActivity"
android:screenOrientation="portrait">
...

 

No caso da atividade FullscreenActivity não tinha o porquê de ter a mesma trava, pois é exatamente naquela tela que o usuário poderá escolher como quer ler o código. Somente defini o configChanges para evitar "dores de cabeça":

...
<activity
android:name=".FullscreenActivity"
android:configChanges="orientation|screenSize"
android:theme="@style/FullscreenTheme" />
...

 

Note que foi necessário um tema específico para a FullscreenActivity, isso, pois caso contrário a barra de topo do Material Design não sairia da atividade.

Em tópicos posteriores você verá que teremos de adicionar um código de "full sensor" para que a tela de FullscreenActivity não fique também travada em portrait.

Por algum motivo, em algumas versões antigas do Android, a trava android:screenOrientation="portrait" acaba sendo herdada em outras atividades.

<uses-feature> é para garantir que somente devices Android com câmera possam instalar o aplicativo. Algo opcional, mas recomendado.

Configurações de estilo

Para os arquivos de estilo vamos iniciar com o XML de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#424242</color>
<color name="colorPrimaryDark">#1b1b1b</color>
<color name="colorAccent">#e53935</color>
<color name="tintIconGrey">#888888</color>
<color name="backgroundLightGrey">#f0f0f0</color>
</resources>

 

Então o arquivo de String, /res/values/strings.xml:

<resources>
<string name="app_name">BarCodeLeitor</string>
<string name="fullscreen_mode">Colocar em modo fullscreen</string>
<string name="turn_on_flash">Ligar flash</string>
<string name="lock_code_reader">Travar leitura de barra de código</string>
<string name="info">Informação</string>
<string name="info_description">Posicione a câmera corretamente sobre o código para que a leitura seja realizada.</string>
<string name="last_content_read">Conteúdo da última leitura:</string>
<string name="nothing_read">Nada lido ainda</string>
<string name="barcode_format">Tipo barra de código: </string>
<string name="clear_content_read">Limpar área de conteúdo lido</string>
<string name="open_url">ABRIR URL</string>
<string name="close_fullscreen_mode">Sair de modo fullscreen</string>
<string name="open_email">ABRIR EMAIL</string>
<string name="open_call">LIGAR</string>
<string name="request_permission_description">A permissão de uso de camera é necessária para que o aplicativo funcione.</string>
<string name="request_permission_button_ok">Ok</string>
<string name="request_permission_button_cancel">Cancelar</string>
<string name="unrecognized_code">Código não reconhecido.</string>
</resources>

 

Por fim o arquivo de definição de tema, /res/values/styles.xml:

<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<style
name="FullscreenTheme"
parent="Theme.AppCompat.NoActionBar">
</style>
</resources>

 

Como mencionado no tópico anterior: temos um tema somente para a FullscreenActivity para manter o uso do Material Design, porém sem a barra de topo.

Atividade principal

Vamos começar pelo código base que temos de ter na atividade principal, código de leitura e apresentação de conteúdo. Primeiro o layout da MainActivity, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fillViewport="true"
tools:context=".MainActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<FrameLayout
android:id="@+id/fl_scanner"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true">

<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/z_xing_scanner"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_gravity="top|end" />

<ImageButton
android:id="@+id/ib_fullscreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/fullscreen_mode"
android:onClick="openFullscreen"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_fullscreen_white_24dp"
android:tint="@android:color/white" />

<ImageButton
android:id="@+id/ib_flashlight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="57dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/turn_on_flash"
android:onClick="flashLight"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_flashlight_off_white_24dp"
android:tint="@android:color/white" />

<ImageButton
android:id="@+id/ib_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/lock_code_reader"
android:onClick="lockUnlock"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_lock_open_white_24dp"
android:tint="@android:color/white" />
</FrameLayout>

<LinearLayout
android:id="@+id/ll_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/fl_scanner"
android:background="@color/backgroundLightGrey"
android:orientation="horizontal"
android:padding="16dp">

<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@string/info"
android:scaleType="fitCenter"
android:src="@drawable/ic_info_outline_black_18dp"
android:tint="@color/tintIconGrey" />

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="@string/info_description"
android:textColor="@color/tintIconGrey"
android:textSize="12sp" />
</LinearLayout>

<TextView
android:id="@+id/tv_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_info"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/last_content_read" />

<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@+id/tv_label"
android:background="@android:color/white"
android:orientation="vertical"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="4dp">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/ib_clear"
android:layout_toStartOf="@+id/ib_clear"
android:background="@drawable/text_view_content_border"
android:gravity="center"
android:padding="12dp"
android:text="@string/nothing_read" />

<TextView
android:id="@+id/tv_bar_code_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_content"
android:layout_alignStart="@+id/tv_content"
android:layout_below="@+id/tv_content"
android:text="@string/barcode_format"
android:textSize="12sp"
android:textStyle="bold"
android:visibility="gone" />

<ImageButton
android:id="@+id/ib_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/tv_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/image_button_delete"
android:contentDescription="@string/clear_content_read"
android:onClick="clearContent"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_delete_black_24dp"
android:tint="@android:color/white" />
</RelativeLayout>

<Button
android:id="@+id/bt_open"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/colorPrimary"
android:text="@string/open_url"
android:textColor="@android:color/white"
android:visibility="gone" />
</LinearLayout>
</RelativeLayout>
</ScrollView>

 

Note que no primeiro LinearLayout do layout acima foi necessário colocar uma cor de background:

...
<LinearLayout
...
android:background="@color/backgroundLightGrey"
...>
...

 

Isso, pois em alguns aplicativos aparecia o conteúdo de visualização da câmera ao final da tela. Este problema ocorreu em um device em específico, com o Android API 21, Lollipop.

A seguir o diagrama do layout anterior:

Diagrama do xml activity_main.xml

Assim o código Kotlin, base, da MainActivity:

class MainActivity : AppCompatActivity(),
ZXingScannerView.ResultHandler,
EasyPermissions.PermissionCallbacks {

val REQUEST_CODE_CAMERA = 182 /* Inteiro aleatório */

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

askCameraPermission()
}

override fun onResume() {
super.onResume()
z_xing_scanner.setResultHandler(this)
z_xing_scanner.startCamera()
}

override fun onPause() {
super.onPause()
z_xing_scanner.stopCamera()

val camera = CameraUtils.getCameraInstance()
if( camera != null ){
(camera as Camera).release()
}
}

/* *** Algoritmos de requisição de permissão *** */
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

EasyPermissions.onRequestPermissionsResult(
requestCode,
permissions,
grantResults,
this )
}

override fun onPermissionsDenied(
requestCode: Int,
perms: MutableList<String>) {

askCameraPermission()
}

private fun askCameraPermission(){
EasyPermissions.requestPermissions(
PermissionRequest.Builder(this, REQUEST_CODE_CAMERA, Manifest.permission.CAMERA)
.setRationale( getString(R.string.request_permission_description) )
.setPositiveButtonText( getString(R.string.request_permission_button_ok) )
.setNegativeButtonText( getString(R.string.request_permission_button_cancel) )
.build() )
}

override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

z_xing_scanner.startCamera()
}

/* *** Algoritmos de interpretação de código de barras *** */
override fun handleResult(result: Result?) {
z_xing_scanner.resumeCameraPreview( this )
/* TODO */
}
}

 

Com o código anterior já conseguimos a leitura, se houver permissão de uso de câmera, porém ainda faltam algumas funcionalidades, incluindo a apresentação do conteúdo lido.

Processamento e apresentação do conteúdo lido

O conteúdo lido, se diferente de null, deve entrar no TextView de ID tv_content e o tipo de código lido deve entrar no TextView de ID tv_bar_code_type, este último por padrão está escondido, ele somente é apresentado quando há algum conteúdo.

Na MainActivity, logo após o método handleResult(), adicione os métodos processBarcodeResult() e processBarcodeType():

...
private fun processBarcodeResult(
text: String,
barcodeFormatName: String ){

val result = Result(
text,
text.toByteArray(), /* Somente para ter algo */
arrayOf(), /* Somente para ter algo */
BarcodeFormat.valueOf(barcodeFormatName))

/* Modificando interface do usuário. */
tv_content.text = result.text
processBarcodeType(true, result.barcodeFormat.name)

z_xing_scanner.resumeCameraPreview(this)
}

private fun processBarcodeType(
status: Boolean = false,
barcode: String = ""){
tv_bar_code_type.text = getString(R.string.barcode_format) + barcode
tv_bar_code_type.visibility = if(status) View.VISIBLE else View.GONE
}
...

 

Infelizmente não temos ainda um construtor simples para um objeto Result, por isso temos de colocar alguns argumentos mesmo que não tenhamos de utiliza-los.

O método valueOf() de BarcodeFormat retorna o Enum correto para entrar como quarto parâmetro do construtor de Result.

Note que no código anterior temos novamente o método resumeCameraPreview(), sendo assim podemos atualizar o método handleResult():

...
override fun handleResult(result: Result?) {
proccessBarcodeResult(
result.text,
result.barcodeFormat.name)
}
...

 

Note que não temos um objeto Result como único parâmetro no método proccessBarcodeResult(), pois em outros momentos do algoritmo, mais precisamente na parte em que dados vêm da FullscreenActivity, vamos ter duas String: uma com o conteúdo lido; e outra com o formato do código lido.

Sendo assim conseguimos manter a mesma interface para todas as invocações que ocorrerão.

O método processBarcodeType() tem valores padrões, pois no código de "limpeza de interface" que construiremos ele poderá ser invocado sem argumentos, mantendo o código legível, limpo.

Proteção para resultado null

Como em handleResult() podemos obter um resultado null e tendo em mente que teremos de ter um código de proteção de null tanto na MainActivity como na FullscreenActivity, vamos criar esse algoritmo protetor em um arquivo de funções úteis.

Crie um novo pacote nomeado /util e coloque nele um novo arquivo Kotlin nomeado functions. Assim adicione no novo arquivo o método a seguir:

fun unrecognizedCode( context: Context, callbackClear: ()->Unit = {} ){
Toast
.makeText(
context,
context.getString(R.string.unrecognized_code),
Toast.LENGTH_SHORT )
.show()

callbackClear()
}

 

Note que a sintaxe callbackClear: ()->Unit = {} informa que estamos esperando como segundo parâmetro uma função que não tem nenhum argumento de entrada e nenhum dado de saída, onde caso nada seja informado o valor padrão será um literal de função vazio: {}. Em Kotlin o tipo Unit é equivalente ao void em Java.

Com isso podemos novamente atualizar o código de handleResult():

...
override fun handleResult(result: Result?) {
/*
* Padrão Cláusula de Guarda - Caso o resultado seja
* null, apresente uma mensagem e finaliza o processamento
* do método handleResult().
* */
if( result == null ){
unrecognizedCode(this)
return
}

proccessBarcodeResult(
result.text,
result.barcodeFormat.name)
}
...

 

Ainda precisamos criar uma função para limpar o conteúdo em tela, pois este será o comportamento do app caso um conteúdo inválido, null, seja lido. Essa função será o segundo parâmetro do unrecognizedCode() anterior.

Veja que estamos utilizando o padrão Cláusula de Guarda para facilitar a "não continuação da execução do código".

Antes de criarmos esse "método de limpeza de tela" vamos primeiro ao código de interpretação de conteúdo lido.

Interpretando conteúdos para acionamento de botão

Para acompanhar os TextViews do tópico anterior precisamos também ter um algoritmo para apresentar o Button de ID bt_open. Lembrando que este botão somente deve ser apresentado em tela caso o conteúdo seja:

  • URL;
  • Email;
  • Telefone.

Para isso vamos adicionar dois novos métodos: um para definir a função que acompanhará o código de listener de clique do botão; e outro para poder apresentar ou esconder o Button. Ambos na MainActivity:

...
/*
* Verificação de tipo de conteúdo lido em código de
* barra para o correto trabalho com o botão de ação.
* */
private fun processButtonOpen(result: Result){
when{
URLUtil.isValidUrl(result.text) ->
setButtonOpenAction(resources.getString(R.string.open_url)) {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(result.text)
startActivity(i)
}
Patterns.EMAIL_ADDRESS.matcher(result.text).matches() ->
setButtonOpenAction( getString(R.string.open_email) ) {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse("mailto:?body=${result.text}")
startActivity(i)
}
Patterns.PHONE.matcher(result.text).matches() ->
setButtonOpenAction( getString(R.string.open_call) ) {
val i = Intent(Intent.ACTION_DIAL)
i.data = Uri.parse("tel:${result.text}")
startActivity(i)
}
else -> setButtonOpenAction(status = false)
}
}

/*
* Método de configuração de status e conteúdo do
* botão de acionamento de ação caso o conteúdo da
* barra de código seja: email, url ou telefone.
* */
private fun setButtonOpenAction(
label: String = "",
status: Boolean = true,
callbackClick:()->Unit = {} ){

bt_open.text = label
bt_open.visibility = if(status) View.VISIBLE else View.GONE
bt_open.setOnClickListener { callbackClick() }
}
...

 

Para o método processButtonOpen(), no escopo de when (similar ao switch() do Java) temos alguns códigos de verificação de tipo de conteúdo, utilizando a API nativa do Android:

  • URLUtil.isValidUrl() para verificar se é uma URL válida;
  • Patterns.EMAIL_ADDRESS.matcher(content).matches() para verificar se é um email válido;
  • Patterns.PHONE.matcher(content).matches() para verificar se é um número telefônico válido. Para este último, caso você queira atender a somente usuários no Brasil, seria prudente criar a sua própria expressão regular de verificação de número, pois a versão nativa aponta quase todos os conteúdos numéricos como um número telefônico válido.

Em setButtonOpenAction() temos valores padrões para todos os parâmetros, pois assim podemos utilizar a mesma interface para esconder o Button, quando não há necessidade de rótulo e nem de função de callback.

Com isso podemos atualizar o código de processBarcodeResult() para poder também invocar o algoritmo do Button:

private fun processBarcodeResult(
text: String,
barcodeFormatName: String ){

val result = Result(
text,
text.toByteArray(), /* Somente para ter algo */
arrayOf(), /* Somente para ter algo */
BarcodeFormat.valueOf(barcodeFormatName))

/* Modificando interface do usuário. */
tv_content.text = result.text
processBarcodeType(true, result.barcodeFormat.name)
processButtonOpen(result)

z_xing_scanner.resumeCameraPreview(this)
}

 

Agora podemos salvar e capturar o último conteúdo lido, isso em uma persistência local.

Database de último conteúdo lido

Para persistência local utilizaremos o SharedPreferences, pois é simples e temos poucos dados para serem salvos, na verdade duas Strings: texto do conteúdo lido; texto do formato de código lido.

Note que estaremos salvando o formato do código lido como String, pois com BarcodeFormat.valueOf() conseguiremos utilizar essa String salva para obter o Enum correto.

Ok, mas é preciso salvar também o formato de simbologia de código?

Sim, pois na interface de usuário que construímos temos este dado também sendo apresentado.

No pacote /util adicione a classe Database:

class Database {
companion object {
private val SP_NAME = "SP"
val KEY_NAME = "text"
val KEY_BARCODE_NAME = "barcode_name"

fun saveResult(context: Context, result: Result? = null) {
val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
val contents = result?.text
val barcodeName = result?.barcodeFormat?.name

sp.edit()
.putString(KEY_NAME, contents)
.putString(KEY_BARCODE_NAME, barcodeName)
.apply()
}

fun getSavedResult(context: Context): Result? {
val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
val text = sp.getString(KEY_NAME, null)

/*
* Padrão Cláusula de Guarda - Caso o valor de text
* seja null, não há necessidade de continuar a
* execução do método, pois BarcodeFormat.valueOf()
* também terá como argumento um valor null e uma
* exception será gerada, mesmo sabendo que somente
* o text como null já é o suficiente para informar
* que não há nada válido salvo.
* */
if (text == null) {
return null
}

val barcodeFormat = BarcodeFormat
.valueOf(sp.getString(KEY_BARCODE_NAME, null))
val result = Result(
text,
text.toByteArray(),
arrayOf(),
barcodeFormat)

return result
}
}
}

 

O trabalho com companion object é um estilo de programação pessoal meu. Neste caso de uma base de dados local eu não vejo necessidade de manter algoritmos de criação de instância, prefiro a invocação via modelo de código estático.

Deixei somente a propriedade SP_NAME como private, pois todas as outras serão utilizadas também fora da classe Database.

Ok, mas por que o método saveResult() pode receber um valor null, alias este é o valor padrão?

Isso, pois no código de "limpeza" que ainda construiremos terá também uma invocação para a limpeza dos dados persistidos, até porque eles são os últimos dados lidos. Sendo assim poderemos invocar saveResult() somente com o argumento necessário para limpa-lo, argumento de contexto: this.

Com isso podemos atualizar o método processBarcodeResult() para conter também o código que irá salvar o último conteúdo lido:

...
private fun processBarcodeResult(
text: String,
barcodeFormatName: String ){

val result = Result(
text,
text.toByteArray(), /* Somente para ter algo */
arrayOf(), /* Somente para ter algo */
BarcodeFormat.valueOf(barcodeFormatName))

/* Salvando o último resultado lido. */
Database.saveResult(this, result)

/* Modificando interface do usuário. */
tv_content.text = result.text
processBarcodeType(true, result.barcodeFormat.name)
processButtonOpen(result)

z_xing_scanner.resumeCameraPreview(this)
}
...

 

Agora temos de criar o algoritmo de recuperação de conteúdo salvo, caso haja algum, para invoca-lo logo no onCreate(). Segue novo método, lastResultVerification():

...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

askCameraPermission()
lastResultVerification()
}

/*
* Caso tenha algum último resultado lido e
* salvo no SharedPreferences, utilizamos
* este último resultado já em tela.
* */
private fun lastResultVerification(){
val result = Database.getSavedResult( this )
if( result != null ){
processBarcodeResult( result.text, result.barcodeFormat.name )
}
}
...

 

Assim podemos partir para o algoritmo de limpeza.

Limpando da tela o último dado lido

Ainda na MainActivity coloque o seguinte novo método:

...
/*
* Método para limpar a interface do usuário.
* */
fun clearContent(view: View? = null){
tv_content.text = getString(R.string.nothing_read)
processBarcodeType(false)
setButtonOpenAction(status = false)
Database.saveResult(this)
}
...

 

Assim primeiro vamos atualizar o código em handleResult():

...
override fun handleResult(result: com.google.zxing.Result?) {
/*
* Padrão Cláusula de Guarda - Caso o resultado seja
* null, limpa a tela, se houver um último dado lido,
* apresente uma mensagem e finaliza o processamento
* do método handleResult().
* */
if( result == null ){
unrecognizedCode(this, { clearContent() })
return
}

processBarcodeResult(
result.text,
result.barcodeFormat.name )
}
...

 

Agora quando o código lido for inválido, até mesmo limparemos tudo da tela.

Assim a atualização do layout, colocando o clearContent() vinculado ao botão que indica a limpeza na tela:

...
<ImageButton
android:id="@+id/ib_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/tv_content"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/image_button_delete"
android:contentDescription="@string/clear_content_read"
android:onClick="clearContent"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_delete_black_24dp"
android:tint="@android:color/white" />
...

 

Este botao:

Botão de limpeza de tela

Com isso vamos a algumas melhorias no que temos até o momento.

Corrigindo a lógica para invocação do startCamera()

Temos trechos de código problemáticos em nosso projeto, veja novamente o onResume() da MainActivity:

...
override fun onResume() {
super.onResume()
z_xing_scanner.setResultHandler(this)
z_xing_scanner.startCamera()
}
...

 

Agora o onPermissionsGranted():

...
override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

z_xing_scanner.startCamera()
}
...

 

Não viu o problema? Assim que o aplicativo é acionado o startCamera() é invocado duas vezes, uma no onResume() e outra em onPermissionsGranted().

Com esses dois acionamentos ocorrendo a câmera não terá um comportamento consistente, não quando levarmos em consideração que teremos ainda uma outra atividade que trabalhará também com os recursos de câmera, a atividade de fullscreen.

Adicione a isto o problema de: verificação de permissão que não está ocorrendo no onResume().

O startCamera() presente em onResume() é importante a partir da segunda invocação a este método do ciclo de vida da atividade, onde onPermissionsGranted() não mais é invocado.

Assim podemos criar um método específico para ser invocado somente em onResume(), segue:

...
override fun onResume() {
super.onResume()
z_xing_scanner.setResultHandler(this)
restartCameraIfInactive()
}

/*
* Método necessário para que a câmera volte a
* funcionar, tendo em mente que a partir do onPause()
* até mesmo os recursos destinados a ela, nesta
* entidade, foram todos liberados. Lembrando também
* que em caso de volta a esta atividade, sem passar
* pelo onCreate(), o método onPermissionsGranted()
* não será invocado novamente assim é preciso restart
* à câmera caso ela já não esteja ativa.
* */
private fun restartCameraIfInactive(){
if( !z_xing_scanner.isCameraStarted()
&& EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA) ){

z_xing_scanner.startCamera()
}
}
...

 

O código em onPermissionsGranted() continua o mesmo. Note que o método restartCameraIfInactive() em onResume() será fundamental para o correto funcionamento da API Barcode Scanner na volta da atividade de fullscreen.

Ok, mas quem é isCameraStarted()?

Quase esqueci deste. É o método que nos permitirá saber se a câmera já foi ou não iniciada, para isso teremos de encapsular o startCamera() e o stopCamera() em um arquivo de extensão da View ZXingScannerView.

No pacote /util crie um novo arquivo Kotlin, extension_functions, e adicione:

fun ZXingScannerView.startCameraForAllDevices(context: Context){
this.startCamera()

/*
* Para saber sobre recursos alocados - via
* isCameraStarted()
* */
this.setTag(this.id, true)
}

fun ZXingScannerView.stopCameraForAllDevices(){
this.stopCamera()
this.releaseForAllDevices()

/*
* Para saber sobre recursos liberados - via
* isCameraStarted()
* */
this.setTag(this.id, false)
}

private fun ZXingScannerView.releaseForAllDevices(){
val camera = CameraUtils.getCameraInstance()
if( camera != null ){
(camera as Camera).release()
}
}

/*
* Como não há uma maneira nativa de saber se o método
* startCamera() já foi invocado, o método abaixo, com
* apoio de setTag() da View de leitura de código de
* barra, faz isso para nós.
* */
fun ZXingScannerView.isCameraStarted(): Boolean{
val startData = this.getTag(this.id)
val startStatus = (startData ?: false) as Boolean
return startStatus
}

 

Escolhi estender a View ZXingScannerView para uma melhor leitura de código, assim conseguimos encapsular nesta entidade os códigos fortemente ligados a ela e limpar um pouco a MainActivity.

Tendo em mente que alguns dos métodos em extension_functions serão também utilizados na FullscreenActivity.

Na MainActivity, vamos trocar todos os z_xing_scanner.startCamera() por startCameraForAllDevices().

Primeiro em onPermissionsGranted():

...
override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

startCameraForAllDevices( this )
}
...

 

Então o método recém adicionado, restartCameraIfInactive():

...
private fun restartCameraIfInactive(){
if( !z_xing_scanner.isCameraStarted()
&& EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA) ){
startCameraForAllDevices( this )
}
}
...

 

Ainda temos de atualizar o onPause() da MainActivity, como a seguir:

...
override fun onPause() {
super.onPause()
z_xing_scanner.stopCameraForAllDevices()
}
...

Habilitando a luz de flash

Ainda temos de trabalhar o código de ativação e desativação da luz de flash, mas para isso, antes temos de verificar se há o LED de flash no device.

Como estes são códigos que estão fortemente vinculados ao domínio da API Barcode Scanner com ZXing, vamos estender a classe ZXingScannerView acrescentando dois novos métodos a ela. No arquivo extension_functions adicione:

...
/*
* Como alguns devices não têm a luz de flash, é
* necessária a verificação para a não geração de
* exception.
* */
fun ZXingScannerView.isFlashSupported(context: Context) =
context
.packageManager
.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)

/*
* A ativação e desativação do flash somente pode
* ocorrer caso haja suporte a este hardware.
* */
fun ZXingScannerView.enableFlash(
context: Context,
status: Boolean) {

if( this.isFlashSupported(context) ){
this.flash = status
}
}

 

Assim, na MainActivity, adicione:

...
/*
* Ativa e desativa a luz de flash do celular caso esteja
* disponível no device.
* */
fun flashLight(view: View? = null){
/*
* Utilizando a propriedade tag de Button para salvar o
* valor atual do status de luz de flash.
* */
val value = if(ib_flashlight.tag == null)
true
else
!(ib_flashlight.tag as Boolean)
ib_flashlight.tag = value

if(value){
z_xing_scanner.enableFlash(this, true)
ib_flashlight.setImageResource(R.drawable.ic_flashlight_white_24dp)
}
else{
z_xing_scanner.enableFlash(this, false)
ib_flashlight.setImageResource(R.drawable.ic_flashlight_off_white_24dp)
}
}

/*
* Método necessário, pois não faz sentido deixar a luz
* de flash ligada quando a tela não mais está lendo
* códigos, está travada. Método somente invocado quando
* o lock de tela ocorre.
* */
private fun turnOffFlashlight(){
ib_flashlight.tag = true
flashLight()
}
...

 

Note que no flashLight() estamos utilizando tag como recurso de lógica de negócio, assim evitamos o trabalho com uma nova propriedade somente para conter o status do botão de flash.

O método turnOffFlashlight() é a modularização do algoritmo que desativa a luz de flash. Este é invocado caso o botão de trava de câmera tenha sido acionado. Você entenderá melhor este método no tópico a seguir.

Agora vamos atualizar o ImageButton de flash no layout /res/layout/activity_main.xml para que o método flashLight() possa ser vinculado a ele.

...
<ImageButton
android:id="@+id/ib_flashlight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="57dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/turn_on_flash"
android:onClick="flashLight"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_flashlight_off_white_24dp"
android:tint="@android:color/white" />
...

 

Este botão:

Botão de acionamento de luz de flash

Ainda temos que poder esconder o botão de acionamento de flash caso o device não tenha o LED.

Vamos colocar essa verificação próximo ao startCameraForAllDevices(), para isso vamos criar na MainActivity o nosso próprio startCamera():

...
private fun startCamera(){
if( !z_xing_scanner.isFlashSupported(this) ){
ib_flashlight.visibility = View.GONE
}

z_xing_scanner.startCameraForAllDevices(this)
}
...

 

Com isso, em todos os lugares da MainActivity que havia a invocação startCameraForAllDevices(), colocaremos somente startCamera().

Começando por onPermissionsGranted():

...
override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String>) {

startCamera()
}
...

 

Depois o método restartCameraIfInactive():

...
private fun restartCameraIfInactive(){
if( !z_xing_scanner.isCameraStarted()
&& EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA) ){
startCamera()
}
}
...

 

Assim podemos prosseguir ao botão de trava de leitura de código.

Colocando o lock de câmera

Ainda temos de definir o algoritmo que permitirá que o usuário trave a câmera preview para não mais ler código até que seja destravada.

É possível fazer isso com dois novos simples métodos na MainActivity:

...
/*
* Como o lock não é mantido, o método abaixo é
* necessário para que o usuário veja o unlock
* ocorrendo, caso esteja a câmera como lock.
* */
private fun unlockCamera(){
ib_lock.tag = true
lockUnlock()
}

/*
* Função responsável por mudar o status de funcionamento
* do algoritmo de interpretação de código de barra
* lido, incluindo a mudança do ícone de apresentação,
* ao usuário, de status do algoritmo de interpretação de
* código. Note que a luz e botão de flash não deve funcionar
* se a CameraPreview estiver parada, stopped.
* */
fun lockUnlock(view: View? = null){
/*
* Utilizando a propriedade tag de Button para salvar o
* valor atual do lock de leitura de código, assim não
* temos a necessidade de trabalho com uma nova variável
* de instância somente para manter este valor.
* */
val value = if(ib_lock.tag == null)
true
else
!(ib_lock.tag as Boolean)
ib_lock.tag = value

if( value ){
/*
* Para funcionar deve ser invocado antes do
* stopCameraPreview().
* */
turnOffFlashlight()

/*
* Parar com a verificação de códigos de barra com
* a câmera.
* */
z_xing_scanner.stopCameraPreview()
ib_lock.setImageResource(R.drawable.ic_lock_white_24dp)
ib_flashlight.isEnabled = false
}
else{
/*
* Retomar a verificação de códigos de barra com
* a câmera.
* */
z_xing_scanner.resumeCameraPreview(this)
ib_lock.setImageResource(R.drawable.ic_lock_open_white_24dp)
ib_flashlight.isEnabled = true
}
}
...

 

O método unlockCamera() será utilizado no método que nos permitirá invocar a FullscreenActivity, isso, pois em nosso domínio do problema não faz sentido manter a câmera travada quando há mudança de tela. Ainda vamos construir esse método de invocação da FullscreenActivity.

Ainda temos de colocar o vinculo do método lockUnlock() ao ImageButton dele. Atualize o /res/layout/activity_main.xml como a seguir:

...
<ImageButton
android:id="@+id/ib_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/lock_code_reader"
android:onClick="lockUnlock"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_lock_open_white_24dp"
android:tint="@android:color/white" />
...

 

Este botão:

Botão de trava de CameraPreview

Invocação da FullscreenActivity

Para a MainActivity ainda temos que definir o método de invocação da FullscreenActivity, logo, adicione a ela:

...
val REQUEST_CODE_FULLSCREEN = 184

/*
* Abre a atividade que permite o uso da câmera
* em toda a tela do device.
* */
fun openFullscreen(view: View){
/*
* Padrão Cláusula de Guarda - Sem permissão
* de câmera: não abre atividade.
* */
if( !EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA) ){
return
}

unlockCamera()

/*
* A linha de código abaixo é necessária para
* que não haja o risco de uma exception caso
* o usuário abra o aplicativo e já ative a
* tela de fullscreen.
* */
val value = if(ib_flashlight.tag == null) false else (ib_flashlight.tag as Boolean)

val i = Intent(this, FullscreenActivity::class.java)
i.putExtra(Database.KEY_IS_LIGHTENED, value)
startActivityForResult(i, REQUEST_CODE_FULLSCREEN)
}
...

 

O padrão Cláusula de Guarda é necessário, pois é possível que o usuário não dê permissão de câmera e mesmo assim queira acessar a FullscreenActivity, entidade que não contará com o algoritmo de solicitação de permissão em tempo de execução.

Agora temos de vincular este método ao botão da interface de MainActivity:

...
<ImageButton
android:id="@+id/ib_fullscreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/fullscreen_mode"
android:onClick="openFullscreen"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_fullscreen_white_24dp"
android:tint="@android:color/white" />
...

 

Este botão:

Botão de acionamento da tela de fullscreen

Veja que em Database adicionamos uma nova KEY para trafegar o status atual da luz de flash:

class Database {
companion object {
...
val KEY_IS_LIGHTENED = "is_lightened"

...
}
}

 

Assim podemos ir ao código da fullscreen.

Atividade de fullscreen

Como com a MainActivity, para a FullscreenActivity também iniciaremos pelo layout, /res/layout/activity_fullscreen.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fl_scanner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:background="@color/colorPrimary"
tools:context=".FullscreenActivity">

<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/z_xing_scanner"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<ImageButton
android:id="@+id/ib_fullscreen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/close_fullscreen_mode"
android:onClick="closeFullscreen"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_fullscreen_exit_white_24dp"
android:tint="@android:color/white" />

<ImageButton
android:id="@+id/ib_flashlight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="57dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/turn_on_flash"
android:onClick="flashLight"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_flashlight_off_white_24dp"
android:tint="@android:color/white" />

<ImageButton
android:id="@+id/ib_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="@drawable/image_button_black"
android:contentDescription="@string/lock_code_reader"
android:onClick="lockUnlock"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_lock_open_white_24dp"
android:tint="@android:color/white" />
</FrameLayout>

 

Então o diagrama do layout anterior:

Diagrama do XML activity_fullscreen.xml

O código da FullscreenActivity tem várias invocações de métodos já explicados em tópicos anteriores, além de ser uma classe com algoritmos menores do que os da MainActivity.

A seguir o código completo da fullscreen, é importante a leitura de todos os comentários:

class FullscreenActivity : AppCompatActivity(),
ZXingScannerView.ResultHandler {

val KEY_IS_LOCKED = "is_locked"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

/* Algoritmo de requisição de modo fullscreen. */
requestWindowFeature( Window.FEATURE_NO_TITLE )
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN )

setContentView(R.layout.activity_fullscreen)

/*
* O código abaixo é para garantir que ib_lock.tag
* sempre terá um valor Boolean depois do onCreate().
* */
ib_lock.tag = if(ib_lock.tag == null) false else (ib_lock.tag as Boolean)
if( savedInstanceState != null ){
ib_lock.tag = savedInstanceState.getBoolean(KEY_IS_LOCKED)
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_IS_LOCKED, ib_lock.tag as Boolean)
}

override fun onResume() {
super.onResume()
z_xing_scanner.setResultHandler(this)
startCamera()

/*
* Para manter os status da luz de flash
* da câmera como ativa / não ativa,
* status vindo da atividade anterior.
* */
ib_flashlight.tag = false /* Garantindo um valor inicial. */
if( intent != null ){
ib_flashlight.tag = !intent.getBooleanExtra(Database.KEY_IS_LIGHTENED, false)
flashLight()
}

/*
* Necessário para manter o status da trava
* da câmera, ativa / não ativa, quando houver
* reconstrução de atividade.
* */
if(ib_lock.tag as Boolean){
ib_lock.tag = !(ib_lock.tag as Boolean)
lockUnlock()
}

z_xing_scanner.threadCallWhenCameraIsWorking{
runOnUiThread {
/*
* Caso a linha de código abaixo não esteja presente,
* em algumas versões do Android está atividade também
* ficará travada em portrait screen devido ao uso
* desta trava no AndroidManifest.xml, mesmo que a trava
* esteja definida somente para a atividade principal.
* Outra, a invocação da linha abaixo deve ocorrer
* depois que a câmera já está em funcionamento
* na tela, caso contrário há a possibilidade de a
* câmera não funcionar em alguns devices.
* */
setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR )
}
}
}

override fun onPause() {
super.onPause()
z_xing_scanner.stopCameraForAllDevices()
}

private fun startCamera(){
if( !z_xing_scanner.isFlashSupported(this) ){
ib_flashlight.visibility = View.GONE
}

z_xing_scanner.startCameraForAllDevices(this)
}

/*
* Para que o comportamento de exit activity do
* domínio do problema seja respeitado.
* */
override fun onBackPressed() {
closeFullscreen()
}


/* *** Algoritmos de interpretação de barra de código *** */
override fun handleResult( result: Result? ) {
/*
* Padrão Cláusula de Guarda - Caso o resultado seja
* null, apresente um mensagem e finaliza o processamento
* do método handleResult().
* */
if( result == null ){
unrecognizedCode(this)
return
}

proccessBarcodeResult( result )
}

fun proccessBarcodeResult( result: Result ){
val text = result.text
val barcodeName = result.barcodeFormat.name

val i = Intent()
i.putExtra( Database.KEY_NAME, text )
i.putExtra( Database.KEY_BARCODE_NAME, barcodeName )
finish( i, Activity.RESULT_OK )
}

/*
* Volta para a atividade principal sempre com o isLightened
* fazendo parte do conteúdo de resposta.
* */
fun finish(intent: Intent, resultAction: Int) {
intent.putExtra(Database.KEY_IS_LIGHTENED, ib_flashlight.tag as Boolean)
setResult( resultAction, intent )
finish()
}


/* *** Algoritmos de listeners de clique *** */

/*
* Para voltar ao modo "não fullscreen" antes que algum
* código de barra seja interpretado.
* */
fun closeFullscreen(view: View? = null){
unlockCamera()
finish( Intent(), Activity.RESULT_CANCELED )
}

/*
* Como o lock não é mantido, o método abaixo é
* necessário para que o usuário veja o unlock
* ocorrendo, caso esteja como lock, a câmera.
* */
private fun unlockCamera(){
ib_lock.tag = true
lockUnlock()
}

/*
* Ativa e desativa a luz de flash do celular caso esteja
* disponível.
* */
fun flashLight(view: View? = null){
ib_flashlight.tag = !(ib_flashlight.tag as Boolean)

/*
* A linha de código abaixo é necessária, pois caso haja
* uma reconstrução de atividade o valor obtido para
* ib_flashlight.tag vem da intent em memória, então a
* intent tem que estar com o valor atual.
* */
intent.putExtra(Database.KEY_IS_LIGHTENED, ib_flashlight.tag as Boolean)

if(ib_flashlight.tag as Boolean){
z_xing_scanner.enableFlash(this, true)
ib_flashlight.setImageResource(R.drawable.ic_flashlight_white_24dp)
}
else{
z_xing_scanner.enableFlash(this, false)
ib_flashlight.setImageResource(R.drawable.ic_flashlight_off_white_24dp)
}
}

/*
* Função responsável por mudar o status de funcionamento
* do algoritmo de interpretação de código de barra
* lido, incluindo a mudança do ícone de apresentação,
* ao usuário, de status do algoritmo de interpretação de
* código.
* */
fun lockUnlock(view: View? = null){
ib_lock.tag = !(ib_lock.tag as Boolean)

if(ib_lock.tag as Boolean){
/*
* Para funcionar deve ser invocado antes do
* stopCameraPreview().
* */
turnOffFlashlight()

z_xing_scanner.stopCameraPreview()
ib_lock.setImageResource(R.drawable.ic_lock_white_24dp)
ib_flashlight.isEnabled = false
}
else{
z_xing_scanner.resumeCameraPreview(this)
ib_lock.setImageResource(R.drawable.ic_lock_open_white_24dp)
ib_flashlight.isEnabled = true
}
}

/*
* Método necessário, pois não faz sentido deixar a luz
* de flash ligada quando a tela não mais está lendo
* códigos, está travada. Método invocado quando o lock
* de tela ocorre.
* */
private fun turnOffFlashlight(){
ib_flashlight.tag = true
flashLight()
}
}

 

Vale lembrar que a FullscreenActivity faz uso do configChanges, logo é bem provável que todos os códigos não vinculados a listeners de clique sejam invocados somente uma única vez enquanto está atividade estiver aberta.

Porém foi necessário colocar todo o código de segurança caso haja reconstrução de atividade, pois a configuração do configChanges está para somente: mudança de orientação de tela; e mudança de tamanho de interface do usuário.

Ou seja, caso o usuário receba uma ligação telefônica enquanto usa o aplicativo, haverá uma reconstrução de atividade na volta ao app BarCodeLeitor.

Eu sei que você notou o método threadCallWhenCameraIsWorking(). Ele foi criado para que métodos que causam problemas quando são invocados antes da câmera estar em tela possam ser invocados somente nesta condição: com a câmera já em funcionamento.

O método setRequestedOrientation() que está auxiliando versões antigas do Android que travam a tela em portrait devido a MainActivity, em alguns de meus testes a invocação deste método travava a câmera caso esta invocação ocorresse antes de a câmera estar funcional, rodando na interface do usuário.

Com o threadCallWhenCameraIsWorking() e um delay de um segundo este problema foi resolvido. O nome disso é hackcode, acredite: é comum em qualquer projeto de software.

Você deve ter notado também que há muito código similar ao da MainActivity e que poderia ser encapsulado até mesmo em extension_functions, certo?

Essa tarefa de codificação limpa eu deixo para ti. Vamos adicionar ao projeto o método threadCallWhenCameraIsWorking() e então colocar na MainActivity o algoritmo que recebe os dados da FullscreenActivity.

Método de processamento assíncrono

No arquivo extension_functions adicione o seguinte método:

...
/*
* Para métodos em que é seguro invoca-los somente
* depois que a câmera está funcionando em tela.
* */
fun ZXingScannerView.threadCallWhenCameraIsWorking(callback: ()->Unit){
thread {
while( !this.isShown ){
SystemClock.sleep(1000) /* 1 segundo foi o tempo ideal para não parar com o funcionamento da câmera. */
}

callback()
}
}

Recebendo na MainActivity os dados da fullscreen

Na atividade principal adicione o método onActivityResult() para que seja possível processarmos os dados encaminhados da FullscreenActivity:

...
/*
* Método herdado utilizado para que seja possível
* interpretar qualquer valor vindo da atividade
* de fullscreen cam.
* */
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if( requestCode == REQUEST_CODE_FULLSCREEN ){
/*
* Garantindo que o botão de luz de flash e o
* status controlado por ele continuem com os
* valores corretos.
* */
ib_flashlight.tag = !data!!.getBooleanExtra(Database.KEY_IS_LIGHTENED, false)
flashLight()

if( resultCode == Activity.RESULT_OK ){
processBarcodeResult(
data.getStringExtra(Database.KEY_NAME),
data.getStringExtra(Database.KEY_BARCODE_NAME) )
}
}
}

 

Veja que o flash é a única entidade que transita o mesmo status entre as duas atividades presentes no projeto. Com isso podemos partir para os testes.

Testes e resultados

Lendo e limpando códigos na primeira tela:

Animação da leitura de Qr Code em app Android

Lendo códigos com a tela de fullscreen:

Animação leitura QR Code em fullscreen no Android

Com isso terminamos por completo um aplicativo de leitor de simbologias de códigos de barras utilizando a API Barcode Scanner junto ao projeto ZXing.

Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.

Se inscreva também no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a apresentação completa da Barcode Scanner API com projeto ZXing:

Para acessar o projeto de exemplo junto a API Barcode Scanner, entre no GitHub a seguir:https://github.com/viniciusthiengo/bar-code-leitor.

Conclusão

Mesmo com os vários problemas apontados para a API Barcode Scanner, quando utilizando o ZXing via interface pública dela o custo / benefício é ainda válido ante ao uso direto e complexo do ZXing ou ao uso da ainda não muito eficaz ZBar library.

Há inúmeras outras APIs de leitura de código, mas muitas delas são específicas, permitindo, por exemplo: a leitura de somente QR Codes. Ou são pagas, estas costumam dar alguns dias de uso gratuito.

Quanto a Barcode Scanner API, não esqueça da necessidade do configChanges e do entendimento das regras de negócio para alocação e liberação de recurso de câmera.

Como desafio, tente fazer o swipe de câmeras sendo uma funcionalidade simples do app. O swipe sendo possível com um único touch no botão correto, este que pode vir acima do botão de flash.

Comente abaixo suas dúvidas e o que achou da Barcode Scanner. Indique suas próprias APIs caso as tenha.

Não deixe de se inscrever na 📩 lista de emails.

Abraço.

Fontes

Documentação Barcode Scanner library

Documentação projeto ZXing

Documentação projeto ZBar

An error occurred while connecting to camera: 0 #193

How do I set the preview size? #527

Camera View is rotated

Documentação Android <uses-feature>

How to check if device has flash light led android? - Resposta de Erik B

Get Android Phone Model Programmatically? - Resposta de Jared Rummler

Programmatically make app FULL SCREEN in Android? - Resposta de Samir Mangroliya

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

MVP AndroidMVP AndroidAndroid
Android Studio: Instalação, Configuração e OtimizaçãoAndroid Studio: Instalação, Configuração e OtimizaçãoAndroid
Trabalhando Análise Qualitativa em seu Aplicativo AndroidTrabalhando Análise Qualitativa em seu Aplicativo AndroidAndroid
Como Reter Objetos Utilizando Android-State APIComo Reter Objetos Utilizando Android-State APIAndroid

Compartilhar

Comentários Facebook (2)

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...