This commit is contained in:
2025-04-04 17:04:55 +02:00
commit 3f5f894e86
16 changed files with 753 additions and 0 deletions

129
lib/src/ambient_widget.dart Normal file
View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:wear_plus/src/wear.dart';
/// Ambient modes for a Wear device
enum WearMode {
/// The screen is active
active,
/// The screen is in ambient mode
ambient,
}
/// Builds a child for [AmbientMode]
typedef AmbientModeWidgetBuilder = Widget Function(
BuildContext context,
WearMode mode,
Widget? child,
);
/// Widget that listens for when a Wear device enters full power or ambient mode,
/// and provides this in a builder. It optionally takes an [onUpdate] function that's
/// called every time the wear device triggers an ambient update request.
@immutable
class AmbientMode extends StatefulWidget {
/// Constructor
const AmbientMode({
super.key,
required this.builder,
this.child,
this.onUpdate,
});
/// Built when the mode changes
final AmbientModeWidgetBuilder builder;
/// Optional child that will not get rebuilt when the mode changes
final Widget? child;
/// Called each time the the wear device triggers an ambient update request.
final VoidCallback? onUpdate;
/// Get current [WearMode].
static WearMode wearModeOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedAmbientMode>()!
.mode;
}
/// Get current [AmbientDetails].
static AmbientDetails ambientDetailsOf(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<_InheritedAmbientMode>()!
.details;
}
@override
State<StatefulWidget> createState() => _AmbientModeState();
}
class _AmbientModeState extends State<AmbientMode> with AmbientCallback {
var _ambientMode = WearMode.active;
final _ambientDetails = const AmbientDetails(false, false);
@override
void initState() {
super.initState();
Wear.instance.registerAmbientCallback(this);
_initState();
}
void _initState() async {
final isAmbient = await Wear.instance.isAmbient();
_updateMode(isAmbient);
}
@override
void dispose() {
Wear.instance.unregisterAmbientCallback(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return _InheritedAmbientMode(
mode: _ambientMode,
details: _ambientDetails,
child: Builder(
builder: (context) {
return widget.builder(context, _ambientMode, widget.child);
},
),
);
}
void _updateMode(bool isAmbient) {
if (!mounted) return;
setState(
() => _ambientMode = isAmbient ? WearMode.ambient : WearMode.active,
);
}
@override
void onEnterAmbient(AmbientDetails ambientDetails) => _updateMode(true);
@override
void onExitAmbient() => _updateMode(false);
@override
void onUpdateAmbient() {
_updateMode(true);
widget.onUpdate?.call();
}
}
class _InheritedAmbientMode extends InheritedWidget {
const _InheritedAmbientMode({
required this.mode,
required this.details,
required super.child,
});
final WearMode mode;
final AmbientDetails details;
@override
bool updateShouldNotify(_InheritedAmbientMode old) {
return mode != old.mode || details != old.details;
}
}

94
lib/src/shape_widget.dart Normal file
View File

@@ -0,0 +1,94 @@
import 'package:flutter/widgets.dart';
import 'package:wear_plus/src/wear.dart';
/// Shape of a Wear device
enum WearShape {
/// The display is square
square,
/// The display is round
round,
}
/// Builds a child for a [WatchShape]
typedef WatchShapeBuilder = Widget Function(
BuildContext context,
WearShape shape,
Widget? child,
);
/// Builder widget for watch shapes
@immutable
class WatchShape extends StatefulWidget {
/// Constructor
const WatchShape({
super.key,
required this.builder,
this.child,
});
/// Built when the shape changes
final WatchShapeBuilder builder;
/// Optional child that will not get rebuilt when the shape changes
final Widget? child;
/// Call [WatchShape.of(context)] to retrieve the shape further down
/// in the widget hierarchy.
static WearShape of(BuildContext context) {
return _InheritedShape.of(context).shape;
}
@override
State<StatefulWidget> createState() => _WatchShapeState();
}
class _WatchShapeState extends State<WatchShape> {
// Default to round until the platform returns the shape
// round being the most common form factor for WearOS
var _shape = WearShape.round;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
final shape = await Wear.instance.getShape();
if (!mounted) return;
setState(
() => _shape = (shape == 'round' ? WearShape.round : WearShape.square),
);
}
@override
Widget build(BuildContext context) {
return _InheritedShape(
shape: _shape,
child: Builder(
builder: (context) {
return widget.builder(context, _shape, widget.child);
},
),
);
}
}
class _InheritedShape extends InheritedWidget {
/// Constructor
const _InheritedShape({
required this.shape,
required super.child,
});
final WearShape shape;
static _InheritedShape of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_InheritedShape>()!;
}
@override
bool updateShouldNotify(_InheritedShape oldWidget) =>
shape != oldWidget.shape;
}

152
lib/src/wear.dart Normal file
View File

@@ -0,0 +1,152 @@
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter/services.dart'
show MethodChannel, MethodCall, PlatformException;
/// Provides access to Wearable features
class Wear {
static const _channel = MethodChannel('wear');
/// Get the [Wear] instance
factory Wear() => instance;
/// Access to the singleton instance
static final instance = Wear._();
Wear._() {
_channel.setMethodCallHandler(_onMethodCallHandler);
}
final _ambientCallbacks = <AmbientCallback>[];
/// Register callback for ambient notifications
void registerAmbientCallback(AmbientCallback callback) {
_ambientCallbacks.add(callback);
}
/// Unregister callback for ambient notifications
void unregisterAmbientCallback(AmbientCallback callback) {
_ambientCallbacks.remove(callback);
}
Future<dynamic> _onMethodCallHandler(MethodCall call) async {
switch (call.method) {
case 'onEnterAmbient':
final args = (call.arguments as Map).cast<String, bool>();
final details =
AmbientDetails(args['burnInProtection']!, args['lowBitAmbient']!);
_notifyAmbientCallbacks((callback) => callback.onEnterAmbient(details));
case 'onExitAmbient':
_notifyAmbientCallbacks((callback) => callback.onExitAmbient());
case 'onUpdateAmbient':
_notifyAmbientCallbacks((callback) => callback.onUpdateAmbient());
case 'onInvalidateAmbientOffload':
_notifyAmbientCallbacks(
(callback) => callback.onInvalidateAmbientOffload(),
);
}
}
void _notifyAmbientCallbacks(Function(AmbientCallback callback) fn) {
final callbacks = List<AmbientCallback>.from(_ambientCallbacks);
for (final callback in callbacks) {
try {
fn(callback);
} catch (e, st) {
debugPrint('Failed callback: $callback\n$e\n$st');
}
}
}
/// Fetches the shape of the watch face
Future<String> getShape() async {
try {
return (await _channel.invokeMethod<String>('getShape'))!;
} on PlatformException catch (e, st) {
// Default to round
debugPrint('Error calling getShape: $e\n$st');
return 'round';
}
}
/// Tells the application if we are currently in ambient mode
Future<bool> isAmbient() async {
try {
return (await _channel.invokeMethod<bool>('isAmbient'))!;
} on PlatformException catch (e, st) {
debugPrint('Error calling isAmbient: $e\n$st');
return false;
}
}
/// Sets whether this activity's task should be moved to the front when
/// the system exits ambient mode.
///
/// If true, the activity's task may be moved to the front if it was the
/// last activity to be running when ambient started, depending on how
/// much time the system spent in ambient mode.
///
Future<void> setAutoResumeEnabled(bool enabled) async {
try {
await _channel.invokeMethod<String>(
'setAutoResumeEnabled',
{'enabled': enabled},
);
} on PlatformException catch (e, st) {
debugPrint('Error calling setAutoResumeEnabled: $e\n$st');
rethrow;
}
}
/// Sets whether this activity is currently in a state that supports ambient offload mode.
Future<void> setAmbientOffloadEnabled(bool enabled) async {
try {
await _channel.invokeMethod<String>(
'setAmbientOffloadEnabled',
{'enabled': enabled},
);
} on PlatformException catch (e, st) {
debugPrint('Error calling setAmbientOffloadEnabled: $e\n$st');
rethrow;
}
}
}
/// Provides details of current ambient mode configuration.
class AmbientDetails {
/// Constructor
const AmbientDetails(this.burnInProtection, this.lowBitAmbient);
/// Used to indicate whether burn-in protection is required.
///
/// When this property is set to true, views must be shifted around
/// periodically in ambient mode. To ensure that content isn't
/// shifted off the screen, avoid placing content within 10 pixels
/// of the edge of the screen. Activities should also avoid solid
/// white areas to prevent pixel burn-in. Both of these
/// requirements only apply in ambient mode, and only when
/// this property is set to true.
final bool burnInProtection;
/// Used to indicate whether the device has low-bit ambient mode.
///
/// When this property is set to true, the screen supports fewer bits
/// for each color in ambient mode. In this case, activities should
/// disable anti-aliasing in ambient mode.
final bool lowBitAmbient;
}
/// Callback to receive ambient mode state changes.
mixin AmbientCallback {
/// Called when an activity is entering ambient mode.
void onEnterAmbient(AmbientDetails ambientDetails) {}
/// Called when an activity should exit ambient mode.
void onExitAmbient() {}
/// Called when the system is updating the display for ambient mode.
void onUpdateAmbient() {}
/// Called to inform an activity that whatever decomposition it has sent to
/// Sidekick is no longer valid and should be re-sent before enabling ambient offload.
void onInvalidateAmbientOffload() {}
}

3
lib/wear_plus.dart Normal file
View File

@@ -0,0 +1,3 @@
export 'src/ambient_widget.dart';
export 'src/shape_widget.dart';
export 'src/wear.dart';