Flutter动画渲染插件rive_bloc的使用

发布于 1周前 作者 eggper 来自 Flutter

Flutter动画渲染插件rive_bloc的使用

River Bloc

RiveBloc #

pub package style: very_good_analysis License: MIT

RiveBloc 是基于 Flutter BLoC 库构建的编程接口层,深受 Riverpod 启发,使得使用 BLoC 更加简洁且省时。

快速开始 🚀 #

RiveBloc 的主要构建块包括 providersbuilders

Provider 是一种获取状态(Object 值或 Bloc/Cubit 实例)的方式,而 Builder 则是一种在小部件树中使用该状态的方式。

RiveBlocProvider 是创建提供者的主入口点。

重要提示:为了让提供者正常工作,你需要在 Flutter 应用程序的根部添加 RiveBlocScope,如下所示:

void main() {
  runApp(RiveBlocScope(child: MyApp()));
}

提供者解决了以下问题:

  • 提供者具有全局变量的灵活性,但没有它们的缺点。它们可以从任何地方访问,同时确保可测试性和可扩展性。
  • 提供者是安全的。不可能在一个未初始化的状态下读取值。
  • 提供者可以通过一行代码访问。

RiveBloc 提供了 5 个提供者,分为两大类:

  • 1 个 Final 提供者用于暴露最终的 Dart Object 值。
  • 4 个 RiveBloc 提供者用于暴露 Bloc/Cubit 动态 state 值。

FinalProvider 使用 RiveBlocProvider.finalValue 创建,而其他提供者则使用 RiveBlocProvider.valueRiveBlocProvider.stateRiveBlocProvider.asyncRiveBlocProvider.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());

一旦声明了提供者,你就可以通过使用 readwatch 方法来访问它们所暴露的值和实例:

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

1 回复

更多关于Flutter动画渲染插件rive_bloc的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter应用中使用rive_bloc插件进行动画渲染的代码案例。rive_bloc结合了rivebloc库,允许你以声明式的方式管理和渲染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组件中使用RiveAnimationBlocRive组件来渲染动画。

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组件。

回到顶部