MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter平台特定相机功能:集成iOS与Android的相机API

2021-10-227.4k 阅读

Flutter平台特定相机功能:集成iOS与Android的相机API

Flutter相机功能概述

在移动应用开发中,相机功能是极为常见且重要的一部分。Flutter作为一款跨平台的移动应用开发框架,提供了丰富的工具和插件来实现相机功能。然而,有时开发者需要针对特定平台(如iOS和Android)进行更深入的相机功能定制,这就涉及到集成原生的相机API。

Flutter默认的相机插件(如camera插件)为开发者提供了基础的相机功能,例如拍照、录制视频等。但对于一些特定的需求,像iOS上的深度融合拍摄模式,或者Android上利用特定厂商相机特性(如华为的超级夜景模式),集成原生相机API就显得尤为必要。

集成iOS相机API

了解iOS相机框架

在iOS中,主要使用AVFoundation框架来实现相机功能。AVFoundation提供了捕捉、编辑和播放基于时间的视听媒体的功能。对于相机功能,核心类包括AVCaptureDevice(代表物理相机设备)、AVCaptureInput(用于配置输入源,如相机)、AVCaptureOutput(处理相机输出,如照片或视频)以及AVCaptureSession(管理输入和输出之间的数据流动)。

创建Flutter插件项目

为了集成iOS相机API,我们首先需要创建一个Flutter插件项目。可以使用以下命令在Flutter项目目录下创建插件:

flutter create --template=plugin <plugin_name>

这将生成一个基础的Flutter插件项目结构,其中ios目录包含与iOS相关的代码。

配置iOS项目

  1. 添加权限:在ios/Runner/Info.plist文件中添加相机权限描述,如下:
<key>NSCameraUsageDescription</key>
<string>Your use of the camera is required to use camera functionality.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Your use of the photo library is required to save the photo taken by the camera.</string>
  1. 导入框架:在ios/<plugin_name>/Classes/<plugin_name>-Swift.h文件中导入AVFoundation框架:
#import <AVFoundation/AVFoundation.h>

实现相机功能

  1. 初始化相机:在ios/<plugin_name>/Classes/<plugin_name>-Swift.m文件中,编写初始化相机的代码:
#import "FlutterPluginRegistrar.h"
#import "FlutterMethodChannel.h"
#import <AVFoundation/AVFoundation.h>

@interface CameraPlugin : NSObject<FlutterPlugin>

@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureDeviceInput *videoInput;
@property (nonatomic, strong) AVCaptureStillImageOutput *imageOutput;

@end

@implementation CameraPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    FlutterMethodChannel* channel = [FlutterMethodChannel
                                     methodChannelWithName:@"<channel_name>"
                                     binaryMessenger:[registrar messenger]];
    CameraPlugin* instance = [[CameraPlugin alloc] init];
    [registrar addMethodCallDelegate:instance channel:channel];
}

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([@"initializeCamera" isEqualToString:call.method]) {
        [self initializeCamera:result];
    } else {
        result(FlutterMethodNotImplemented);
    }
}

- (void)initializeCamera:(FlutterResult)result {
    self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = AVCaptureSessionPresetPhoto;

    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error;
    self.videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    if (self.videoInput) {
        if ([self.captureSession canAddInput:self.videoInput]) {
            [self.captureSession addInput:self.videoInput];
        } else {
            result([FlutterError errorWithCode:@"CAMERA_ERROR" message:@"Can't add video input" details:nil]);
            return;
        }
    } else {
        result([FlutterError errorWithCode:@"CAMERA_ERROR" message:@"Can't create video input" details:error.localizedDescription]);
        return;
    }

    self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
    NSDictionary *outputSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
    [self.imageOutput setOutputSettings:outputSettings];
    if ([self.captureSession canAddOutput:self.imageOutput]) {
        [self.captureSession addOutput:self.imageOutput];
    } else {
        result([FlutterError errorWithCode:@"CAMERA_ERROR" message:@"Can't add image output" details:nil]);
        return;
    }

    [self.captureSession startRunning];
    result(nil);
}

@end
  1. 拍照:在handleMethodCall方法中添加拍照逻辑:
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([@"takePicture" isEqualToString:call.method]) {
        [self takePicture:result];
    } else if ([@"initializeCamera" isEqualToString:call.method]) {
        [self initializeCamera:result];
    } else {
        result(FlutterMethodNotImplemented);
    }
}

- (void)takePicture:(FlutterResult)result {
    AVCaptureConnection *videoConnection = nil;
    for (AVCaptureConnection *connection in self.imageOutput.connections) {
        for (AVCaptureInputPort *port in connection.inputPorts) {
            if ([[port mediaType] isEqual:AVMediaTypeVideo] ) {
                videoConnection = connection;
                break;
            }
        }
        if (videoConnection) { break; }
    }

    [self.imageOutput captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler: ^(CMSampleBufferRef imageSampleBuffer, NSError *error) {
        if (imageSampleBuffer != NULL) {
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];
            UIImage *image = [[UIImage alloc] initWithData:imageData];
            NSData *pngData = UIImagePNGRepresentation(image);
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirectory = [paths objectAtIndex:0];
            NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:@"captured_image.png"];
            [pngData writeToFile:imagePath atomically:YES];
            result(imagePath);
        } else {
            result([FlutterError errorWithCode:@"CAMERA_ERROR" message:@"Failed to capture image" details:error.localizedDescription]);
        }
    }];
}

集成Android相机API

了解Android相机框架

在Android中,从Android 5.0(API级别21)开始,推荐使用CameraX库来实现相机功能。CameraX是一个Jetpack库,它提供了一个基于用例的相机API,简化了相机开发。其核心组件包括CameraProvider(用于管理相机设备的连接和生命周期)、Preview(用于显示相机预览)、ImageCapture(用于拍照)和VideoCapture(用于录制视频)。

配置Android项目

  1. 添加权限:在android/app/src/main/AndroidManifest.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" />
  1. 添加依赖:在android/app/build.gradle文件中添加CameraX依赖:
implementation "androidx.camera:camera-core:1.2.0"
implementation "androidx.camera:camera-camera2:1.2.0"
implementation "androidx.camera:camera-lifecycle:1.2.0"
implementation "androidx.camera:camera-view:1.2.0"
implementation "androidx.camera:camera-extensions:1.2.0"

实现相机功能

  1. 初始化相机:在android/src/main/kotlin/<package_name>/<plugin_name>.kt文件中,编写初始化相机的代码:
package <package_name>

import android.content.Context
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class <plugin_name> : FlutterPlugin, MethodCallHandler, ActivityAware {
    private lateinit var channel: MethodChannel
    private lateinit var context: Context
    private lateinit var cameraExecutor: ExecutorService
    private lateinit var imageCapture: ImageCapture

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(binding.binaryMessenger, "<channel_name>")
        channel.setMethodCallHandler(this)
        context = binding.applicationContext
        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        if (call.method == "initializeCamera") {
            initializeCamera(result)
        } else if (call.method == "takePicture") {
            takePicture(result)
        } else {
            result.notImplemented()
        }
    }

    private fun initializeCamera(result: Result) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
              .build()
              .also {
                    it.setSurfaceProvider(/* surfaceView.surfaceProvider */)
                }
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            imageCapture = ImageCapture.Builder()
              .build()
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    /* lifecycleOwner */,
                    cameraSelector,
                    preview,
                    imageCapture
                )
                result.success(null)
            } catch (exc: Exception) {
                result.error("CAMERA_ERROR", "Failed to initialize camera", exc.message)
            }
        }, ContextCompat.getMainExecutor(context))
    }

    private fun takePicture(result: Result) {
        val photoFile = File(
            context.externalMediaDirs.firstOrNull()?.absolutePath + File.separator +
                    SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis()) + ".jpg"
        )
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(context),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    result.error("CAMERA_ERROR", "Failed to take picture", exc.message)
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val savedUri = output.savedUri
                    result.success(savedUri?.path)
                }
            }
        )
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
        cameraExecutor.shutdown()
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {}

    override fun onDetachedFromActivityForConfigChanges() {}

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}

    override fun onDetachedFromActivity() {}
}

在Flutter中调用原生相机功能

完成iOS和Android原生相机功能的实现后,需要在Flutter端调用这些功能。在Flutter项目的lib目录下,创建一个与插件交互的服务类。

import 'package:flutter/services.dart';

class CameraService {
  static const MethodChannel _channel =
      MethodChannel('<channel_name>');

  static Future<void> initializeCamera() async {
    try {
      await _channel.invokeMethod('initializeCamera');
    } on PlatformException catch (e) {
      print('Error initializing camera: ${e.message}');
    }
  }

  static Future<String?> takePicture() async {
    try {
      return await _channel.invokeMethod('takePicture');
    } on PlatformException catch (e) {
      print('Error taking picture: ${e.message}');
      return null;
    }
  }
}

在需要使用相机功能的页面中,可以这样调用:

import 'package:flutter/material.dart';
import 'package:your_plugin_package/camera_service.dart';

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

  @override
  _CameraPageState createState() => _CameraPageState();
}

class _CameraPageState extends State<CameraPage> {
  String? _imagePath;

  @override
  void initState() {
    super.initState();
    CameraService.initializeCamera();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Camera Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_imagePath != null)
              Image.file(File(_imagePath!)),
            ElevatedButton(
              onPressed: () async {
                String? path = await CameraService.takePicture();
                setState(() {
                  _imagePath = path;
                });
              },
              child: const Text('Take Picture'),
            ),
          ],
        ),
      ),
    );
  }
}

优化与注意事项

权限管理优化

在iOS和Android中,权限管理至关重要。除了在配置文件中声明权限,还应在运行时检查权限,并根据权限状态做出相应处理。例如,在iOS中可以使用AVCaptureDeviceauthorizationStatus(for:)方法检查相机权限,在Android中可以使用ContextCompat.checkSelfPermission方法。如果权限未授予,应引导用户到设置页面手动授予权限。

性能优化

  1. iOS:在使用AVFoundation时,合理设置AVCaptureSessionsessionPreset,根据需求选择合适的分辨率和帧率,以平衡性能和图像质量。例如,AVCaptureSessionPresetPhoto适用于高质量拍照,而AVCaptureSessionPresetHigh适用于一般视频拍摄。
  2. Android:在CameraX中,避免频繁创建和销毁相机资源。可以通过合理管理ProcessCameraProvider的生命周期来优化性能。例如,在页面销毁时正确解绑相机资源,而不是每次都重新初始化。

兼容性处理

  1. iOS:不同版本的iOS系统可能对相机功能有不同的支持。例如,某些高级相机功能(如深度融合)可能仅在较新的iOS版本中可用。在代码中应进行版本检查,并根据系统版本提供相应的功能。
  2. Android:由于Android设备的碎片化,不同厂商的设备可能对相机功能有不同的实现。在使用CameraX时,应测试在多种设备上的兼容性。对于特定厂商的相机特性,可以使用CameraX的扩展功能进行适配。

通过以上步骤,开发者可以在Flutter应用中集成iOS和Android原生相机API,实现更丰富、更定制化的相机功能,提升用户体验。同时,在开发过程中注意优化和兼容性处理,确保应用在各种设备和系统版本上稳定运行。