Skip to content

Instantly share code, notes, and snippets.

@h0landa
Created September 29, 2025 20:55
Show Gist options
  • Select an option

  • Save h0landa/d00388c467efd37b6b55f59efda747c1 to your computer and use it in GitHub Desktop.

Select an option

Save h0landa/d00388c467efd37b6b55f59efda747c1 to your computer and use it in GitHub Desktop.
Página de Câmera
import 'dart:developer';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
class CameraPage extends StatefulWidget {
const CameraPage({super.key});
@override
State<CameraPage> createState() => _CameraPageState();
}
IconData getCameraLensIcon(CameraLensDirection direction) {
switch (direction) {
case CameraLensDirection.back:
return Icons.camera_rear;
case CameraLensDirection.front:
return Icons.camera_front;
case CameraLensDirection.external:
return Icons.camera;
}
}
class _CameraPageState extends State<CameraPage>
with WidgetsBindingObserver, TickerProviderStateMixin {
CameraController? _controller;
XFile? arquivoDeImagem;
double _minAvailableZoom = 1.0;
double _maxAvailableZoom = 1.0;
double _baseScale = 1.0;
double _currentScale = 1.0;
Offset? _focusPoint;
late AnimationController _focusController;
int _pointers = 0;
FlashMode _currentFlashMode = FlashMode.off;
@override
void initState() {
super.initState();
_focusController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
WidgetsBinding.instance.addObserver(this);
_checkCameraPermission();
}
void _showPermissionToConfigDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Permissão necessária'),
content: const Text(
'Este aplicativo precisa de acesso à câmera.'
'Ative a permissão nas configurações do dispositivo.',
),
actions: [
TextButton(
onPressed: () {
context.pop();
context.go('/');
},
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
openAppSettings();
context.pop();
},
child: const Text('Abrir Configurações'),
),
],
);
},
);
}
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
final CameraController? cameraController = _controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
await cameraController.resumePreview();
log('RESUME');
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
await cameraController.pausePreview();
log('PREVIEW PAUSADO');
case AppLifecycleState.detached:
log('DETACEHED');
}
super.didChangeAppLifecycleState(state);
}
Future<void> _checkCameraPermission() async {
final status = await Permission.camera.request();
if (status.isGranted) {
await _initializeBackCameraController();
}
if (status.isDenied) {
if (mounted) context.pop();
}
if (status.isPermanentlyDenied) {
_showPermissionToConfigDialog();
}
}
@override
void dispose() {
log('CAMERA FECHADA');
arquivoDeImagem = null;
_focusController.dispose();
_controller?.dispose();
_controller = null;
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
_cameraPreviewWidget(),
if (_focusPoint != null) _buildFocusIndicator(),
Positioned(
top: MediaQuery.of(context).padding.top - 20,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
decoration: BoxDecoration(
color: const Color.fromARGB(241, 0, 0, 0),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => context.go('/'),
icon: const Icon(Icons.close, color: Colors.white),
),
),
_buildFlashButton(),
],
),
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16,
left: 0,
right: 0,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(flex: 2),
GestureDetector(
onTap: takePicture,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
child: Container(
margin: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
const Spacer(flex: 1),
Padding(
padding: const EdgeInsets.only(right: 16),
child: _buildZoomButton(
'$_currentScale',
_currentScale,
_currentScale,
),
),
],
),
],
),
),
],
),
),
);
}
Widget _cameraPreviewWidget() {
final CameraController? cameraController = _controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return const SizedBox();
} else {
return Listener(
onPointerDown: (_) => _pointers++,
onPointerUp: (_) => _pointers--,
child: CameraPreview(
_controller!,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleStart: _handleScaleStart,
onScaleUpdate: _handleScaleUpdate,
onTapDown: onFocusPoint,
);
},
),
),
);
}
}
Future<void> onFocusPoint(TapDownDetails details) async {
if (_controller == null || !_controller!.value.isInitialized) {
return;
}
final Offset tapPosition = details.localPosition;
setState(() {
_focusPoint = tapPosition;
});
final Size? previewSize = _controller?.value.previewSize;
if (previewSize == null) return;
final Offset normalizedPoint = Offset(
tapPosition.dx / previewSize.width,
tapPosition.dy / previewSize.height,
);
try {
await _controller!.setFocusPoint(normalizedPoint);
await _controller!.setFocusMode(FocusMode.auto);
} on CameraException catch (e) {
_showCameraException(e);
}
}
Widget _buildFocusIndicator() {
if (_focusPoint == null) {
return const SizedBox.shrink();
}
final animation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_focusController);
return AnimatedBuilder(
animation: _focusController,
builder: (context, child) {
if (!_focusController.isAnimating && _focusController.value == 0.0) {
return const SizedBox.shrink();
}
return Positioned(
left: _focusPoint!.dx - 25,
top: _focusPoint!.dy - 25,
child: FadeTransition(
opacity: animation,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
border: Border.all(color: Colors.yellow, width: 2),
borderRadius: BorderRadius.circular(5),
),
),
),
);
}
);
}
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
if (_controller == null) {
return;
}
_currentScale = (_baseScale * details.scale).clamp(
_minAvailableZoom,
_maxAvailableZoom,
);
await _controller!.setZoomLevel(_currentScale);
setState(() {
});
}
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
void showInSnackBar(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _initializeBackCameraController() async {
final cameras = await availableCameras();
final backCamera = cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.back,
orElse: () => cameras.first,
);
log('CAMERA INICIADA');
final CameraController cameraController = CameraController(
backCamera,
ResolutionPreset.medium,
imageFormatGroup: ImageFormatGroup.jpeg,
enableAudio: false,
);
_controller?.dispose();
_controller = cameraController;
cameraController.addListener(() {
if (mounted) {
setState(() {});
}
if (cameraController.value.hasError) {
showInSnackBar(
'Erro de câmera: ${cameraController.value.errorDescription}',
);
}
});
try {
await cameraController.initialize();
await Future.wait(<Future<void>>[
cameraController.getMaxZoomLevel().then(
(double value) => _maxAvailableZoom = value,
),
cameraController.getMinZoomLevel().then(
(double value) => _minAvailableZoom = value,
),
]);
await cameraController.setFlashMode(FlashMode.off);
_currentFlashMode = FlashMode.off;
} on CameraException catch (e) {
switch (e.code) {
case 'CameraAccessDenied':
showInSnackBar('Acesso de câmera negado.');
break;
case 'CameraAccessDeniedWithoutPrompt':
showInSnackBar('Ative o acesso a câmera nas configurações');
break;
default:
_showCameraException(e);
}
}
if (mounted) {
setState(() {});
}
}
void onTakePictureButtonPressed() {
takePicture().then((XFile? file) {
if (mounted) {
setState(() {
arquivoDeImagem = file;
});
if (file != null) {
showInSnackBar('Foto salva em ${file.path}');
}
}
});
}
FlashMode _getNextFlashMode(FlashMode current) {
switch (current) {
case FlashMode.off:
return FlashMode.auto;
case FlashMode.auto:
return FlashMode.always;
case FlashMode.always:
return FlashMode.off;
case FlashMode.torch:
return FlashMode.off;
}
}
void onFlashModeButtonPressed() async {
if (_controller == null || !_controller!.value.isInitialized) {
return;
}
final nextMode = _getNextFlashMode(_currentFlashMode);
try {
await _controller!.setFlashMode(nextMode);
setState(() {
_currentFlashMode = nextMode;
});
} on CameraException catch (e) {
_showCameraException(e);
}
}
Future<void> setFlashMode(FlashMode mode) async {
if (_controller == null) {
return;
}
try {
await _controller!.setFlashMode(mode);
} on CameraException catch (e) {
_showCameraException(e);
rethrow;
}
}
Widget _buildFlashButton() {
IconData icon;
Color color;
switch (_currentFlashMode) {
case FlashMode.off:
icon = Icons.flash_off;
color = Colors.white;
case FlashMode.auto:
icon = Icons.flash_auto;
color = Colors.lightBlueAccent;
case FlashMode.always:
icon = Icons.flash_on;
color = Colors.yellow;
case FlashMode.torch:
icon = Icons.highlight;
color = Colors.orange;
}
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(241, 0, 0, 0),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: onFlashModeButtonPressed,
icon: Icon(icon, color: color),
),
);
}
Future<XFile?> takePicture() async {
final CameraController? cameraController = _controller;
if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Erro: selecione a câmera primeiro.');
return null;
}
if (cameraController.value.isTakingPicture) {
return null;
}
try {
final XFile file = await cameraController.takePicture();
log('foto tirada');
return file;
} on CameraException catch (e) {
_showCameraException(e);
return null;
}
}
void _showCameraException(CameraException e) {
showInSnackBar('Error: ${e.code}\n${e.description}');
}
Widget _buildZoomButton(String text, double zoomLevel, double currentZoom) {
final bool isActive = (currentZoom - zoomLevel).abs() < 0.1;
return GestureDetector(
onTap: () => _onZoomChanged(zoomLevel),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isActive ? Colors.yellow : Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
child: Text(
"${zoomLevel.toStringAsFixed(1)}x",
style: TextStyle(
color: isActive ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
);
}
void _onZoomChanged(double value) async {
if (_controller == null) return;
final currentScale = value.clamp(_minAvailableZoom, _maxAvailableZoom);
await _controller!.setZoomLevel(currentScale);
setState(() {
});
}
}
@carlosdesenvolvedor
Copy link

import 'dart:developer';

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

class CameraPage extends StatefulWidget {
  const CameraPage({super.key});

  @override
  State<CameraPage> createState() => _CameraPageState();
}

IconData getCameraLensIcon(CameraLensDirection direction) {
  switch (direction) {
    case CameraLensDirection.back:
      return Icons.camera_rear;
    case CameraLensDirection.front:
      return Icons.camera_front;
    case CameraLensDirection.external:
      return Icons.camera;
  }
}

class _CameraPageState extends State<CameraPage>
    with WidgetsBindingObserver, TickerProviderStateMixin {
  // Controle de debounce para evitar múltiplos cliques rápidos no botão de foto
  bool _isTakingPicture = false;
  CameraController? _controller;
  XFile? arquivoDeImagem;
  double _minAvailableZoom = 1.0;
  double _maxAvailableZoom = 1.0;

  double _baseScale = 1.0;
  double _currentScale = 1.0;

  Offset? _focusPoint;
  late AnimationController _focusController;

  int _pointers = 0;

  FlashMode _currentFlashMode = FlashMode.off;

  bool _isInitializing = false;

  @override
  void initState() {
    super.initState();
    _focusController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    WidgetsBinding.instance.addObserver(this);
    _checkCameraPermission();
  }

  void _showPermissionToConfigDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Permissão necessária'),
          content: const Text(
            'Este aplicativo precisa de acesso à câmera.'
            'Ative a permissão nas configurações do dispositivo.',
          ),
          actions: [
            TextButton(
              onPressed: () {
                context.pop();
                context.go('/');
              },
              child: const Text('Cancelar'),
            ),
            TextButton(
              onPressed: () {
                openAppSettings();
                context.pop();
              },
              child: const Text('Abrir Configurações'),
            ),
          ],
        );
      },
    );
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    final CameraController? cameraController = _controller;
    log('AppLifecycleState: $state');

    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    switch (state) {
      case AppLifecycleState.paused:
      case AppLifecycleState.inactive:
      case AppLifecycleState.hidden:
      case AppLifecycleState.detached:
        // Pausa o preview para economizar recursos quando o app não está visível.
        try {
          await cameraController.pausePreview();
        } catch (e) {
          log('Erro ao pausar preview: $e');
        }
        break;
      case AppLifecycleState.resumed:
        try {
          await cameraController.resumePreview();
        } catch (e) {
          log('Erro ao retomar preview: $e');
        }
        break;
    }
  }

  Future<void> _checkCameraPermission() async {
    final status = await Permission.camera.request();
    if (status.isGranted) {
      await _initializeBackCameraController();
    }

    if (status.isDenied) {
      if (mounted) context.pop();
    }

    if (status.isPermanentlyDenied) {
      _showPermissionToConfigDialog();
    }
  }

  @override
  void dispose() {
    // AGORA É VOID, NÃO FUTURE<VOID>
    log('CAMERA FECHADA - DISPOSE SYNC');

    // 1. Liberação síncrona de recursos
    _focusController.dispose();
    WidgetsBinding.instance.removeObserver(this);

    // 2. Chama dispose do controlador (fire-and-forget)
    // O controlador será liberado em segundo plano sem bloquear
    _controller?.removeListener(_cameraListener);
    _controller?.dispose();
    _controller = null; // Zera a referência

    arquivoDeImagem = null;

    // 3. SEMPRE CHAME super.dispose() NO FINAL
    super.dispose();
  }

  // Listener para o CameraController, extraído para poder ser removido no dispose.
  void _cameraListener() {
    if (mounted) {
      setState(() {});
    }
    if (_controller != null && _controller!.value.hasError) {
      showInSnackBar('Erro de câmera: ${_controller!.value.errorDescription}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            _cameraPreviewWidget(),
            if (_focusPoint != null) _buildFocusIndicator(),
            Positioned(
              top: MediaQuery.of(context).padding.top - 20,
              left: 16,
              right: 16,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Container(
                    decoration: BoxDecoration(
                      color: const Color.fromARGB(241, 0, 0, 0),
                      shape: BoxShape.circle,
                    ),
                    child: IconButton(
                      onPressed: () => context.go('/'),
                      icon: const Icon(Icons.close, color: Colors.white),
                    ),
                  ),
                  _buildFlashButton(),
                ],
              ),
            ),

            Positioned(
              bottom: MediaQuery.of(context).padding.bottom + 16,
              left: 0,
              right: 0,
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Spacer(flex: 2),
                      GestureDetector(
                        onTap: _onTakePictureDebounced,
                        child: Container(
                          width: 80,
                          height: 80,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            border: Border.all(color: Colors.white, width: 4),
                          ),
                          child: Container(
                            margin: const EdgeInsets.all(8),
                            decoration: const BoxDecoration(
                              color: Colors.white,
                              shape: BoxShape.circle,
                            ),
                          ),
                        ),
                      ),
                      const Spacer(flex: 1),
                      Padding(
                        padding: const EdgeInsets.only(right: 16),
                        child: _buildZoomToggle(),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  // Função debounce para tirar foto
  Future<void> _onTakePictureDebounced() async {
    if (_isTakingPicture) return;
    setState(() {
      _isTakingPicture = true;
    });
    try {
      final XFile? file = await takePicture();
      if (mounted) {
        setState(() {
          arquivoDeImagem = file;
        });
        if (file != null) {
          showInSnackBar('Foto salva em ${file.path}');
        }
      }
    } finally {
      if (mounted) {
        setState(() {
          _isTakingPicture = false;
        });
      } else {
        _isTakingPicture = false;
      }
    }
  }

  Widget _cameraPreviewWidget() {
    final CameraController? cameraController = _controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      return const SizedBox();
    } else {
      return Listener(
        onPointerDown: (_) => _pointers++,
        onPointerUp: (_) => _pointers--,
        child: CameraPreview(
          _controller!,
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              return GestureDetector(
                behavior: HitTestBehavior.opaque,
                onScaleStart: _handleScaleStart,
                onScaleUpdate: _handleScaleUpdate,
                onTapDown: onFocusPoint,
              );
            },
          ),
        ),
      );
    }
  }

  Future<void> onFocusPoint(TapDownDetails details) async {
    if (_controller == null || !_controller!.value.isInitialized) {
      return;
    }

    final Offset tapPosition = details.localPosition;

    setState(() {
      _focusPoint = tapPosition;
    });

    final Size? previewSize = _controller?.value.previewSize;

    if (previewSize == null) return;

    final Offset normalizedPoint = Offset(
      tapPosition.dx / previewSize.width,
      tapPosition.dy / previewSize.height,
    );

    try {
      await _controller!.setFocusPoint(normalizedPoint);
      await _controller!.setFocusMode(FocusMode.auto);
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Widget _buildFocusIndicator() {
    if (_focusPoint == null) {
      return const SizedBox.shrink();
    }

    final animation = Tween<double>(
      begin: 1.0,
      end: 0.0,
    ).animate(_focusController);

    return AnimatedBuilder(
      animation: _focusController,
      builder: (context, child) {
        if (!_focusController.isAnimating && _focusController.value == 0.0) {
          return const SizedBox.shrink();
        }
        return Positioned(
          left: _focusPoint!.dx - 25,
          top: _focusPoint!.dy - 25,
          child: FadeTransition(
            opacity: animation,
            child: Container(
              width: 50,
              height: 50,
              decoration: BoxDecoration(
                border: Border.all(color: Colors.yellow, width: 2),
                borderRadius: BorderRadius.circular(5),
              ),
            ),
          ),
        );
      },
    );
  }

  void _handleScaleStart(ScaleStartStartDetails details) {
    _baseScale = _currentScale;
  }

  Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
    if (_controller == null) {
      return;
    }

    final newScale = (_baseScale * details.scale).clamp(
      _minAvailableZoom,
      _maxAvailableZoom,
    );
    _currentScale = newScale;
    await _controller!.setZoomLevel(_currentScale);

    setState(() {});
  }

  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

  void showInSnackBar(String message) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  Future<void> _initializeBackCameraController() async {
    if (_isInitializing) {
      return;
    }
    _isInitializing = true;

    try {
      log('Buscando câmeras disponíveis...');
      final cameras = await availableCameras();
      log('Câmeras encontradas: ${cameras.length}');
      if (cameras.isEmpty) {
        log('Nenhuma câmera disponível no dispositivo.');
        showInSnackBar('Nenhuma câmera disponível.');
        return;
      }
      final backCamera = cameras.firstWhere(
        (c) => c.lensDirection == CameraLensDirection.back,
        orElse: () => cameras.first,
      );

      log('CAMERA INICIADA (${backCamera.name})');

      // Descarta o controlador antigo ANTES de criar um novo.
      // Isso garante uma limpeza completa antes de começar um novo ciclo.
      await _controller?.dispose();

      // Usar resolução média como padrão
      final CameraController cameraController = CameraController(
        backCamera,
        ResolutionPreset.medium,
        imageFormatGroup: ImageFormatGroup.jpeg, // Garante a saída em JPEG
        enableAudio: false, // Desativa o áudio
      );

      // Atribui o novo controlador à variável de estado
      _controller = cameraController;
      cameraController.addListener(_cameraListener);

      log('Inicializando cameraController...');
      await cameraController.initialize();
      log('CameraController inicializado. Buscando níveis de zoom...');
      await Future.wait(<Future<void>>[
        cameraController.getMaxZoomLevel().then((double value) {
          _maxAvailableZoom = value;
          log('Max zoom: $value');
        }),
        cameraController.getMinZoomLevel().then((double value) {
          _minAvailableZoom = value;
          log('Min zoom: $value');
        }),
      ]);
      log('Configurando flash...');
      await cameraController.setFlashMode(FlashMode.off);
      _currentFlashMode = FlashMode.off;

      if (mounted) {
        setState(() {});
      }
      log('Câmera pronta para uso.');
    } on CameraException catch (e) {
      log('CameraException: ${e.code} - ${e.description}');
      switch (e.code) {
        case 'CameraAccessDenied':
          showInSnackBar('Acesso de câmera negado.');
          break;
        case 'CameraAccessDeniedWithoutPrompt':
          showInSnackBar('Ative o acesso a câmera nas configurações');
          break;
        default:
          _showCameraException(e);
      }
    } catch (e) {
      log('Erro inesperado ao inicializar a câmera: $e');
      showInSnackBar('Erro inesperado ao inicializar a câmera.');
    } finally {
      if (mounted) {
        _isInitializing = false;
      }
    }
  }

  void onTakePictureButtonPressed() {
    takePicture().then((XFile? file) {
      if (mounted) {
        setState(() {
          arquivoDeImagem = file;
        });
        if (file != null) {
          showInSnackBar('Foto salva em ${file.path}');
        }
      }
    });
  }

  FlashMode _getNextFlashMode(FlashMode current) {
    switch (current) {
      case FlashMode.off:
        return FlashMode.auto;
      case FlashMode.auto:
        return FlashMode.always;
      case FlashMode.always:
        return FlashMode.off;
      case FlashMode.torch:
        return FlashMode.off;
    }
  }

  void onFlashModeButtonPressed() async {
    if (_controller == null || !_controller!.value.isInitialized) {
      return;
    }

    final nextMode = _getNextFlashMode(_currentFlashMode);

    try {
      await _controller!.setFlashMode(nextMode);

      setState(() {
        _currentFlashMode = nextMode;
      });
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Future<void> setFlashMode(FlashMode mode) async {
    if (_controller == null) {
      return;
    }

    try {
      await _controller!.setFlashMode(mode);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  Widget _buildFlashButton() {
    IconData icon;
    Color color;

    switch (_currentFlashMode) {
      case FlashMode.off:
        icon = Icons.flash_off;
        color = Colors.white;
      case FlashMode.auto:
        icon = Icons.flash_auto;
        color = Colors.lightBlueAccent;
      case FlashMode.always:
        icon = Icons.flash_on;
        color = Colors.yellow;
      case FlashMode.torch:
        icon = Icons.highlight;
        color = Colors.orange;
    }

    return Container(
      decoration: const BoxDecoration(
        color: Color.fromARGB(241, 0, 0, 0),
        shape: BoxShape.circle,
      ),
      child: IconButton(
        onPressed: onFlashModeButtonPressed,
        icon: Icon(icon, color: color),
      ),
    );
  }

  Future<XFile?> takePicture() async {
    final CameraController? cameraController = _controller;
    if (cameraController == null || !cameraController.value.isInitialized) {
      showInSnackBar('Erro: selecione a câmera primeiro.');
      return null;
    }

    if (cameraController.value.isTakingPicture) {
      log('Já está tirando uma foto, ignorando chamada duplicada.');
      return null;
    }

    try {
      log('Iniciando captura de foto...');
      final XFile file = await cameraController.takePicture();
      log('foto tirada');
      return file;
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    } catch (e) {
      log('Erro inesperado ao tirar foto: $e');
      showInSnackBar('Erro inesperado ao tirar foto.');
      return null;
    }
  }

  void _showCameraException(CameraException e) {
    showInSnackBar('Error: ${e.code}\n${e.description}');
  }

  Widget _buildZoomToggle() {
    // Define o próximo nível de zoom. Se o atual for 1.0, o próximo será 2.0, senão volta para 1.0.
    // Garante que o zoom 2.0 não ultrapasse o máximo permitido.
    final double nextZoomLevel = _currentScale < 1.5
        ? 2.0.clamp(_minAvailableZoom, _maxAvailableZoom)
        : 1.0;

    return GestureDetector(
      onTap: () => _onZoomChanged(nextZoomLevel),
      child: Container(
        width: 48,
        height: 48,
        decoration: BoxDecoration(
          color: const Color.fromARGB(150, 0, 0, 0),
          shape: BoxShape.circle,
        ),
        child: Center(
          child: Text(
            '${_currentScale.toStringAsFixed(1)}x',
            style: const TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _onZoomChanged(double value) async {
    if (_controller == null) return;
    _currentScale = value;
    await _controller!.setZoomLevel(_currentScale);
    setState(() {});
  }
}

@carlosdesenvolvedor
Copy link

teste os dois, esse mudei somente como te falei lá no discord A correção removeu um vazamento de memória ao transformar o método dispose() de assíncrono para síncrono (@OverRide void dispose()). Essa mudança fundamental permite que o Flutter descarte a tela imediatamente. A liberação do controlador de câmera (_controller?.dispose()) agora ocorre em segundo plano (sem await), garantindo que o widget não fique retido na memória esperando a conclusão dessa tarefa demorada, eliminando o acúmulo de recursos como o CameraXProxy. Além disso, foi adicionada a remoção explícita do listener para evitar referências circulares.

import 'dart:developer';

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

class CameraPage extends StatefulWidget {
  const CameraPage({super.key});

  @override
  State<CameraPage> createState() => _CameraPageState();
}

IconData getCameraLensIcon(CameraLensDirection direction) {
  switch (direction) {
    case CameraLensDirection.back:
      return Icons.camera_rear;
    case CameraLensDirection.front:
      return Icons.camera_front;
    case CameraLensDirection.external:
      return Icons.camera;
  }
}

class _CameraPageState extends State<CameraPage>
    with WidgetsBindingObserver, TickerProviderStateMixin {
  CameraController? _controller;
  XFile? arquivoDeImagem;
  double _minAvailableZoom = 1.0;
  double _maxAvailableZoom = 1.0;

  double _baseScale = 1.0;
  double _currentScale = 1.0;

  Offset? _focusPoint;
  late AnimationController _focusController;

  int _pointers = 0;

  FlashMode _currentFlashMode = FlashMode.off;

  @override
  void initState() {
    super.initState();
    _focusController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );
    WidgetsBinding.instance.addObserver(this);
    _checkCameraPermission();
  }

  void _showPermissionToConfigDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Permissão necessária'),
          content: const Text(
            'Este aplicativo precisa de acesso à câmera.'
            'Ative a permissão nas configurações do dispositivo.',
          ),
          actions: [
            TextButton(
              onPressed: () {
                context.pop();
                context.go('/');
              },
              child: const Text('Cancelar'),
            ),
            TextButton(
              onPressed: () {
                openAppSettings();
                context.pop();
              },
              child: const Text('Abrir Configurações'),
            ),
          ],
        );
      },
    );
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    final CameraController? cameraController = _controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }
    switch (state) {
      case AppLifecycleState.resumed:
        await cameraController.resumePreview();
        log('RESUME');
      case AppLifecycleState.paused:
      case AppLifecycleState.inactive:
      case AppLifecycleState.hidden:
        await cameraController.pausePreview();
        log('PREVIEW PAUSADO');
      case AppLifecycleState.detached:
        log('DETACEHED');
    }
    super.didChangeAppLifecycleState(state);
  }

  Future<void> _checkCameraPermission() async {
    final status = await Permission.camera.request();
    if (status.isGranted) {
      await _initializeBackCameraController();
    }

    if (status.isDenied) {
      if (mounted) context.pop();
    }

    if (status.isPermanentlyDenied) {
      _showPermissionToConfigDialog();
    }
  }

  @override
  void dispose() {
    log('CAMERA FECHADA - DISPOSE SYNC');

    // 1. Liberação síncrona de recursos
    _focusController.dispose();
    WidgetsBinding.instance.removeObserver(this);

    // 2. Remove o listener e chama dispose do controlador (fire-and-forget)
    // O controlador será liberado em segundo plano sem bloquear
    _controller?.removeListener(_cameraListener);
    _controller?.dispose();
    _controller = null; // Zera a referência

    arquivoDeImagem = null;

    // 3. SEMPRE CHAME super.dispose() NO FINAL
    super.dispose();
  }

  // Listener extraído para poder ser removido no dispose.
  void _cameraListener() {
    if (mounted && _controller != null && !_controller!.value.hasError) {
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Stack(
          children: [
            _cameraPreviewWidget(),
            if (_focusPoint != null) _buildFocusIndicator(),
            Positioned(
              top: MediaQuery.of(context).padding.top - 20,
              left: 16,
              right: 16,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Container(
                    decoration: BoxDecoration(
                      color: const Color.fromARGB(241, 0, 0, 0),
                      shape: BoxShape.circle,
                    ),
                    child: IconButton(
                      onPressed: () => context.go('/'),
                      icon: const Icon(Icons.close, color: Colors.white),
                    ),
                  ),
                  _buildFlashButton(),
                ],
              ),
            ),

            Positioned(
              bottom: MediaQuery.of(context).padding.bottom + 16,
              left: 0,
              right: 0,
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Spacer(flex: 2),
                      GestureDetector(
                        onTap: takePicture,
                        child: Container(
                          width: 80,
                          height: 80,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            border: Border.all(color: Colors.white, width: 4),
                          ),
                          child: Container(
                            margin: const EdgeInsets.all(8),
                            decoration: const BoxDecoration(
                              color: Colors.white,
                              shape: BoxShape.circle,
                            ),
                          ),
                        ),
                      ),
                      const Spacer(flex: 1),
                      Padding(
                        padding: const EdgeInsets.only(right: 16),
                        child: _buildZoomButton(
                          '$_currentScale',
                          _currentScale,
                          _currentScale,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _cameraPreviewWidget() {
    final CameraController? cameraController = _controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      return const SizedBox();
    } else {
      return Listener(
        onPointerDown: (_) => _pointers++,
        onPointerUp: (_) => _pointers--,
        child: CameraPreview(
          _controller!,
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              return GestureDetector(
                behavior: HitTestBehavior.opaque,
                onScaleStart: _handleScaleStart,
                onScaleUpdate: _handleScaleUpdate,
                onTapDown: onFocusPoint,
              );
            },
          ),
        ),
      );
    }
  }

  Future<void> onFocusPoint(TapDownDetails details) async {
    if (_controller == null || !_controller!.value.isInitialized) {
      return;
    }

    final Offset tapPosition = details.localPosition;

    setState(() {
      _focusPoint = tapPosition;
    });

    final Size? previewSize = _controller?.value.previewSize;

    if (previewSize == null) return;

    final Offset normalizedPoint = Offset(
      tapPosition.dx / previewSize.width,
      tapPosition.dy / previewSize.height,
    );

    try {
      await _controller!.setFocusPoint(normalizedPoint);
      await _controller!.setFocusMode(FocusMode.auto);
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Widget _buildFocusIndicator() {
    if (_focusPoint == null) {
      return const SizedBox.shrink();
    }

    final animation = Tween<double>(
      begin: 1.0,
      end: 0.0,
    ).animate(_focusController);

    return AnimatedBuilder(
      animation: _focusController,
      builder: (context, child) {
        if (!_focusController.isAnimating && _focusController.value == 0.0) {
          return const SizedBox.shrink();
        }
        return Positioned(
          left: _focusPoint!.dx - 25,
          top: _focusPoint!.dy - 25,
          child: FadeTransition(
            opacity: animation,
            child: Container(
              width: 50,
              height: 50,
              decoration: BoxDecoration(
                border: Border.all(color: Colors.yellow, width: 2),
                borderRadius: BorderRadius.circular(5),
              ),
            ),
          ),
        );
      },
    );
  }

  void _handleScaleStart(ScaleStartDetails details) {
    _baseScale = _currentScale;
  }

  Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
    if (_controller == null) {
      return;
    }

    _currentScale = (_baseScale * details.scale).clamp(
      _minAvailableZoom,
      _maxAvailableZoom,
    );

    await _controller!.setZoomLevel(_currentScale);

    setState(() {});
  }

  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

  void showInSnackBar(String message) {
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  Future<void> _initializeBackCameraController() async {
    final cameras = await availableCameras();
    final backCamera = cameras.firstWhere(
      (c) => c.lensDirection == CameraLensDirection.back,
      orElse: () => cameras.first,
    );

    log('CAMERA INICIADA');

    final CameraController cameraController = CameraController(
      backCamera,
      ResolutionPreset.medium,
      imageFormatGroup: ImageFormatGroup.jpeg,
      enableAudio: false,
    );

    _controller?.dispose();

    _controller = cameraController;

    cameraController.addListener(_cameraListener);

    try {
      await cameraController.initialize();
      await Future.wait(<Future<void>>[
        cameraController.getMaxZoomLevel().then(
          (double value) => _maxAvailableZoom = value,
        ),
        cameraController.getMinZoomLevel().then(
          (double value) => _minAvailableZoom = value,
        ),
      ]);
      await cameraController.setFlashMode(FlashMode.off);
      _currentFlashMode = FlashMode.off;
    } on CameraException catch (e) {
      switch (e.code) {
        case 'CameraAccessDenied':
          showInSnackBar('Acesso de câmera negado.');
          break;
        case 'CameraAccessDeniedWithoutPrompt':
          showInSnackBar('Ative o acesso a câmera nas configurações');
          break;
        default:
          _showCameraException(e);
      }
    }

    if (mounted) {
      setState(() {});
    }
  }

  void onTakePictureButtonPressed() {
    takePicture().then((XFile? file) {
      if (mounted) {
        setState(() {
          arquivoDeImagem = file;
        });
        if (file != null) {
          showInSnackBar('Foto salva em ${file.path}');
        }
      }
    });
  }

  FlashMode _getNextFlashMode(FlashMode current) {
    switch (current) {
      case FlashMode.off:
        return FlashMode.auto;
      case FlashMode.auto:
        return FlashMode.always;
      case FlashMode.always:
        return FlashMode.off;
      case FlashMode.torch:
        return FlashMode.off;
    }
  }

  void onFlashModeButtonPressed() async {
    if (_controller == null || !_controller!.value.isInitialized) {
      return;
    }

    final nextMode = _getNextFlashMode(_currentFlashMode);

    try {
      await _controller!.setFlashMode(nextMode);

      setState(() {
        _currentFlashMode = nextMode;
      });
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  Future<void> setFlashMode(FlashMode mode) async {
    if (_controller == null) {
      return;
    }

    try {
      await _controller!.setFlashMode(mode);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  Widget _buildFlashButton() {
    IconData icon;
    Color color;

    switch (_currentFlashMode) {
      case FlashMode.off:
        icon = Icons.flash_off;
        color = Colors.white;
      case FlashMode.auto:
        icon = Icons.flash_auto;
        color = Colors.lightBlueAccent;
      case FlashMode.always:
        icon = Icons.flash_on;
        color = Colors.yellow;
      case FlashMode.torch:
        icon = Icons.highlight;
        color = Colors.orange;
    }

    return Container(
      decoration: const BoxDecoration(
        color: Color.fromARGB(241, 0, 0, 0),
        shape: BoxShape.circle,
      ),
      child: IconButton(
        onPressed: onFlashModeButtonPressed,
        icon: Icon(icon, color: color),
      ),
    );
  }

  Future<XFile?> takePicture() async {
    final CameraController? cameraController = _controller;
    if (cameraController == null || !cameraController.value.isInitialized) {
      showInSnackBar('Erro: selecione a câmera primeiro.');
      return null;
    }

    if (cameraController.value.isTakingPicture) {
      return null;
    }

    try {
      final XFile file = await cameraController.takePicture();
      log('foto tirada');
      return file;
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
  }

  void _showCameraException(CameraException e) {
    showInSnackBar('Error: ${e.code}\n${e.description}');
  }

  Widget _buildZoomButton(String text, double zoomLevel, double currentZoom) {
    final bool isActive = (currentZoom - zoomLevel).abs() < 0.1;

    return GestureDetector(
      onTap: () => _onZoomChanged(zoomLevel),
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: isActive ? Colors.yellow : Colors.transparent,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Text(
          "${zoomLevel.toStringAsFixed(1)}x",
          style: TextStyle(
            color: isActive ? Colors.black : Colors.white,
            fontWeight: FontWeight.bold,
            fontSize: 16,
          ),
        ),
      ),
    );
  }

  void _onZoomChanged(double value) async {
    if (_controller == null) return;

    final currentScale = value.clamp(_minAvailableZoom, _maxAvailableZoom);

    await _controller!.setZoomLevel(currentScale);

    setState(() {});
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment