mirror of
https://github.com/rive-app/rive-flutter.git
synced 2025-08-24 16:53:17 +08:00

* chore(flutter): remove old build scrips * chore(flutter): fix analyzer warnings * chore(flutter): update rive_native 0.0.4 * docs(flutter): update readme to point to legacy code List virtualization hit testing 3 (#10157) 8b7aa84704 * feat: propagate hit tests up in the component tree to detect if they are hitting a clipped area Fix bug with Artboard ScaleType in Lists (#10159) 8251f1b259 Fixes a bug when databinding scale type to Artboards used in Lists due to the created Artboard not knowing what it's parent's main layout axis was. We need to pass it down as soon as the artboard is created. List fixes 2 (#10153) 8749deb70a When virtualizing lists, there were some issues with instanced artboards not rendering correctly until the following frame. This fixes that issue, as addresses similar 1 frame delayed layout updates particularly when using nested artboards fix: only shape text with modifiers if the shapes are not empty (#10147) 95bf14f49b fix: include missing header for randomization (#10126) 099266fec8 fix: change keys to rcp to avoid memory issues (#10146) 11042e5b4c change keys to rcp to avoid memory issues List virtualization fixes (#10143) bf3b33a30a First round of fixes for List Virtualization/Carousel Fix layout flicker when adding a new artboard using virtualization Fix hit detection Fix scroll snapping not working properly in Carousel mode Fix z-index issue with Carousel when items wrapped around from end to start. Fix a transform issue found by @JcToon Nnnn data biinding artboard fixes (#10139) e54d2ba962 * update overrides when artboard changes * add nested artboard as hit component even if it does not have state machines to support data binding * when adding a new item to a list, use the previous view model from the list as source if it exists feature: scripting require (#10133) 496fa2b490 * fix: working on dependent imports * feature: add full problem report to wasm * feature: runtime require * feature: scripting require * chore: remove string_view * chore: assert instead of runtime error * chore: fix warning on windows * chore: fix memory leak * chore: fix unused var * chore: more windows warnings * chore: fix dart code_core tests * fix: missing bool in FFI setScriptSource List Virtualization & Scroll Carousel (#9965) d973e8c253 Add support for List virtualization as well as a parameter to the ScrollConstraint to support infinite scrolling (Carousel). There are important caveats with Virtualization enabled: The List instances enough artboards to fit into their parent's bounds and when not rendered, they are pooled and reused as necessary. Lists can be non-uniform meaning they can consist of more than 1 Artboard (ViewModel) type Does not currently work when its parent is set to wrap because more complex computations may result when wrapping In order to use Carousel, virtualization must also be enabled Infinite scroll only works in a single direction, not both at the same time feat(apple): add support for data binding list properties (#9936) d2997eeef4 feat: nested artboards -> components (#10082) 7379bdd49f * publishable mixin * only show components in nested artboard list * updates * icons… * icons * change increment to boolean * remove lib component keys in ext * remove icons * flag * remove `didPublish` property * get this library * fix ups * test fixes * flag inspcetor icons * move publishable to custom enums * tool icon fix * extra flags * use resolver to check if nestable * panel sizes * regen keys * panel width core default value Co-authored-by: Gordon <pggordonhayes@gmail.com>
440 lines
14 KiB
Dart
440 lines
14 KiB
Dart
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:rive_example/examples/databinding_images.dart';
|
|
import 'package:rive_example/examples/examples.dart';
|
|
import 'package:rive/rive.dart' as rive;
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await rive.RiveNative.init();
|
|
|
|
runApp(
|
|
MaterialApp(
|
|
title: 'Rive Example',
|
|
home: const RiveExampleApp(),
|
|
darkTheme: ThemeData(
|
|
fontFamily: 'JetBrainsMono',
|
|
brightness: Brightness.dark,
|
|
scaffoldBackgroundColor: _backgroundColor,
|
|
appBarTheme: const AppBarTheme(backgroundColor: _appBarColor),
|
|
colorScheme: ColorScheme.fromSwatch(brightness: Brightness.dark)
|
|
.copyWith(primary: _primaryColor),
|
|
),
|
|
themeMode: ThemeMode.dark,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Determines which factory/renderer to use for the Rive examples.
|
|
///
|
|
/// In your app you can combine the usage of the Rive Renderer and the Flutter
|
|
/// Renderer. For this example app we have a static variable to determine which
|
|
/// factory to use app wide.
|
|
///
|
|
/// - `rive` uses the Rive Renderer
|
|
/// - `flutter` uses the Flutter Renderer (Skia / Impeller)
|
|
enum RiveFactoryToUse {
|
|
rive,
|
|
flutter,
|
|
}
|
|
|
|
/// An example application demoing Rive.
|
|
class RiveExampleApp extends StatefulWidget {
|
|
const RiveExampleApp({Key? key}) : super(key: key);
|
|
|
|
static RiveFactoryToUse factoryToUse = RiveFactoryToUse.rive;
|
|
|
|
static rive.Factory get getCurrentFactory => switch (factoryToUse) {
|
|
RiveFactoryToUse.rive => rive.Factory.rive,
|
|
RiveFactoryToUse.flutter => rive.Factory.flutter,
|
|
};
|
|
|
|
@override
|
|
State<RiveExampleApp> createState() => _RiveExampleAppState();
|
|
}
|
|
|
|
class _RiveExampleAppState extends State<RiveExampleApp> {
|
|
// ScrollController for the CustomScrollView
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
// Examples organized into sections
|
|
final _sections = [
|
|
const _Section(
|
|
'Getting Started',
|
|
[
|
|
_Page('Rive Widget', ExampleRiveWidget(),
|
|
'Simple example usage of the Rive widget with common parameters.'),
|
|
_Page('Rive Widget Builder', ExampleRiveWidgetBuilder(),
|
|
'Example usage of the Rive builder widget with common parameters.'),
|
|
],
|
|
),
|
|
const _Section(
|
|
'Rive Features',
|
|
[
|
|
_Page('Data Binding', ExampleDataBinding(),
|
|
'Example using Rive data binding at runtime.'),
|
|
_Page('Data Binding - Images', ExampleDataBindingImages(),
|
|
'Example using Rive data binding images at runtime.'),
|
|
_Page('Data Binding - Lists', Todo(),
|
|
'Example using Rive data binding lists at runtime.'),
|
|
_Page('Responsive Layouts', ExampleResponsiveLayouts(),
|
|
'Create responsive Rive graphics that adapt to screen size.'),
|
|
_Page('Events', ExampleEvents(), 'Handle Rive events.'),
|
|
_Page('Audio', ExampleRiveAudio(), 'Example Rive file with audio.'),
|
|
],
|
|
),
|
|
const _Section(
|
|
'Asset Loading',
|
|
[
|
|
_Page('Network .riv Asset', ExampleNetworkAsset(),
|
|
'Load and display Rive graphics from network URLs.'),
|
|
_Page('Out-of-band Assets', ExampleOutOfBandAssetLoading(),
|
|
'Load Rive files with external assets (images, audio) separately.'),
|
|
_Page(
|
|
'Out-of-band Assets - Cached',
|
|
ExampleOutOfBandCachedAssetLoading(),
|
|
'Load Rive files with cached external assets for better immediate availability.',
|
|
),
|
|
],
|
|
),
|
|
const _Section(
|
|
'Painters [Advanced]',
|
|
[
|
|
_Page('State Machine Painter', ExampleStateMachinePainter(),
|
|
'Advanced: Custom painter for state machines.'),
|
|
_Page('Single Animation Painter', ExampleSingleAnimationPainter(),
|
|
'Advanced: Custom painter for single animation playback.'),
|
|
],
|
|
),
|
|
const _Section(
|
|
'Flutter Concepts/Integration',
|
|
[
|
|
// _Page('Flutter Lists', Todo(),
|
|
// 'Integrate Rive graphics with Flutter list widgets.'),
|
|
_Page('Flutter Hit Test + Cursor Behaviour', ExampleHitTestBehaviour(),
|
|
'Specifying hit test and cursor behaviour.'),
|
|
_Page('Flutter Ticker Mode', ExampleTickerMode(),
|
|
'Rive graphics respect Flutter ticker mode.'),
|
|
_Page('Flutter Time Dilation', ExampleTimeDilation(),
|
|
'Rive graphics respect Flutter time dilation.'),
|
|
// _Page('Flutter Hero Transitions', Todo(),
|
|
// 'Create smooth transitions between pages with Rive graphics.'),
|
|
// _Page('Flutter State Management', Todo(),
|
|
// 'Manage Rive state with Flutter state management.'),
|
|
// _Page('Flutter Localization', Todo(),
|
|
// 'Localize Rive graphics for different languages.'),
|
|
// _Page('Flutter Internationalization', Todo(),
|
|
// 'Internationalize Rive graphics with Flutter i18n.'),
|
|
],
|
|
),
|
|
const _Section(
|
|
'Legacy Features [Use data binding instead]',
|
|
[
|
|
_Page('Inputs [Nested]', ExampleInputs(),
|
|
'Legacy: Handle input [nested] controls in Rive graphics.'),
|
|
_Page('Text Runs [Nested]', ExampleTextRuns(),
|
|
'Legacy: Handle text runs [nested] components in Rive graphics.'),
|
|
],
|
|
),
|
|
];
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Rive Examples')),
|
|
body: Column(children: [
|
|
Expanded(
|
|
child: Scrollbar(
|
|
controller: _scrollController,
|
|
child: CustomScrollView(
|
|
controller: _scrollController,
|
|
slivers: [
|
|
SliverPadding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
// Calculate which section and item we're at
|
|
int itemIndex = index;
|
|
|
|
for (int i = 0; i < _sections.length; i++) {
|
|
if (itemIndex == 0) {
|
|
// This is a section header
|
|
return _SectionHeader(_sections[i].title);
|
|
}
|
|
itemIndex--;
|
|
|
|
if (itemIndex < _sections[i].pages.length) {
|
|
// This is a page within the current section
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 16.0),
|
|
child: _NavButton(
|
|
page: _sections[i].pages[itemIndex]),
|
|
);
|
|
}
|
|
itemIndex -= _sections[i].pages.length;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
childCount: _getTotalItemCount(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ColoredBox(
|
|
color: Colors.black,
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: 16),
|
|
const Text('Factory to use:',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Radio<RiveFactoryToUse>(
|
|
value: RiveFactoryToUse.rive,
|
|
groupValue: RiveExampleApp.factoryToUse,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
RiveExampleApp.factoryToUse =
|
|
value as RiveFactoryToUse;
|
|
});
|
|
},
|
|
),
|
|
const Text('Rive Renderer',
|
|
style: TextStyle(fontSize: 14)),
|
|
],
|
|
),
|
|
const SizedBox(width: 32),
|
|
Row(
|
|
children: [
|
|
Radio<RiveFactoryToUse>(
|
|
value: RiveFactoryToUse.flutter,
|
|
groupValue: RiveExampleApp.factoryToUse,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
RiveExampleApp.factoryToUse =
|
|
value as RiveFactoryToUse;
|
|
});
|
|
},
|
|
),
|
|
const Text('Flutter Renderer',
|
|
style: TextStyle(fontSize: 14)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
int _getTotalItemCount() {
|
|
int count = 0;
|
|
for (final section in _sections) {
|
|
count += 1; // Section header
|
|
count += section.pages.length; // Pages in section
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
/// Class used to organize demo sections.
|
|
class _Section {
|
|
final String title;
|
|
final List<_Page> pages;
|
|
|
|
const _Section(this.title, this.pages);
|
|
}
|
|
|
|
/// Class used to organize demo pages.
|
|
class _Page {
|
|
final String name;
|
|
final Widget page;
|
|
final String description;
|
|
|
|
const _Page(this.name, this.page, this.description);
|
|
}
|
|
|
|
/// Section header widget with divider.
|
|
class _SectionHeader extends StatelessWidget {
|
|
const _SectionHeader(this.title);
|
|
|
|
final String title;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
color: _primaryColor,
|
|
),
|
|
),
|
|
),
|
|
const Divider(
|
|
color: _primaryColor,
|
|
thickness: 0.5,
|
|
height: 1,
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Button to navigate to demo pages with hover overlay.
|
|
class _NavButton extends StatefulWidget {
|
|
const _NavButton({required this.page});
|
|
|
|
final _Page page;
|
|
|
|
@override
|
|
State<_NavButton> createState() => _NavButtonState();
|
|
}
|
|
|
|
class _NavButtonState extends State<_NavButton> {
|
|
bool _isHovered = false;
|
|
OverlayEntry? _overlayEntry;
|
|
|
|
@override
|
|
void dispose() {
|
|
_removeOverlay();
|
|
super.dispose();
|
|
}
|
|
|
|
void _removeOverlay() {
|
|
_overlayEntry?.remove();
|
|
_overlayEntry = null;
|
|
}
|
|
|
|
void _showOverlay() {
|
|
_removeOverlay();
|
|
|
|
final RenderBox renderBox = context.findRenderObject() as RenderBox;
|
|
final position = renderBox.localToGlobal(Offset.zero);
|
|
final size = renderBox.size;
|
|
|
|
_overlayEntry = OverlayEntry(
|
|
builder: (context) => Positioned(
|
|
top: position.dy - 80, // Position above the button
|
|
left: position.dx + (size.width / 2) - 150, // Center horizontally
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Container(
|
|
width: 300,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withOpacity(0.9),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: _primaryColor, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
widget.page.description,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
height: 1.4,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
Overlay.of(context).insert(_overlayEntry!);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MouseRegion(
|
|
onEnter: (_) {
|
|
setState(() => _isHovered = true);
|
|
_showOverlay();
|
|
},
|
|
onExit: (_) {
|
|
setState(() => _isHovered = false);
|
|
_removeOverlay();
|
|
},
|
|
child: Center(
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _isHovered ? _primaryColor.withOpacity(0.1) : null,
|
|
elevation: _isHovered ? 8 : 2,
|
|
),
|
|
child: SizedBox(
|
|
width: 300,
|
|
child: Center(
|
|
child: Text(
|
|
widget.page.name,
|
|
style: Theme.of(context).textTheme.labelLarge,
|
|
),
|
|
),
|
|
),
|
|
onPressed: () {
|
|
_removeOverlay();
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute<void>(
|
|
builder: (context) => _WrappedPage(page: widget.page),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Scaffold wrapper for the page.
|
|
class _WrappedPage extends StatelessWidget {
|
|
const _WrappedPage({required this.page});
|
|
|
|
final _Page page;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(page.name)),
|
|
body: page.page,
|
|
);
|
|
}
|
|
}
|
|
|
|
const _appBarColor = Color(0xFF323232);
|
|
const _backgroundColor = Color(0xFF1D1D1D);
|
|
const _primaryColor = Color(0xFF57A5E0);
|