Flutter动画渲染插件rive_bloc的使用
Flutter动画渲染插件rive_bloc的使用
RiveBloc #
RiveBloc 是基于 Flutter BLoC 库构建的编程接口层,深受 Riverpod 启发,使得使用 BLoC 更加简洁且省时。
快速开始 🚀 #
RiveBloc 的主要构建块包括 providers
和 builders
。
Provider
是一种获取状态(Object
值或 Bloc
/Cubit
实例)的方式,而 Builder
则是一种在小部件树中使用该状态的方式。
RiveBlocProvider
是创建提供者的主入口点。
重要提示:为了让提供者正常工作,你需要在 Flutter 应用程序的根部添加 RiveBlocScope
,如下所示:
void main() {
runApp(RiveBlocScope(child: MyApp()));
}
提供者解决了以下问题:
- 提供者具有全局变量的灵活性,但没有它们的缺点。它们可以从任何地方访问,同时确保可测试性和可扩展性。
- 提供者是安全的。不可能在一个未初始化的状态下读取值。
- 提供者可以通过一行代码访问。
RiveBloc 提供了 5 个提供者,分为两大类:
- 1 个
Final
提供者用于暴露最终的 DartObject
值。 - 4 个
RiveBloc
提供者用于暴露Bloc
/Cubit
动态state
值。
FinalProvider
使用 RiveBlocProvider.finalValue
创建,而其他提供者则使用 RiveBlocProvider.value
、RiveBlocProvider.state
、RiveBlocProvider.async
和 RiveBlocProvider.stream
创建。
提供者有许多变体,但它们的工作方式都相同。
最常见的用法是在声明为全局变量时使用,如下所示:
final myProvider = RiveBlocProvider.finalValue(() => MyValue());
final myBlocProvider = RiveBlocProvider.state(() => MyBloc());
其次,所有提供者都应该通过 RiveBlocScope
小部件声明(不是强制性的,但强烈推荐),以便它们可以肯定地从应用程序的任何地方访问:
RiveBlocScope(
providers: [
myProvider,
myBlocProvider,
...
],
child: MyApp(),
);
重要提示:多个提供者不能共享相同的值类型!
// 这不起作用:
final value1 = RiveBlocProvider.value(() => MyValueCubit(1));
final value2 = RiveBlocProvider.value(() => MyValueCubit(2));
// 而应该这样做:
final value1 = RiveBlocProvider.value(() => MyValueCubit1());
final value2 = RiveBlocProvider.value(() => MyValueCubit2());
一旦声明了提供者,你就可以通过使用 read
和 watch
方法来访问它们所暴露的值和实例:
RiveBlocBuilder(
builder: (context, ref) {
final myValue = ref.read(myProvider);
final myBloc = ref.watch(myBlocProvider);
},
);
myValue
是一个MyValueCubit
实例。myBloc
是一个MyBloc
实例。
示例代码
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:rive_bloc/rive_bloc.dart';
import 'todo.dart';
/// Some keys used for testing
final addTodoKey = UniqueKey();
final activeFilterKey = UniqueKey();
final completedFilterKey = UniqueKey();
final allFilterKey = UniqueKey();
/// Creates a [TodoListCubit] and initialise it with pre-defined values.
///
/// We are using [StateProvider] here as a `List<Todo>` is a complex
/// object, with advanced business logic like how to edit a todo.
final todoListProvider = RiveBlocProvider.state(TodoListCubit.new);
/// The different ways to filter the list of todos
enum TodoListFilter {
all,
active,
completed,
}
/// The currently active filter.
///
/// We use [StateProvider] here because we need to be able to update the
/// cubit's state from the UI.
final todoListFilter = RiveBlocProvider.state(() => ValueCubit(TodoListFilter.all));
/// The number of uncompleted todos
///
/// By using [ValueProvider], this value is cached, making it performant.
/// Even multiple widgets try to read the number of uncompleted todos,
/// the value will be computed only once (until the todo-list changes).
///
/// This will also optimise unneeded rebuilds if the todo-list changes, but the
/// number of uncompleted todos doesn't (such as when editing a todo).
final uncompletedTodosCount = RiveBlocProvider.value(() => ValueCubit<int>(
0,
build: (ref, args) {
return ref
.watch(todoListProvider)
.state
.where((todo) => !todo.completed)
.length;
},
));
/// The list of todos after applying of [todoListFilter].
///
/// This too uses [ValueProvider], to avoid recomputing the filtered
/// list unless either the filter of or the todo-list updates.
final filteredTodos = RiveBlocProvider.value<ValueCubit<List<Todo>>, List<Todo>>(
() => ValueCubit([], build: (ref, args) {
final filter = ref.watch(todoListFilter).state;
final todos = ref.watch(todoListProvider).state;
switch (filter) {
case TodoListFilter.completed:
return todos.where((todo) => todo.completed).toList();
case TodoListFilter.active:
return todos.where((todo) => !todo.completed).toList();
case TodoListFilter.all:
return todos;
}
}),
);
class SimpleBlocObserver extends BlocObserver {
[@override](/user/override)
void onEvent(Bloc bloc, Object? event) {
print('<EVENT> [${bloc.runtimeType}] $event');
super.onEvent(bloc, event);
print(event);
}
[@override](/user/override)
void onChange(BlocBase bloc, Change change) {
print('<CHANGE> [${bloc.runtimeType}] $change');
super.onChange(bloc, change);
}
[@override](/user/override)
void onCreate(BlocBase bloc) {
print('<CREATE> $bloc');
super.onCreate(bloc);
}
[@override](/user/override)
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('<ERROR> [${bloc.runtimeType}] $error, $stackTrace');
super.onError(bloc, error, stackTrace);
}
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = SimpleBlocObserver();
runApp(RiveBlocScope(
providers: [
todoListProvider,
todoListFilter,
uncompletedTodosCount,
filteredTodos,
],
child: const MyApp(),
));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return const MaterialApp(
home: Home(),
);
}
}
class Home extends HookWidget {
const Home({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
final newTodoController = useTextEditingController();
return RiveBlocBuilder(builder: (context, ref, _) {
final todos = ref.watch(filteredTodos);
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
children: [
const Title(),
TextField(
key: addTodoKey,
controller: newTodoController,
decoration: const InputDecoration(
labelText: 'What needs to be done?',
),
onSubmitted: (value) {
ref.read(todoListProvider).add(value);
newTodoController.clear();
},
),
const SizedBox(height: 42),
const Toolbar(),
if (todos.isNotEmpty) const Divider(height: 0),
for (var i = 0; i < todos.length; i++) ...[
if (i > 0) const Divider(height: 0),
Dismissible(
key: ValueKey(todos[i].id),
onDismissed: (_) {
ref.read(todoListProvider).remove(todos[i]);
},
child: TodoItem(todos[i]),
),
],
],
),
),
);
});
}
}
class Toolbar extends HookWidget {
const Toolbar({
super.key,
});
[@override](/user/override)
Widget build(BuildContext context) {
return RiveBlocBuilder(builder: (context, ref, _) {
final filter = ref.watch(todoListFilter).state;
Color? textColorFor(TodoListFilter value) {
return filter == value ? Colors.blue : Colors.black;
}
return Material(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${ref.watch(uncompletedTodosCount)} items left',
overflow: TextOverflow.ellipsis,
),
),
Tooltip(
key: allFilterKey,
message: 'All todos',
child: TextButton(
onPressed: () =>
ref.read(todoListFilter).state = TodoListFilter.all,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
foregroundColor: MaterialStateProperty.all(
textColorFor(TodoListFilter.all)),
),
child: const Text('All'),
),
),
Tooltip(
key: activeFilterKey,
message: 'Only uncompleted todos',
child: TextButton(
onPressed: () =>
ref.read(todoListFilter).state = TodoListFilter.active,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
foregroundColor: MaterialStateProperty.all(
textColorFor(TodoListFilter.active),
),
),
child: const Text('Active'),
),
),
Tooltip(
key: completedFilterKey,
message: 'Only completed todos',
child: TextButton(
onPressed: () =>
ref.read(todoListFilter).state = TodoListFilter.completed,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
foregroundColor: MaterialStateProperty.all(
textColorFor(TodoListFilter.completed),
),
),
child: const Text('Completed'),
),
),
],
),
);
});
}
}
class Title extends StatelessWidget {
const Title({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return const Text(
'todos',
textAlign: TextAlign.center,
style: TextStyle(
color: Color.fromARGB(38, 47, 47, 247),
fontSize: 100,
fontWeight: FontWeight.w100,
fontFamily: 'Helvetica Neue',
),
);
}
}
/// The widget that that displays the components of an individual Todo Item
class TodoItem extends HookWidget {
const TodoItem(this._todo, {super.key});
// ignore: prefer_typing_uninitialized_variables
final _todo;
[@override](/user/override)
Widget build(BuildContext context) {
final itemFocusNode = useFocusNode();
final itemIsFocused = useIsFocused(itemFocusNode);
final textEditingController = useTextEditingController();
final textFieldFocusNode = useFocusNode();
return RiveBlocBuilder(builder: (context, ref, _) {
// final todo = ref.watch(_currentTodo).state;
return Material(
color: Colors.white,
elevation: 6,
child: Focus(
focusNode: itemFocusNode,
onFocusChange: (focused) {
if (focused) {
textEditingController.text = _todo.description;
} else {
// Commit changes only when the textfield is unfocused, for performance
ref
.read(todoListProvider)
.edit(id: _todo.id, description: textEditingController.text);
}
},
child: ListTile(
onTap: () {
itemFocusNode.requestFocus();
textFieldFocusNode.requestFocus();
},
leading: Checkbox(
value: _todo.completed,
onChanged: (value) => ref.read(todoListProvider).toggle(_todo.id),
),
title: itemIsFocused
? TextField(
autofocus: true,
focusNode: textFieldFocusNode,
controller: textEditingController,
)
: Text(_todo.description),
),
),
);
});
}
}
bool useIsFocused(FocusNode node) {
final isFocused = useState(node.hasFocus);
useEffect(
() {
void listener() {
isFocused.value = node.hasFocus;
}
node.addListener(listener);
return () => node.removeListener(listener);
},
[node],
);
return isFocused.value;
}
更多关于Flutter动画渲染插件rive_bloc的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter动画渲染插件rive_bloc的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何在Flutter应用中使用rive_bloc
插件进行动画渲染的代码案例。rive_bloc
结合了rive
和bloc
库,允许你以声明式的方式管理和渲染Rive动画。
首先,确保你已经在pubspec.yaml
文件中添加了必要的依赖项:
dependencies:
flutter:
sdk: flutter
rive: ^0.8.0 # 请检查最新版本
flutter_bloc: ^8.0.0 # 请检查最新版本
rive_bloc: ^0.1.0 # 假设存在这样的库,实际使用时请检查最新版本
然后,运行flutter pub get
来获取这些依赖项。
1. 创建Rive动画文件
首先,你需要在Rive编辑器中创建一个动画,并导出为.riv
或.flr
文件。假设你已经有了这个文件,并且文件名为animation.riv
。
2. 导入Rive动画文件
在你的Flutter项目中,将导出的Rive动画文件放置在assets
文件夹中,并在pubspec.yaml
中声明它:
flutter:
assets:
- assets/animation.riv
3. 定义Rive动画的Bloc
创建一个新的Bloc来管理Rive动画的状态。假设你已经有了一个Bloc框架的基本设置,这里只展示与Rive动画相关的部分。
import 'package:bloc/bloc.dart';
import 'package:rive/rive.dart';
import 'package:rive_bloc/rive_bloc.dart'; // 假设rive_bloc库存在
part 'rive_animation_event.dart';
part 'rive_animation_state.dart';
class RiveAnimationBloc extends Bloc<RiveAnimationEvent, RiveAnimationState> {
RiveAnimationBloc() : super(RiveAnimationInitial());
@override
Stream<RiveAnimationState> mapEventToState(
RiveAnimationEvent event,
) async* {
if (event is LoadRiveAnimation) {
final RiveFile riveFile = await RiveFile.asset(event.assetPath);
final Artboard artboard = riveFile.mainArtboard;
yield RiveAnimationLoaded(riveFile: riveFile, artboard: artboard);
artboard.addController(
SimpleAnimation('idle', mix: 1.0)
..isLooping = true,
);
}
// 添加其他事件处理逻辑
}
}
4. 定义事件和状态类
rive_animation_event.dart
:
abstract class RiveAnimationEvent {}
class LoadRiveAnimation extends RiveAnimationEvent {
final String assetPath;
LoadRiveAnimation({required this.assetPath});
}
rive_animation_state.dart
:
import 'package:rive/rive.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class RiveAnimationState extends Equatable {
const RiveAnimationState();
}
class RiveAnimationInitial extends RiveAnimationState {
@override
List<Object?> get props => [];
}
class RiveAnimationLoaded extends RiveAnimationState {
final RiveFile riveFile;
final Artboard artboard;
RiveAnimationLoaded({required this.riveFile, required this.artboard});
@override
List<Object?> get props => [riveFile, artboard];
}
5. 使用Rive动画在UI中
在你的Flutter组件中使用RiveAnimationBloc
和Rive
组件来渲染动画。
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rive/rive.dart';
import 'package:rive_bloc/rive_bloc.dart'; // 假设rive_bloc库存在
class RiveAnimationScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => RiveAnimationBloc(),
child: Scaffold(
appBar: AppBar(title: Text('Rive Animation')),
body: BlocBuilder<RiveAnimationBloc, RiveAnimationState>(
builder: (context, state) {
if (state is RiveAnimationLoaded) {
return Center(
child: Rive(
artboard: state.artboard,
),
);
}
return Center(child: CircularProgressIndicator());
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<RiveAnimationBloc>().add(LoadRiveAnimation(assetPath: 'assets/animation.riv'));
},
tooltip: 'Load Animation',
child: Icon(Icons.play_arrow),
),
),
);
}
}
注意:上面的代码假设存在一个rive_bloc
库,但实际上rive_bloc
可能并不存在作为一个独立的库。通常,你会直接使用rive
库与bloc
库结合来管理动画状态。如果rive_bloc
不存在,你可以直接使用rive
库与自定义的Bloc逻辑结合。
这个例子展示了如何使用Bloc模式管理Rive动画的加载和播放。你需要根据实际需求调整Bloc逻辑和UI组件。