Flutter实现Android原生相机拍照


方法1:使用Flutter的camera插件(完整实现)

1. 完整依赖与权限配置
yaml 复制代码
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  camera: ^0.10.5+2
  path_provider: ^2.0.15 # 用于获取存储路径
  path: ^1.8.3           # 用于路径操作
  permission_handler: ^10.4.0 # 权限处理
2. AndroidManifest.xml 配置
xml 复制代码
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- 在<application>标签内添加 -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
3. 创建文件路径配置 (res/xml/file_paths.xml)
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="my_images" path="Android/data/${applicationId}/files/Pictures" />
</paths>
4. Flutter端完整代码
dart 复制代码
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

class CameraScreen extends StatefulWidget {
  @override
  _CameraScreenState createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  late CameraController _controller;
  late Future<void> _initializeControllerFuture;
  bool _isCameraReady = false;
  String? _lastImagePath;

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

  Future<void> _setupCamera() async {
    // 检查并请求权限
    final cameraStatus = await Permission.camera.status;
    final storageStatus = await Permission.storage.status;
    
    if (!cameraStatus.isGranted || !storageStatus.isGranted) {
      final results = await [
        Permission.camera,
        Permission.storage,
      ].request();
      
      if (!results[Permission.camera]!.isGranted || 
          !results[Permission.storage]!.isGranted) {
        return;
      }
    }

    // 获取可用相机
    final cameras = await availableCameras();
    final firstCamera = cameras.firstWhere(
      (camera) => camera.lensDirection == CameraLensDirection.back,
      orElse: () => cameras.first,
    );

    // 初始化控制器
    _controller = CameraController(
      firstCamera,
      ResolutionPreset.high,
      enableAudio: false,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    _initializeControllerFuture = _controller.initialize().then((_) {
      if (!mounted) return;
      setState(() => _isCameraReady = true);
    });
  }

  Future<String> _takePicture() async {
    if (!_isCameraReady) throw 'Camera not ready';

    final Directory appDir = await getApplicationDocumentsDirectory();
    final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
    final String savePath = join(appDir.path, fileName);

    try {
      final XFile image = await _controller.takePicture();
      final File savedImage = await File(image.path).copy(savePath);
      return savedImage.path;
    } on CameraException catch (e) {
      throw 'Camera error: ${e.description}';
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<void>(
        future: _initializeControllerFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            return Stack(
              children: [
                CameraPreview(_controller),
                Positioned(
                  bottom: 30,
                  left: 0,
                  right: 0,
                  child: FloatingActionButton(
                    onPressed: () async {
                      try {
                        final path = await _takePicture();
                        setState(() => _lastImagePath = path);
                        print('Image saved to: $path');
                      } catch (e) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Error: $e')),
                        );
                      }
                    },
                    child: Icon(Icons.camera),
                  ),
                )
              ],
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}

方法2:通过平台通道调用原生相机(完整实现)

Flutter端完整代码
dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class NativeCameraScreen extends StatefulWidget {
  @override
  _NativeCameraScreenState createState() => _NativeCameraScreenState();
}

class _NativeCameraScreenState extends State<NativeCameraScreen> {
  static const platform = MethodChannel('com.example/camera_channel');
  String? _imagePath;

  Future<void> _takePhoto() async {
    try {
      final String? path = await platform.invokeMethod('takePhoto');
      if (path != null) {
        setState(() => _imagePath = path);
        print('Photo path: $path');
      }
    } on PlatformException catch (e) {
      print("Failed to take photo: '${e.message}'.");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_imagePath != null)
              Image.file(File(_imagePath!), height: 300),
            ElevatedButton(
              onPressed: _takePhoto,
              child: Text('Take Photo'),
            ),
          ],
        ),
      ),
    );
  }
}
Android端完整实现 (Kotlin)
kotlin 复制代码
// MainActivity.kt
package com.example.your_app_name

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example/camera_channel"
    private var pendingResult: MethodChannel.Result? = null
    private var currentPhotoPath: String? = null
    private val REQUEST_IMAGE_CAPTURE = 1
    private val REQUEST_CAMERA_PERMISSION = 2

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            when (call.method) {
                "takePhoto" -> {
                    pendingResult = result
                    checkCameraPermission()
                }
                else -> result.notImplemented()
            }
        }
    }

    private fun checkCameraPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.CAMERA),
                REQUEST_CAMERA_PERMISSION
            )
        } else {
            dispatchTakePictureIntent()
        }
    }

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                val photoFile: File? = try {
                    createImageFile()
                } catch (ex: IOException) {
                    pendingResult?.error("FILE_ERROR", ex.message, null)
                    null
                }
                photoFile?.also {
                    val photoURI: Uri = FileProvider.getUriForFile(
                        this,
                        "${BuildConfig.APPLICATION_ID}.fileprovider",
                        it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
                }
            }
        }
    }

    @Throws(IOException::class)
    private fun createImageFile(): File {
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
        val storageDir: File? = getExternalFilesDir("Pictures")
        return File.createTempFile(
            "JPEG_${timeStamp}_",
            ".jpg",
            storageDir
        ).apply {
            currentPhotoPath = absolutePath
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CAMERA_PERMISSION) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                dispatchTakePictureIntent()
            } else {
                pendingResult?.error("PERMISSION_DENIED", "Camera permission denied", null)
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_IMAGE_CAPTURE) {
            when (resultCode) {
                Activity.RESULT_OK -> {
                    pendingResult?.success(currentPhotoPath)
                }
                Activity.RESULT_CANCELED -> {
                    pendingResult?.error("CANCELLED", "User cancelled photo", null)
                }
                else -> {
                    pendingResult?.error("CAPTURE_FAILED", "Image capture failed", null)
                }
            }
            pendingResult = null
        }
    }
}

关键问题解决方案

1. 文件存储问题(Android 10+适配)
kotlin 复制代码
// 在AndroidManifest.xml中添加
<application
    ...
    android:requestLegacyExternalStorage="true" // 临时解决方案
    >

或使用MediaStore API(推荐):

kotlin 复制代码
private fun saveImageToGallery(context: Context, file: File) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
    }

    val resolver = context.contentResolver
    val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
    
    uri?.let {
        resolver.openOutputStream(it).use { output ->
            FileInputStream(file).use { input ->
                input.copyTo(output!!)
            }
        }
    }
}
2. 相机方向问题

在Flutter端处理相机方向:

dart 复制代码
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.resumed) {
    _controller?.initialize().then((_) {
      if (!mounted) return;
      setState(() {});
    });
  }
}

// 监听设备方向
_controller.setOrientation(orientation);
3. 内存泄漏预防
dart 复制代码
@override
void dispose() {
  _controller?.dispose();
  super.dispose();
}
4. 异常处理最佳实践
dart 复制代码
try {
  // 相机操作
} on CameraException catch (e) {
  if (e.code == 'CameraAccessDenied') {
    // 处理权限问题
  } else {
    // 其他相机错误
  }
} on PlatformException catch (e) {
  // 平台通道错误
} catch (e) {
  // 通用错误
}

两种方法对比

特性 camera插件 平台通道
开发难度 ★☆☆ (简单) ★★★ (复杂)
跨平台支持 需要单独实现iOS
功能控制 中等 完全控制
性能 较好 最优
依赖大小 较大 较小
定制能力 有限 无限
维护成本 低 (官方维护) 高 (需自行维护)

推荐方案选择

  1. 大多数情况 :使用camera插件

    • 官方维护
    • 跨平台支持
    • 减少平台特定代码
  2. 需要高级功能时:使用平台通道

    • 需要特殊相机功能(HDR、手动对焦等)
    • 需要深度集成设备硬件
    • 需要完全控制图像处理流程
  3. 混合方案

    dart 复制代码
    // 使用camera插件获取图像流
    final CameraImage image = await _controller.startImageStream((image) {
      // 处理实时图像数据
    });
    
    // 通过平台通道调用原生高级功能
    final hdrEnabled = await platform.invokeMethod('enableHDR');
相关推荐
huwuhang6 分钟前
索尼PS3游戏合集【中文游戏】8.12T 1430个游戏+PS3模拟器
android·游戏·智能手机·游戏机·电视
Grackers2 小时前
Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
android
踩着两条虫2 小时前
AI驱动的Vue3应用开发平台深入探究(十):物料系统之内置组件库
android·前端·vue.js·人工智能·低代码·系统架构·rxjava
sam.li2 小时前
JADX MCP 原理与使用部署
android·逆向·jadx
冬奇Lab3 小时前
Android 15音频子系统(五):AudioPolicyService策略管理深度解析
android·音视频开发·源码阅读
亚历克斯神3 小时前
Flutter for OpenHarmony: Flutter 三方库 mutex 为鸿蒙异步任务提供可靠的临界资源互斥锁(并发安全基石)
android·数据库·安全·flutter·华为·harmonyos
钛态3 小时前
Flutter 三方库 smartstruct 鸿蒙化字段映射适配指南:介入静态预编译引擎扫除视图及数据模型双向强转类型错乱隐患,筑稳如磐石的企业级模型治理防线-适配鸿蒙 HarmonyOS ohos
flutter·华为·harmonyos
键盘鼓手苏苏3 小时前
Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构
flutter·harmonyos·鸿蒙·openharmony
左手厨刀右手茼蒿3 小时前
Flutter 组件 http_requests 适配鸿蒙 HarmonyOS 实战:极简网络请求,构建边缘端轻量级 RESTful 通讯架构
网络·flutter·http
雷帝木木3 小时前
Flutter 三方库 hrk_logging 的鸿蒙化适配指南 - 实现标准化分层日志记录、支持多目的地输出与日志分级过滤
flutter·harmonyos·鸿蒙·openharmony·hrk_logging