Guia para utilização da câmera no Flutter

Guia para utilização da câmera no Flutter

Nos dias de hoje, os aplicativos não se limitam apenas em exibir informações estáticas. A capacidade de interagir com o ambiente ao redor tornou-se uma parte integral da experiência do usuário. Nesse contexto, a integração da câmera em aplicativos desempenha um papel fundamental, permitindo uma gama diversificada de recursos, desde simples capturas de fotos e vídeos até o uso de algoritmos de visão computacional para classificação de objetos.

Neste artigo, exploraremos o uso de diferentes recursos da câmera dos smartphones com a linguagem dart e o framework flutter.

Configurações iniciais

O primeiro passo é a criação de um projeto Flutter. Se você ainda não possui o ambiente de desenvolvimento configurado, siga as instruções na documentação oficial do Flutter para instalar o Flutter SDK e configurar suas ferramentas. Após isso, você pode criar um novo projeto usando o seguinte comando:

flutter create nome_do_projeto

Este comando criará uma estrutura de pastas básica para o seu projeto, incluindo o arquivo “lib/main.dart”, que é o ponto de entrada da sua aplicação. E dentro do arquivo “pubspec.yaml” é onde colocaremos as bibliotecas do projeto. Com o projeto iniciado, adicione as bibliotecas camera e image_picker dentro do “pubspec.yaml” embaixo de dependencies:

dependencies:
  camera: ^0.10.5+2
  path_provider: ^2.1.0
  permission_handler: ^10.4.3

Depois, dentro do arquivo “main.dart” na pasta lib, importe os módulos necessários:

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

Agora precisamos de uma estrutura básica para a aplicação, contendo a função main que é o ponto de entrada, um StatelessWidget que vai retornar o MaterialApp com o menu da câmera como home (esta parte é especialmente importante, pois mais na frente precisaremos pegar o size do MaterialApp para definir o tamanho do preview da câmera) e por fim, um StatefulWidget que vai retornar um Scaffold:

void main() {
  runApp(const MyCameraApp());
}


class MyCameraApp extends StatelessWidget {
  const MyCameraApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CameraMenu(),
    );
  }
}

class CameraMenu extends StatefulWidget {
  const CameraMenu({Key? key}) : super(key: key);

  @override
  State<CameraMenu> createState() => _CameraMenuState();
}

class _CameraMenuState extends State<CameraMenu> {

  @override
  build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Camera App'),
      ),
    );
  }
}

Obtendo permissões da câmera no Android

Para utilizar certos recursos do dispositivo, é necessário pedir a permissão do usuário. A biblioteca permission_handler vai facilitar esse processo, pois devemos projetar o aplicativo para responder de acordo com a interação do usuário, isso significa que, se ele aceitar a solicitação, a câmera vai abrir normalmente, se ele recusar, uma mensagem vai aparecer na tela dizendo que a permissão da câmera é necessária e se ele recusar novamente ou de forma permanente, as configurações do aplicativo no dispositivo serão abertas para que ele modifique as permissões por lá.

Para configurar a câmera, primeiro a versão mínima do sdk deve ser mudada para 21 no arquivo build.gradle dentro da pasta android/app:

defaultConfig {
  minSdkVersion 21
}

Dentro do estado do CameraMenu, primeiro criamos uma função chamada requestPermission, ela é assíncrona e vai solicitar ao dispositivo para que a câmera possa ser utilizada, depois o estado do Widget será alterado com o novo valor do estado para a variável _permissionStatus:

PermissionStatus _permissionStatus = Permission.denied;

Future<void> requestPermission() async {
  final status = await Permission.camera.request();
  setState(() {
  _permissionStatus = status;
});

Uma função será responsável por chamar o openAppSettings da biblioteca permission_handler para abrir as configurações e no início do estado chamamos a função requestPermission:

void _openAppSettings() {
  openAppSettings();
}

@override
void initState() {
  super.initState();
  requestPermission();
}

Agora dentro do Scaffold, adicionamos um Widget Center, dentro dele uma coluna contendo um texto com o valor da variável _permissionStatus para sabermos o estado atual da permissão de acesso à câmera, um botão elevado que quando pressionado chama a função requestPermission e um outro texto e botão que serão desenhados se o valor do Status for permanentlyDenied, esse botão vai chamar a função _openAppSettings:

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text(
        'Camera Permission Status: $_permissionStatus',
        style: const TextStyle(fontSize: 18),
      ),
      const SizedBox(height: 20),
      ElevatedButton(
        onPressed: requestPermission,
        child: const Text('Request Camera Permission'),
      ),
      if (_permissionStatus == PermissionStatus.permanentlyDenied)
        Column(
          children: [
            const SizedBox(height: 20),
            const Text('Please allow camera access to use this feature.',
                    style: TextStyle(fontSize: 16),),
            const SizedBox(height: 10),
            ElevatedButton(
                onPressed: _openAppSettings,
                child: const Text('Open App Settings'),
            ),
          ],
        ),
    ],
  ),
),

Feito isso, a aplicação já tem a funcionalidade de pedir a permissão do usuário para utilizar a câmera e reagir de acordo com a sua resposta. O próximo passo é utilizar da biblioteca camera para utilizar as funções da câmera. Para isso, no início do estado, primeiro criamos uma variável para listar as câmeras disponíveis e outra para o controlador da câmera:

late List<CameraDescription> cameras;
late CameraController cameraController;

Depois, é preciso iniciar a câmera quando o elemento for iniciado e liberar quando o elemento for destruído. Uma função específica assíncrona vai obter as câmeras disponíveis, instanciar e iniciar o controlador, mudando o estado da variável da câmera inicializada para true, a inicialização do controlador deve ser colocada dentro de um bloco try/catch, pois caso o usuário não permita o uso da câmera ou ocorra algum erro na hora de pegar a lista de câmeras disponíveis, será retornado null e vai dar erro no aplicativo.

  bool isCameraInitialized = false;
 
  @override
  void initState() {
    super.initState();
    requestPermission();
  }

  void dispose() {
    cameraController.dispose();
    super.dispose();
  }

  void startCamera() async {
    cameras = await availableCameras();
    cameraController = CameraController(
      cameras[0],
      ResolutionPreset.high,
      enableAudio: false,
    );
    try {
      await cameraController.initialize();
      if (!mounted) return;
      setState(() {
        isCameraInitialized = true;
      });
    } catch (e) {
      print(e);
    }
  }

A função “CameraController” é como se inicializa o controlador da câmera, nela é passada primeiro a câmera a ser selecionada, depois a resolução (que pode ser baixa, média ou alta), o parâmetro enableAudio (que se colocado em falso, só vai habilitar a câmera para capturar vídeos sem áudio), o parâmetro para definir o formato das imagens (por exemplo, jpeg), dentre outros.

Agora, precisamos de uma função que vai retornar um Widget para que possamos alterar no body. Ela vai começar verificando se a permissão da câmera está habilitada, se não estiver, vai retornar o que já tínhamos no body, já se estiver liberado, vai conferir se a câmera ainda não foi inicializada para chamar a função startCamera e retornar um widget com texto informando que a câmera está sendo iniciada, quando finalizar o carregamento, o estado da aplicação será atualizado e a condição vai passar a ser verdadeira, então será retornado o widget contendo o CameraPreview:

Widget cameraElement() {
    if (_permissionStatus == PermissionStatus.granted) {
      if (isCameraInitialized == false) {
        startCamera();
        return const Center(
          child: Text('Starting Camera...'),
        );
      } else {
        return Container(
          child: CameraPreview(cameraController),
        );
      }
    } else {
      return Center(...); // Adicionar o que tinha no body
  }
}

Agora, basta substituir o que tinha no body do Scaffold pela chamada da função cameraElement:

body: cameraElement()

Mudar o tamanho do preview

Para mudar o tamanho do preview, é necessário colocar ele dentro de um container contendo width e height. Outros elementos podem ser adicionados por cima do preview se este container estiver dentro de uma stack como o primeiro elemento, então os elementos abaixo na stack ficarão por cima do preview, dessa forma, você pode adicionar botões, criar menus, colocar as bounding boxes para indicar a posição de elementos no preview com algoritmos de visão computacional e muito mais!

Se quiser deixar o preview do tamanho da tela, a stack precisará estar dentro do scaffold e este dentro do MaterialApp. Pegamos o tamanho da tela pelo context do MaterialApp e então passamos como parâmetro para o width e height do container onde o preview está:

Size? size;

@override
build(BuildContext context) {
    size = MediaQuery.of(context).size;
    return Scaffold(
        body: Stack(
            children: [
                Container(
                    width: size!.width,
                    height: size!.height,
                    child: CameraPreview(controller!)
                )
            ]
        )
    );
}

Mudando Configurações Padrões

Depois que o controlador da câmera é inicializado, algumas funções podem ser chamadas para alterar as configurações padrões da câmera, como o flash, foco e zoom:

cameraController.setFlashMode(FlashMode.off);
cameraController.setFocusMode(FocusMode.locked);
cameraController.setZoomLevel(4.0);

Tirando fotos com a câmera

Para tirar uma foto precisa utilizar a função takePicture do controller, que pode ser colocado em um MaterialButton, sendo que o onPressed deve ser assíncrono, pois para tirar uma foto, as ações necessárias para o hardware podem demorar um tempo imprevisível, pois envolve vários fatores, como por exemplo: iniciar o hardware da câmera, aguardar o foco automático, tirar a foto e salvar em um arquivo. Tarefas que podem ser influenciadas pela capacidade de processamento do dispositivo.

MaterialButton(
    color: Colors.red,
    child: const Icon(
        Icons.camera_alt,
        color: Colors.white,
    ),
    onPressed: () async {
        final image = await cameraController!.takePicture();
        print(image.path);
    }
),

Pegar o stream de imagens da câmera

Capturar o stream de imagens da câmera permite analisar em tempo real utilizando algoritmos de visão computacional o que está sendo capturado pela câmera e reagir desenhando bounding boxes na tela para indicar que um objeto está sendo identificado. Para pegar esse stream, na função de inicialização do controlador da câmera (que é assíncrona), quando ela estiver pronta, deve ser chamada a função startImageStream e quando o Widget for desmontado, o stream deve ser parado:

@override
void dispose() {
    cameraController?.stopImageStream();
    cameraController?.dispose();
    super.dispose();
}
...
try {
  await cameraController?.initialize().then((_) => {
  controller!.startImageStream((image) => print(Datetime.now().microsecondsSinceEpoch))});
} catch (e) {
  print(e);
} // Colocar este bloco na parte da inicialização do controlador

É importante notar que o imageFormatGroup não deve ser especificado na inicialização do controle da câmera quando se for utilizar o ImageStream, pois dará erro e o stream não iniciará. Neste exemplo, se tudo estiver funcionando corretamente, a hora atual estará sendo logada no terminal em tempo real.

Quando o stream estiver funcionando, uma variável do tipo CameraImage pode ser criada para armazenar a imagem que está sendo retornada pelo ImageStream:

late CameraImage _cameraImage;
...
cameraController!.startImageStream((image) => _cameraImage = image)

Conclusão

Neste artigo, abordamos desde as configurações iniciais para instalar as bibliotecas no aplicativo e obter as permissões necessárias para utilizar os recursos de hardware dos smartphones até a criação do controlador e utilização de algumas das funcionalidades da câmera, como alterar o foco, dar zoom, ativar o flash, iniciar o preview dentro de um widget, tirar fotos e pegar o stream de imagens. Ainda existem diversas funcionalidades para aprimorar a experiência do usuário no aplicativo ao utilizar a câmera que poderemos explorar em um próximo artigo, como a possibilidade de gravar vídeos, salvar imagens e vídeos na galeria do dispositivo, além da integração do stream de imagens com o tensorflow lite para criar um aplicativo capaz de reconhecer objetos em tempo real.

A linguagem dart e o framework flutter, aliados com as diversas bibliotecas que existem para trabalhar com os recursos de hardware e inteligência artificial, criam uma abstração e abrem a possibilidade da criação de tecnologias avançadas multiplataforma com um esforço relativamente pequeno, comparado à necessidade de desenvolver tudo nativamente, proporcionando uma ótima experiência de desenvolvimento. A demanda por tecnologias portáteis e eficientes vem crescendo cada vez mais em diferentes mercados e aqueles profissionais que conseguirem se especializar para entrar neste mercado certamente terão ótimas oportunidades em um futuro próximo.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.