How use Artboard and Nestedartboard from Rive in a Flutter app - flutter

I want to use a rive animation like this one
https://rive.app/community/1514-2958-flower-composition-tutorial/
I notice that this artboard contains a somes NestedArtboard this nested artboard are not loaded by my app, I would like to know why this happen?
This is my code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
class RiveBackground extends StatefulWidget {
const RiveBackground({Key? key}) : super(key: key);
#override
State<RiveBackground> createState() => _RiveBackgroundState();
}
class _RiveBackgroundState extends State<RiveBackground> {
// Declarations necessary to rive
final riveFileName = 'assets/rive/last.riv';
Artboard? globalArtboard;
// Animation controller
late RiveAnimationController _animationController;
// Loads a Rive file
Future<void> _loadRiveFile() async {
final bytes = await rootBundle.load(riveFileName);
RiveFile rFile = RiveFile.import(bytes);
final artboard = rFile.artboardByName('Motion');
print(globalArtboard);
globalArtboard = artboard!
..addController(
_animationController = SimpleAnimation('Animation 1'),
);
setState(() {});
}
#override
void initState() {
WidgetsBinding.instance!.addPostFrameCallback((_) => _loadRiveFile());
super.initState();
}
#override
Widget build(BuildContext context) {
print('Building');
return Scaffold(
body: globalArtboard != null
? Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Rive(
fit: BoxFit.cover,
artboard: globalArtboard!,
),
)
: const Center(child: Text('empty')),
);
}
}
Expected result
My result

Not sure if you found a solution for this already, but according to this response in this Github issue,
to get the nested artboard to show through, you should instance the Artboard when you set it up with the Rive widget, so your setup would go from:
Rive(artboard: riveFile.artboardByName("artboard"));
to
Rive(artboard: riveFile.artboardByName("artboard")!.instance());
more detail in the link. I hope this helps!

Related

Flutter: Modify Hero child between two screens

I have two screens, in the first screen i have an image, let's call original image, when i tap a button, i navigate to the second screen with a hero animation over the image. in the second screen i will cut or paint (draw lines) the original image and then i save this image with the RepaintBoundary Widget. My question is how i can modify the hero child (with original image) in the first screen, so that when i make the pop in the second screen, the Hero animation happens normally with the modified image in both screens.
class Screen1 extends StatefulWidget {
final Uint8List bytes;
const Screen1({Key? key, required this.bytes}) : super(key: key);
#override
State<Screen1> createState() => _Screen1State();
}
class _Screen1State extends State<Screen1> {
late final ValueNotifier<ImageProvider> _imageNotifier; /// With Image()
// late final ValueNotifier<Uint8List> _imageNotifier; /// With Image.memory()
#override
void initState() {
super.initState();
_imageNotifier = ValueNotifier(Image.memory(widget.bytes).image);
// _imageNotifier = ValueNotifier(widget.bytes);
}
#override
void dispose() {
_imageNotifier.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder<ImageProvider>(
valueListenable: _imageNotifier,
builder: (_, image, __) {
return Hero(
tag: 'image',
child: Image(image: image),
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.draw),
onPressed: () => Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (_, __, ___) => Screen2(imageNotifier: _imageNotifier))
),
),
);
}
}
class Screen2 extends StatefulWidget {
final ValueNotifier<ImageProvider> imageNotifier;
const Screen2({Key? key, required this.imageNotifier}) : super(key: key);
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final _key = GlobalKey();
static const _duration = Duration(milliseconds: 100);
Future<void> _takeSnapshot() async {
final boundary = _key.currentContext!.findRenderObject() as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 2.0);
image.toByteData(format: ui.ImageByteFormat.png).then((byteData) {
final imageProvider = Image.memory(byteData!.buffer.asUint8List()).image;
widget.imageNotifier.value = imageProvider;
/// Delay for update first screen
Future.delayed(_duration, () => Navigator.of(context).pop());
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RepaintBoundary(
key: _key,
child: Stack(
children: [
/// ORIGINAL IMAGE
ValueListenableBuilder<ImageProvider>(
valueListenable: widget.imageNotifier,
builder: (_, image, __) {
return Hero(
tag: 'image',
child: Image(image: image)
);
}
),
/// LINES WITH CUSTOM PAINTER
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.camera),
onPressed: _takeSnapshot,
),
);
}
}
The code above shows a minimal example of what i want to achieve, I dont put the code to draw the lines because it is significantly long, but the point is that i take a snapshot of the stack widget and update the notifier.
I don't know if to paint the image it is better to use an Image.memory (Uint8List) or Image (ImageProvider), this will be the type of the ValueNotifier.
The points above are some ideas that i have to achive the target, but I would like to know if there is a better way that you know.
Thanks!

Flutter centralized/common loading screen for entire Application

I am working in Riverpod Auth flow boilerplate application.
I want to use common loading screen for all async function even login and logout. Currently I have AppState provider if Appstate loading i show loading screen. it's working fine for login but i wonder it’s good way or bad way.
Can i use this loading screen for all async task in the App?
AuthWidget:
class AuthWidget extends ConsumerWidget {
const AuthWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
AppState appState = ref.watch(appStateProvider);
if(appState.isLoading){
return const Center(child: CircularProgressIndicator(color: Colors.red),);
}
return appState.isAuthenticated ? const HomePage() : const SignIn();
}
}
AppState:
class AppState {
User? user;
bool isLoading;
bool isAuthenticated;
AppState(this.user, this.isLoading, this.isAuthenticated);
}
AuthRepository:
class AuthRepository extends StateNotifier<AppState>{
AuthRepository() : super(AppState(null,false,false));
Future<void> signIn()async {
state = AppState(null,true,false);
await Future.delayed(const Duration(seconds: 3));
User user = User(userName: 'FakeUser', email: 'user#gmail.com');
AppState appState = AppState(user, false, true);
state = appState;
}
}
final appStateProvider = StateNotifierProvider<AuthRepository,AppState>((ref){
return AuthRepository();
});
To answer your question : Yes you can.
The only thing I'd change here is the content of your AppState : I'd use a LoadingState dedicated to trigger your Loader instead.
Here is how I like to manage screens with a common loader in my apps.
1 - Create a LoadingState and provide it
final loadingStateProvider = ChangeNotifierProvider((ref) => LoadingState());
class LoadingState extends ChangeNotifier {
bool isLoading = false;
void startLoader() {
if (!isLoading) {
isLoading = true;
notifyListeners();
}
}
void stopLoader() {
if (isLoading) {
isLoading = false;
notifyListeners();
}
}
}
2 - Define a base page with the "common" loader
class LoadingContainer extends ConsumerWidget {
const LoadingContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(loadingStateProvider);
return Stack(
children: [
child,
if (state.isLoading)
const Center(child: CircularProgressIndicator())
else
const SizedBox(),
],
);
}
}
3 - Implement this widget whenever I need to handle loading datas.
return Scaffold(
backgroundColor: AppColor.blue,
body: LoadingContainer(
child: ...
And then I simply have to update my loadingStateProvider and it's isLoading value from a Controller or the Widget directly
If you want a centralized/common async calls, the InheritedWidget is ideal for that, you can just add a method and call it from anywhere down stream and because the call is offloaded with async, you can attach extra arguments and add usefull functionality such as a live update instead of relying on stuff like .then(). This example might not be as simple as FDuhen's but you can mix them together if you want to not use keys
AppState now is a widget and contains trigers that rely on global keys to rebuild the correct components, here i assumed that you actualy want to have an common overlay and not a loading screen widget, if not using a Navigator would be batter
Using keys is specially good if you end up implementing something this line, <token> been just a number that references a group of widgets
key: AppState.of(ctx).rebuild_on_triger(<token>)
class App_State_Data {
GlobalKey? page_key;
bool is_logged = false;
bool loading_overlay = false;
String loading_message = '';
}
class AppState extends InheritedWidget {
final App_State_Data _state;
bool get is_logged => _state.is_logged;
bool get should_overlay => _state.loading_overlay;
String get loading_message => _state.loading_message;
void page_rebuild() {
(_state.page_key!.currentState as _Page_Base).rebuild();
}
GlobalKey get page_key {
if (_state.page_key == null) {
_state.page_key = GlobalKey();
}
return _state.page_key!;
}
void place_overlay(String msg) {
_state.loading_message = msg;
_state.loading_overlay = true;
page_rebuild();
}
void clear_overlay() {
_state.loading_message = '';
_state.loading_overlay = false;
page_rebuild();
}
Future<void> triger_login(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 2));
_state.is_logged = true;
clear_overlay();
}
Future<void> triger_logout(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 1));
_state.is_logged = false;
clear_overlay();
}
AppState({Key? key, required Widget child})
: this._state = App_State_Data(),
super(key: key, child: child);
static AppState of(BuildContext ctx) {
final AppState? ret = ctx.dependOnInheritedWidgetOfExactType<AppState>();
assert(ret != null, 'No AppState found!');
return ret!;
}
#override
bool updateShouldNotify(AppState old) => true;
}
Here i added it as the topmost element making it like a global data class with is not necessary, you can split the state content and add just the necessary to where its needed
void main() => runApp(AppState(child: App()));
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: Page_Base(
key: AppState.of(ctx).page_key,
),
),
);
}
}
class Page_Base extends StatefulWidget {
final GlobalKey key;
const Page_Base({
required this.key,
}) : super(key: key);
#override
_Page_Base createState() => _Page_Base();
}
class _Page_Base extends State<Page_Base> {
Widget build_overlay(BuildContext ctx) {
return Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: Color(0xC09E9E9E),
child: Center(
child: Text(AppState.of(ctx).loading_message),
),
),
);
}
#override
Widget build(BuildContext ctx) {
return Stack(
children: [
AppState.of(ctx).is_logged ? Page_Home() : Page_Login(),
AppState.of(ctx).should_overlay ? build_overlay(ctx) : Material(),
],
);
}
void rebuild() {
// setState() is protected and can not be called
// from outside of the this. scope
setState(() => null);
}
}
Using AppState is the best part, just because the widget does not have to call more than 1 function and it will rebuild with the correct data on complition
class Page_Login extends StatelessWidget {
const Page_Login({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_login('Login'),
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
child: Text('Page_Login'),
),
),
);
}
}
class Page_Home extends StatelessWidget {
const Page_Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_logout('Logout'),
child: Container(
width: 200,
height: 200,
color: Colors.blueAccent,
child: Text('Page_Home'),
),
),
);
}
}
Global loading indicator
If you want a centralized loading indicator to use in your whole app you could take advantage of Overlay's, which flutter already uses for dialogs, popups, bottom sheets etc. This way we don't introduce new widget in the widget tree.
If you only want to toggle between loading states you can use a StateProvider to handle the simple boolean value, else you could create a State/Change Notifier. This way you decouple your loading state from your AppState
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(const ProviderScope(child: MaterialApp(home: GlobalLoadingIndicator(child: Home()))));
// This widget should wrap your entire app, but be below MaterialApp in order to have access to the Overlay
class GlobalLoadingIndicator extends ConsumerStatefulWidget {
final Widget child;
const GlobalLoadingIndicator({required this.child, Key? key}) : super(key: key);
#override
ConsumerState createState() => _GlobalLoadingIndicatorState();
}
class _GlobalLoadingIndicatorState extends ConsumerState<GlobalLoadingIndicator> {
//We need to cache the overlay entries we are showing as part of the indicator in order to remove them when the indicator is hidden.
final List<OverlayEntry> _entries = [];
#override
Widget build(BuildContext context) {
ref.listen<bool>(loadingProvider, (previous, next) {
// We just want to make changes if the states are different
if (previous == next) return;
if (next) {
// Add a modal barrier so the user cannot interact with the app while the loading indicator is visible
_entries.add(OverlayEntry(builder: (_) => ModalBarrier(color: Colors.black12.withOpacity(.5))));
_entries.add(OverlayEntry(
builder: (_) =>const Center(
child: Card(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())))));
// Insert the overlay entries into the overlay to actually show the loading indicator
Overlay.of(context)?.insertAll(_entries);
} else {
// Remove the overlay entries from the overlay to hide the loading indicator
_entries.forEach((e) => e.remove());
// Remove the cached overlay entries from the widget state
_entries.clear();
}
});
return widget.child;
}
}
We insert the GlobalLoadingIndicator high up in the widget tree although anywhere below the MaterialApp is fine (as long as it can access the Overlay via context).
The GlobalLoadingIndicator wont create extra widgets in the widget tree, and will only manage the overlays, here I add two overlays, one is a ModalBarrier which the user from interacting with widgets behind itself. And the other the actual LoadingIndicator. You are free to not add the ModalBarrier, or make it dismissible (or even if you decide to create a more complex loadingProvider, customize it in case you need to cater different use cases).
A sample usage after you have this set up is just switching the state of the loadingProvider, most of the times you would do this programatically, but for interactiveness I'll use a Switch :
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
Future.delayed(const Duration(seconds: 4)).then((value) {
ref.read(loadingProvider.notifier).state = false;
});
},
title: const FlutterLogo(),
),
));
}
}
You can fiddle with this snippet in dartpad
Result:
Per Screen/Section loading indicator
As a side note when displaying loading states inside components of the app I recommend you to use an AnimatedSwitcher , as it fades between the widgets , super handy when dealing with screens which can change content abruptly.
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(ProviderScope(child: MaterialApp(home: Home())));
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
},
title: AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isLoading?CircularProgressIndicator():FlutterLogo()
),
),
));
}
}

How to change video on tap with Pod Player in flutter?

I am able to load the video initially but when I setState to change the youtube link on tap it doesn't work, don't know why. what is the approach to changing the video on button press?
I am using the Pod Player plugin.
Just like Load Video Button Functionality in this image
import 'package:flutter/material.dart';
import 'package:pod_player/pod_player.dart';
class VideoPlayer extends StatefulWidget {
const VideoPlayer({Key? key}) : super(key: key);
#override
State<VideoPlayer> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
String YTLink = "https://youtu.be/A3ltMaM6noM";
late final PodPlayerController controller;
#override
void initState() {
controller = PodPlayerController(
playVideoFrom: PlayVideoFrom.youtube(YTLink),
)..initialise();
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListView(
children: [
PodVideoPlayer(controller: controller),
OutlinedButton(
onPressed: () {
setState(() {
YTLink =
"https://www.youtube.com/watch?v=Q98aCklzCBE&ab_channel=DWDocumentary";
});
},
child: Text("Play Another Video")),
OutlinedButton(
onPressed: () {
setState(() {
YTLink =
"https://www.youtube.com/watch?v=AnYsa_c4GxU&ab_channel=FreeDocumentary";
});
},
child: Text("Play Another Video 2")),
],
)),
);
}
}
Instead of using setState, use controllers changeVideo method.
Add this to onPressed of Button:
//1
`controller.changeVideo(
playVideoFrom: PlayVideoFrom.youtube(
"https://www.youtube.com/watch?v=Q98aCklzCBE&ab_channel=DWDocumentary"));`
//2
`controller.changeVideo(
playVideoFrom: PlayVideoFrom.youtube(
"https://www.youtube.com/watch?v=AnYsa_c4GxU&ab_channel=FreeDocumentary"));`

How do I play a Rive Animation in flutter

I am new to Rive and because there is close to no good documentation for Rive 2 I wanted to ask here. How do I play my Rive Animation in flutter? I copy + pasted the example that was on pub.dev for the rive dependecy and switched out their animation name to mine, but it just shows me a weird freezeframe of my animation. This is the code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _togglePlay() {
setState(() => _controller.isActive = !_controller.isActive);
}
/// Tracks if the animation is playing by whether controller is running.
bool get isPlaying => _controller?.isActive ?? false;
Artboard _riveArtboard;
RiveAnimationController _controller;
#override
void initState() {
super.initState();
// Load the animation file from the bundle, note that you could also
// download this. The RiveFile just expects a list of bytes.
rootBundle.load('assets/file.riv').then(
(data) async {
// Load the RiveFile from the binary data.
final file = RiveFile.import(data);
// The artboard is the root of the animation and gets drawn in the
// Rive widget.
final artboard = file.mainArtboard;
// Add a controller to play back a known animation on the main/default
// artboard.We store a reference to it so we can toggle playback.
artboard.addController(_controller = SimpleAnimation('idle'));
setState(() => _riveArtboard = artboard);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _riveArtboard == null
? const SizedBox()
: Rive(artboard: _riveArtboard),
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
tooltip: isPlaying ? 'Pause' : 'Play',
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}
You might need to change the name of the animation you want to play from your Rive file.
This line:
artboard.addController(_controller = SimpleAnimation('idle'));
attempts to play an animation called 'idle'. If you animation is named differently, try replacing the name here.
This blog post has more info on using Rive with Flutter.
final url = Uri.parse(
'https://raw.githubusercontent.com/rive-app/rive-flutter/master/example/assets/liquid_download.riv');
final res = await http.get(url);
final file = RiveFile.import(res.bodyBytes.buffer.asByteData());
// rootBundle.load('assets/file.riv')

Flutter Splash Screen Video Player

I am trying to create a background video splash screen for my app.
Currently, I am achieving a blank screen by running this code.
void main() => runApp(WalkThrough());
class WalkThrough extends StatefulWidget {
#override
_WalkThroughState createState() => _WalkThroughState();
}
class _WalkThroughState extends State<WalkThrough> {
VideoPlayerController _controller;
#override
void initState() {
super.initState();
// Pointing the video controller to our local asset.
_controller = VideoPlayerController.asset('assets/video.mp4')
..initialize().then((_) {
// Once the video has been loaded we play the video and set looping to true.
_controller.play();
_controller.setLooping(true);
_controller.setVolume(0.0);
_controller.play();
// Ensure the first frame is shown after the video is initialized.
setState(() {});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
I suspect the problem may be here and have based my research off this Full screen video background in Flutter on Login as I am trying to achieve a similar result.
SizedBox.expand(
child: FittedBox(
// If your background video doesn't look right, try changing the BoxFit property.
// BoxFit.fill created the look I was going for.
fit: BoxFit.fill,
child: SizedBox(
width: _controller.value.size?.width ?? 0,
height: _controller.value.size?.height ?? 0,
child: VideoPlayer(_controller),
),
),
),
I think the video player package has an issue to show video in the Ios simulator. I had the same issue and search for it and find out this issue in Github. until now this issue is open. I tested the video player on a real device and there were no problems.
Use this video player video_player plugin
& Save your mp4 file in assets/videos location
Try the below code
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import 'HomePage.dart';
class SplashPage extends StatefulWidget {
SplashPage({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> {
VideoPlayerController _controller;
bool _visible = false;
#override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
_controller = VideoPlayerController.asset("assets/video/splash_video.mp4");
_controller.initialize().then((_) {
_controller.setLooping(true);
Timer(Duration(milliseconds: 100), () {
setState(() {
_controller.play();
_visible = true;
});
});
});
Future.delayed(Duration(seconds: 4), () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => HomePage(param_homepage: 'Welcome Home')),
(e) => false);
});
}
#override
void dispose() {
super.dispose();
if (_controller != null) {
_controller.dispose();
_controller = null;
}
}
_getVideoBackground() {
return AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 1000),
child: VideoPlayer(_controller),
);
}
_getBackgroundColor() {
return Container(color: Colors.transparent //.withAlpha(120),
);
}
_getContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: <Widget>[
_getVideoBackground(),
],
),
),
);
}
}