blob: 528d1bac7554ca1fc231c18e8ac40ede5b02ee2c [file] [log] [blame]
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera')
..invokeMethod('init');
enum CameraLensDirection { front, back, external }
enum ResolutionPreset { low, medium, high }
String serializeResolutionPreset(ResolutionPreset resolutionPreset) {
switch (resolutionPreset) {
case ResolutionPreset.high:
return 'high';
case ResolutionPreset.medium:
return 'medium';
case ResolutionPreset.low:
return 'low';
}
throw new ArgumentError('Unknown ResolutionPreset value');
}
CameraLensDirection _parseCameraLensDirection(String string) {
switch (string) {
case 'front':
return CameraLensDirection.front;
case 'back':
return CameraLensDirection.back;
case 'external':
return CameraLensDirection.external;
}
throw new ArgumentError('Unknown CameraLensDirection value');
}
/// Completes with a list of available cameras.
///
/// May throw a [CameraException].
Future<List<CameraDescription>> availableCameras() async {
try {
final List<Map<String, String>> cameras =
await _channel.invokeMethod('list');
return cameras.map((Map<String, dynamic> camera) {
return new CameraDescription(
name: camera['name'],
lensDirection: _parseCameraLensDirection(camera['lensFacing']),
);
}).toList();
} on PlatformException catch (e) {
throw new CameraException(e.code, e.message);
}
}
class CameraDescription {
final String name;
final CameraLensDirection lensDirection;
CameraDescription({this.name, this.lensDirection});
@override
bool operator ==(Object o) {
return o is CameraDescription &&
o.name == name &&
o.lensDirection == lensDirection;
}
@override
int get hashCode {
return hashValues(name, lensDirection);
}
@override
String toString() {
return '$runtimeType($name, $lensDirection)';
}
}
class CameraException implements Exception {
String code;
String description;
CameraException(this.code, this.description);
@override
String toString() => '$runtimeType($code, $description)';
}
class CameraPreview extends StatelessWidget {
final CameraController controller;
const CameraPreview(this.controller);
@override
Widget build(BuildContext context) {
return controller.value.initialized
? new Texture(textureId: controller._textureId)
: new Container();
}
}
class CameraValue {
/// True if the camera is on.
final bool isStarted;
/// True after [CameraController.initialize] has completed successfully.
final bool initialized;
final String errorDescription;
/// The size of the preview in pixels.
///
/// Is `null` until initialized is `true`.
final Size previewSize;
const CameraValue(
{this.isStarted,
this.initialized,
this.errorDescription,
this.previewSize});
const CameraValue.uninitialized() : this(isStarted: true, initialized: false);
/// Convenience getter for `previewSize.height / previewSize.width`.
///
/// Can only be called when [initialized] is done.
double get aspectRatio => previewSize.height / previewSize.width;
bool get hasError => errorDescription != null;
CameraValue copyWith({
bool isStarted,
bool initialized,
String errorDescription,
Size previewSize,
}) {
return new CameraValue(
isStarted: isStarted ?? this.isStarted,
initialized: initialized ?? this.initialized,
errorDescription: errorDescription ?? this.errorDescription,
previewSize: previewSize ?? this.previewSize,
);
}
@override
String toString() {
return '$runtimeType('
'started: $isStarted, '
'initialized: $initialized, '
'errorDescription: $errorDescription, '
'previewSize: $previewSize)';
}
}
/// Controls a device camera.
///
/// Use [availableCameras] to get a list of available cameras.
///
/// Before using a [CameraController] a call to [initialize] must complete.
///
/// To show the camera preview on the screen use a [CameraPreview] widget.
class CameraController extends ValueNotifier<CameraValue> {
final CameraDescription description;
final ResolutionPreset resolutionPreset;
int _textureId;
bool _disposed = false;
StreamSubscription<Map<String, dynamic>> _eventSubscription;
Completer<Null> _creatingCompleter;
CameraController(this.description, this.resolutionPreset)
: super(const CameraValue.uninitialized());
/// Initializes the camera on the device.
///
/// Throws a [CameraException] if the initialization fails.
Future<Null> initialize() async {
if (_disposed) {
return;
}
try {
_creatingCompleter = new Completer<Null>();
final Map<String, dynamic> reply = await _channel.invokeMethod(
'create',
<String, dynamic>{
'cameraName': description.name,
'resolutionPreset': serializeResolutionPreset(resolutionPreset),
},
);
_textureId = reply['textureId'];
value = value.copyWith(
initialized: true,
previewSize: new Size(
reply['previewWidth'].toDouble(),
reply['previewHeight'].toDouble(),
),
);
_applyStartStop();
} on PlatformException catch (e) {
value = value.copyWith(errorDescription: e.message);
throw new CameraException(e.code, e.message);
}
_eventSubscription =
new EventChannel('flutter.io/cameraPlugin/cameraEvents$_textureId')
.receiveBroadcastStream()
.listen(_listener);
_creatingCompleter.complete(null);
}
void _listener(dynamic event) {
final Map<String, dynamic> map = event;
if (_disposed) {
return;
}
if (map['eventType'] == 'error') {
value = value.copyWith(errorDescription: event['errorDescription']);
}
}
/// Captures an image and saves it to [path].
///
/// A path can for example be obtained using
/// [path_provider](https://pub.dartlang.org/packages/path_provider).
///
/// Throws a [CameraException] if the capture fails.
Future<Null> capture(String path) async {
if (!value.initialized || _disposed) {
throw new CameraException(
'Uninitialized capture()',
'capture() was called on uninitialized CameraController',
);
}
try {
await _channel.invokeMethod(
'capture',
<String, dynamic>{'textureId': _textureId, 'path': path},
);
} on PlatformException catch (e) {
throw new CameraException(e.code, e.message);
}
}
void _applyStartStop() {
if (value.initialized && !_disposed) {
if (value.isStarted) {
_channel.invokeMethod(
'start',
<String, dynamic>{'textureId': _textureId},
);
} else {
_channel.invokeMethod(
'stop',
<String, dynamic>{'textureId': _textureId},
);
}
}
}
/// Starts the preview.
///
/// If called before [initialize] it will take effect just after
/// initialization is done.
void start() {
value = value.copyWith(isStarted: true);
_applyStartStop();
}
/// Stops the preview.
///
/// If called before [initialize] it will take effect just after
/// initialization is done.
void stop() {
value = value.copyWith(isStarted: false);
_applyStartStop();
}
/// Releases the resources of this camera.
@override
Future<Null> dispose() {
if (_disposed) {
return new Future<Null>.value(null);
}
_disposed = true;
super.dispose();
if (_creatingCompleter == null) {
return new Future<Null>.value(null);
} else {
return _creatingCompleter.future.then((_) async {
await _eventSubscription?.cancel();
await _channel.invokeMethod(
'dispose',
<String, dynamic>{'textureId': _textureId},
);
});
}
}
}