-
Notifications
You must be signed in to change notification settings - Fork 222
Closed
Labels
Description
Steps To Reproduce
- Run the example below with the rev/riv file provided to [email protected] using rive_native: ^0.0.11
- RivePlayer is taken from rive_native example
- Observe that animation does not work as expected (see video for reference)
- Change value e.g. Points
- Observe that changing properties via data binding is lagged by one step e.g. if I change Points from 10 to 20 the value in the animation stays at 10, only after I change it to 30 its value changes to 20 in the animation, and so on; this applies to all properties
- Additionally the values are not interpolated between same way as they are in the Rive editor
Source Code
// ignore_for_file: avoid_print, unused_field
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:rive_native/rive_native.dart' as rive;
class PacingArcPage extends StatefulWidget {
const PacingArcPage({super.key});
static Route<void> get route {
return CupertinoPageRoute<void>(
builder: (_) => const PacingArcPage(),
);
}
@override
State<PacingArcPage> createState() => _PacingArcPageState();
}
class _PacingArcPageState extends State<PacingArcPage> {
bool showGuideLines = false;
/// Total budget in pacepoints
int totalBudget = 15;
/// Current pacepoints
double currentPacePoints = 6.4;
/// Position of the PaceSetter in % of day complete
int paceSetterPosition = 60;
/// Show/hide the start and end times
bool showTimes = true;
/// Show/hide the PaceSetter
bool showPaceSetter = true;
/// Show/hide the budget
bool showBudget = true;
/// Show/hide the current pacepoints and pacepoints arc
bool showPacePoints = true;
final List<String> properties = [];
@override
Widget build(BuildContext context) {
const amendedWidth = 273.0;
const amendedHeight = amendedWidth * 0.606;
return Scaffold(
body: Column(
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
width: amendedWidth,
height: amendedHeight,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
if (showGuideLines)
Container(
height: 24,
width: 254,
decoration: BoxDecoration(
border: Border.all(color: VisibleColors.surfaceSuccess, width: 1),
),
child: Center(child: Text('<- 254px. ->', style: VisibleTextStyle.body5.onDark)),
),
PacePointsDial(
pacePoints: showPacePoints ? currentPacePoints : null,
pacingBudget: showBudget ? totalBudget : null,
paceSetterFraction: showPaceSetter ? paceSetterPosition / 100 : null,
state: PacePointsDialState.currentDay,
paceSetterStartTime: showTimes ? '07:00' : null,
paceSetterEndTime: showTimes ? '21:00' : null,
),
],
),
),
),
),
Expanded(
child: ListView(
children: [
Row(
children: [
ShowingIndicator(
label: 'PP',
isShowing: showPacePoints,
onPressed: () {
setState(() {
showPacePoints = !showPacePoints;
});
},
),
Expanded(
child: SliderWithNumberInside(
label: 'Points',
number: currentPacePoints,
max: 150,
onChanged: (v) {
setState(() {
currentPacePoints = v;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
},
),
),
],
),
Row(
children: [
ShowingIndicator(
label: 'PS',
isShowing: showPaceSetter,
onPressed: () {
// showPaceSetterBool?.value = !showPaceSetterBool!.value;
// paceSetterPositionProp?.value = paceSetterPosition.toDouble();
setState(() {
showPaceSetter = !showPaceSetter;
});
setState(() {});
},
),
Expanded(
child: SliderWithNumberInside(
label: 'PaceSetter',
number: paceSetterPosition.toDouble(),
max: 100,
onChanged: (v) {
setState(() {
paceSetterPosition = v.toInt();
});
setState(() {});
},
),
),
],
),
Row(
children: [
ShowingIndicator(
label: 'BG',
isShowing: showBudget,
onPressed: () {
// budgetBool?.value = !budgetBool!.value;
setState(() {
showBudget = !showBudget;
});
},
),
Expanded(
child: SliderWithNumberInside(
label: 'Budget',
number: totalBudget.toDouble(),
max: 100,
onChanged: (v) {
setState(() {
totalBudget = v.toInt();
});
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
},
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ShowingIndicator(
label: 'Times',
isShowing: showTimes,
onPressed: () {
showTimes = !showTimes;
setState(() {});
},
),
],
),
],
),
),
],
),
),
],
),
);
}
}
enum PacePointsDialState {
currentDay,
pendingScore,
previousDay,
}
class PacePointsDial extends StatefulWidget {
const PacePointsDial({
super.key,
required this.pacePoints,
required this.state,
this.pacingBudget,
this.paceSetterFraction,
this.paceSetterStartTime,
this.paceSetterEndTime,
});
/// Passing null shows two dashes - - instead of PacePoints
final double? pacePoints;
/// Passing this value shows the budget
final int? pacingBudget;
/// Passing this value shows the pace setter
final double? paceSetterFraction;
final String? paceSetterStartTime;
final String? paceSetterEndTime;
final PacePointsDialState state;
@override
State<PacePointsDial> createState() => _PacePointsDialState();
}
class _PacePointsDialState extends State<PacePointsDial> {
late rive.ViewModelInstance vm;
// Public properties
late rive.ViewModelInstanceBoolean? arcBool;
late rive.ViewModelInstanceTrigger? showTrigger;
late rive.ViewModelInstanceTrigger? hideTrigger;
late rive.ViewModelInstanceTrigger? pendingStateTrigger;
late rive.ViewModelInstanceNumber? paceSetterPositionProp;
late rive.ViewModelInstanceTrigger? animationPendingScoreTrigger;
late rive.ViewModelInstanceTrigger? animationPendingToLiveTrigger;
late rive.ViewModelInstanceTrigger? animationIntroTrigger;
late rive.ViewModelInstanceTrigger? animationPreviousDayOutTransition;
late rive.ViewModelInstanceTrigger? animationPreviousDayInTransition;
late rive.ViewModelInstanceBoolean? budgetBool;
late rive.ViewModelInstanceNumber? totalBudgetProp;
late rive.ViewModelInstanceBoolean? showPacepointsBool;
late rive.ViewModelInstanceNumber? pacePointsNumberProp;
late rive.ViewModelInstanceBoolean? showStartEndTimesBool;
late rive.ViewModelInstanceBoolean? showPaceSetterBool;
late rive.ViewModelInstanceString? paceSetterStartTimeProp;
late rive.ViewModelInstanceString? paceSetterEndTimeProp;
late rive.ViewModelInstanceColor? gradientStartColor;
late rive.ViewModelInstanceColor? gradientStopColor;
// Private properties (not meant to be accessed directly)
late rive.ViewModelInstanceNumber? _pacePointsProgressProp;
late rive.ViewModelInstanceTrigger? _pastRedTrigger;
late rive.ViewModelInstanceTrigger? _colorRedTrigger;
late rive.ViewModelInstanceTrigger? _colorGreenTrigger;
late rive.ViewModelInstanceTrigger? _pastGreenTrigger;
late rive.ViewModelInstanceNumber? _endXProp;
late rive.ViewModelInstanceNumber? _startXProp;
late rive.ViewModelInstanceNumber? _arcDelay;
late rive.ViewModelInstanceNumber? _pacerDelay;
final List<String> properties = [];
void _initialState() async {
hideTrigger?.trigger();
// gradientStartColor?.value = VisibleColors.surfaceOnDarkDefault.withSafeOpacity(0);
// gradientStopColor?.value = VisibleColors.surfaceOnDarkDefault.withSafeOpacity(1);
await Future.delayed(1500.ms);
animationIntroTrigger?.trigger();
_updateStartEndTimes();
_updatePacePoints();
_updatePacingBudget();
_updatePaceSetterFraction();
}
void _updatePaceSetterFraction() {
if (widget.paceSetterFraction != null) {
showPaceSetterBool?.value = true;
paceSetterPositionProp?.value = (widget.paceSetterFraction! * 100).clamp(0.0, 100.0);
} else {
showPaceSetterBool?.value = false;
paceSetterPositionProp?.value = 0;
}
}
void _updatePacingBudget() {
if (widget.pacingBudget != null) {
budgetBool?.value = true;
totalBudgetProp?.value = widget.pacingBudget!.toDouble();
} else {
budgetBool?.value = false;
totalBudgetProp?.value = 0;
}
}
void _updatePacePoints() {
if (widget.pacePoints != null) {
showPacepointsBool?.value = true;
pacePointsNumberProp?.value = widget.pacePoints!;
arcBool?.value = true;
} else {
showPacepointsBool?.value = false;
pacePointsNumberProp?.value = 0;
arcBool?.value = false;
}
}
void _updateStartEndTimes() {
if (widget.paceSetterStartTime != null && widget.paceSetterEndTime != null) {
showStartEndTimesBool?.value = true;
paceSetterStartTimeProp?.value = widget.paceSetterStartTime!;
paceSetterEndTimeProp?.value = widget.paceSetterEndTime!;
} else {
showStartEndTimesBool?.value = false;
paceSetterStartTimeProp?.value = '';
paceSetterEndTimeProp?.value = '';
}
}
@override
void didUpdateWidget(covariant PacePointsDial oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.pacePoints != oldWidget.pacePoints) {
_updatePacePoints();
}
if (widget.paceSetterStartTime != oldWidget.paceSetterStartTime ||
widget.paceSetterEndTime != oldWidget.paceSetterEndTime) {
_updateStartEndTimes();
}
if (widget.pacingBudget != oldWidget.pacingBudget) {
_updatePacingBudget();
}
if (widget.paceSetterFraction != oldWidget.paceSetterFraction) {
_updatePaceSetterFraction();
}
}
@override
Widget build(BuildContext context) {
return RivePlayer(
asset: Assets.animations.pacingArc,
autoBind: true,
artboardName: 'main',
fit: rive.Fit.fitWidth,
withViewModelInstance: (viewModelInstance) {
vm = viewModelInstance;
for (final property in viewModelInstance.properties) {
properties.add('${property.name} | ${property.type}');
}
// Public properties
arcBool = vm.boolean('arc');
arcBool?.value = true;
showTrigger = vm.trigger('show');
hideTrigger = vm.trigger('hide');
hideTrigger?.trigger();
pendingStateTrigger = vm.trigger('pendingState');
animationPreviousDayOutTransition = vm.trigger('animationPreviousDayOutTransition');
animationPreviousDayInTransition = vm.trigger('animationPreviousDayInTransition');
animationPendingScoreTrigger = vm.trigger('animationPendingScore');
animationPendingToLiveTrigger = vm.trigger('animationPendingToLive');
animationIntroTrigger = vm.trigger('animationIntro');
paceSetterPositionProp = vm.number('paceSetterPosition');
paceSetterPositionProp?.value = 0;
budgetBool = vm.boolean('budget');
budgetBool?.value = false;
pacePointsNumberProp = vm.number('pacePoints');
pacePointsNumberProp?.value = 0;
showPacepointsBool = vm.boolean('showPacePoints');
showPacepointsBool?.value = false;
totalBudgetProp = vm.number('totalBudget');
totalBudgetProp?.value = 0;
showStartEndTimesBool = vm.boolean('showStartEndTimes');
showPaceSetterBool = vm.boolean('showPaceSetter');
paceSetterEndTimeProp = vm.string('paceSetterEndTime');
paceSetterStartTimeProp = vm.string('paceSetterStartTime');
gradientStartColor = vm.color('gradientStart');
gradientStopColor = vm.color('gradientStop');
// gradientStartColor?.value = VisibleColors.surfaceOnDarkDefault.withSafeOpacity(0);
// gradientStopColor?.value = VisibleColors.surfaceOnDarkDefault.withSafeOpacity(1);
// Private properties
_pacePointsProgressProp = vm.number('_pacePointsProgress');
_pastRedTrigger = vm.trigger('_pastRed');
_colorRedTrigger = vm.trigger('_colorRed');
_colorGreenTrigger = vm.trigger('_colorGreen');
_pastGreenTrigger = vm.trigger('_pastGreen');
_endXProp = vm.number('_endX');
_startXProp = vm.number('_startX');
_arcDelay = vm.number('_arcDelay');
_pacerDelay = vm.number('_pacerDelay');
_initialState();
// Set some defaults
// WidgetsBinding.instance.addPostFrameCallback((_) {
// // _initialState();
// });
},
);
}
}
class SliderWithNumberInside extends StatelessWidget {
const SliderWithNumberInside({
super.key,
required this.label,
required this.number,
required this.max,
this.onChanged,
});
final String label;
final double number;
final double max;
final void Function(double)? onChanged;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LayoutBuilder(
builder: (context, constraints) {
return GestureDetector(
onTapDown: (details) {
// final deta = details.localPosition.dx / constraints.maxWidth;
// final newValue = (deta * max).clamp(0.0, max);
// onChanged?.call(newValue);
HapticFeedback.lightImpact();
},
onHorizontalDragUpdate: (details) {
// HapticFeedback.lightImpact();
// final deta = details.localPosition.dx / constraints.maxWidth;
// final newValue = (deta * max).clamp(0.0, max);
// onChanged?.call(newValue);
},
onHorizontalDragEnd: (details) {
final deta = details.localPosition.dx / constraints.maxWidth;
final newValue = (deta * max).clamp(0.0, max);
onChanged?.call(newValue);
print('Drag end: $newValue');
HapticFeedback.lightImpact();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(
width: double.infinity,
height: 24,
decoration: ShapeDecoration(
color: Colors.grey[300],
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Stack(
children: [
Positioned.fill(
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: (number / max).clamp(0.0, 1.0),
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20),
),
),
),
),
Center(
child: Text(
'$label ${number.toStringAsFixed(1)}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
},
),
);
}
}
class ShowingIndicator extends StatelessWidget {
const ShowingIndicator({
super.key,
required this.label,
required this.isShowing,
this.onPressed,
});
final String label;
final bool isShowing;
final void Function()? onPressed;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isShowing ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$label: ${isShowing ? '✅' : '❌'}',
style: const TextStyle(color: Colors.white),
),
),
);
}
}
/// Visible note: this is a direct copy of the example RivePlayer provided by the package example.
/// https://pub.dev/packages/rive_native#getting-started see example in unpackaged source code.
class RivePlayer extends StatefulWidget {
const RivePlayer({
super.key,
required this.asset,
this.stateMachineName,
this.artboardName,
this.hitTestBehavior = rive.RiveHitTestBehavior.opaque,
this.cursor = MouseCursor.defer,
this.fit = rive.Fit.contain,
this.alignment = Alignment.center,
this.layoutScaleFactor = 1.0,
this.withArtboard,
this.withStateMachine,
this.withViewModelInstance,
this.assetLoader,
this.autoBind = true,
});
final String asset;
final String? stateMachineName;
final String? artboardName;
final rive.RiveHitTestBehavior hitTestBehavior;
final MouseCursor cursor;
final rive.Fit fit;
final Alignment alignment;
final double layoutScaleFactor;
final rive.AssetLoaderCallback? assetLoader;
final bool autoBind;
final void Function(rive.StateMachine stateMachine)? withStateMachine;
final void Function(rive.Artboard artboard)? withArtboard;
final void Function(rive.ViewModelInstance viewModelInstance)? withViewModelInstance;
@override
State<RivePlayer> createState() => _RivePlayerState();
}
class _RivePlayerState extends State<RivePlayer> {
rive.File? riveFile;
late rive.Artboard artboard;
late rive.StateMachinePainter stateMachinePainter;
@override
void initState() {
super.initState();
init();
}
Future<void> init() async {
riveFile = await _loadFile();
if (riveFile == null) return;
if (widget.artboardName != null) {
artboard = riveFile!.artboard(widget.artboardName!)!;
} else {
artboard = riveFile!.artboardAt(0)!;
}
widget.withArtboard?.call(artboard);
stateMachinePainter =
rive.RivePainter.stateMachine(
stateMachineName: widget.stateMachineName,
withStateMachine: (stateMachine) {
widget.withStateMachine?.call(stateMachine);
if (!widget.autoBind) return;
final vm = riveFile!.defaultArtboardViewModel(artboard);
if (vm == null) {
return;
}
final vmi = vm.createDefaultInstance();
if (vmi == null) {
return;
}
stateMachine.bindViewModelInstance(vmi);
widget.withViewModelInstance?.call(vmi);
},
)
..hitTestBehavior = widget.hitTestBehavior
..cursor = widget.cursor
..fit = widget.fit
..alignment = widget.alignment
..layoutScaleFactor = widget.layoutScaleFactor;
setState(() {});
}
Future<rive.File?> _loadFile() async {
final bytes = await rootBundle.load(widget.asset);
return rive.File.decode(
bytes.buffer.asUint8List(),
riveFactory: rive.Factory.rive,
assetLoader: widget.assetLoader,
);
}
@override
void didUpdateWidget(RivePlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.hitTestBehavior != oldWidget.hitTestBehavior) {
stateMachinePainter.hitTestBehavior = widget.hitTestBehavior;
}
if (widget.cursor != oldWidget.cursor) {
stateMachinePainter.cursor = widget.cursor;
}
if (widget.fit != oldWidget.fit) {
stateMachinePainter.fit = widget.fit;
}
if (widget.alignment != oldWidget.alignment) {
stateMachinePainter.alignment = widget.alignment;
}
if (widget.layoutScaleFactor != oldWidget.layoutScaleFactor) {
stateMachinePainter.layoutScaleFactor = widget.layoutScaleFactor;
}
stateMachinePainter.scheduleRepaint();
}
@override
Widget build(BuildContext context) {
return riveFile != null
? rive.RiveArtboardWidget(
artboard: artboard,
painter: stateMachinePainter,
)
: const SizedBox();
}
@override
void dispose() {
artboard.dispose();
stateMachinePainter.dispose();
riveFile?.dispose();
super.dispose();
}
}
Source .riv/.rev file
sent to [email protected]
Expected behavior
screenshot_20250919_140100.mp4
Screenshots
screenshot_20250919_140156.mp4
Device & Versions (please complete the following information)
- Device: Android Samsung S23 Android 15, iOS simulator iOS 26
- OS: Android/iOS
- Flutter Version: 3.35.4