mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Scrollable TextBoxComponent (#2901)
This PR introduces a new ScrollTextBoxComponent, enhancing the existing text box functionalities with scrollable text capabilities. This component, built on top of the existing TextBoxComponent, is designed to handle scrollable text, thereby providing a better user interface for games that require displaying longer text content. Added docs and and an example. --------- Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com> Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
This commit is contained in:
@ -10,10 +10,15 @@ components:
|
|||||||
|
|
||||||
- `TextComponent` for rendering a single line of text
|
- `TextComponent` for rendering a single line of text
|
||||||
- `TextBoxComponent` for bounding multi-line text within a sized box, including the possibility of a
|
- `TextBoxComponent` for bounding multi-line text within a sized box, including the possibility of a
|
||||||
typing effect
|
typing effect
|
||||||
|
- `ScrollTextBoxComponent` enhances the functionality of `TextBoxComponent` by adding scrolling
|
||||||
|
capability when the text exceeds the boundaries of the enclosing box.
|
||||||
|
|
||||||
Both components are showcased in [this
|
Use the `onFinished` callback to get notified when the text is completely printed.
|
||||||
example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/text_example.dart).
|
|
||||||
|
|
||||||
|
All components are showcased in
|
||||||
|
[this example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/text_example.dart).
|
||||||
|
|
||||||
|
|
||||||
### TextComponent
|
### TextComponent
|
||||||
@ -109,10 +114,36 @@ class MyTextBox extends TextBoxComponent {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
You can find all the options under [TextBoxComponent's
|
You can find all the options under [TextBoxComponent's
|
||||||
API](https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html).
|
API](https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html).
|
||||||
|
|
||||||
|
|
||||||
|
### ScrollTextBoxComponent
|
||||||
|
|
||||||
|
The `ScrollTextBoxComponent` is an advanced version of the `TextBoxComponent`,
|
||||||
|
designed for displaying scrollable text within a defined area.
|
||||||
|
This component is particularly useful for creating interfaces where large amounts of text
|
||||||
|
need to be presented in a constrained space, such as dialogues or information panels.
|
||||||
|
|
||||||
|
Note that the `align` property of `TextBoxComponent` is not available.
|
||||||
|
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyScrollableText extends ScrollTextBoxComponent {
|
||||||
|
MyScrollableText(Vector2 frameSize, String text) : super(
|
||||||
|
size: frameSize,
|
||||||
|
text: text,
|
||||||
|
textRenderer: regular,
|
||||||
|
boxConfig: TextBoxConfig(timePerChar: 0.05),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### TextElementComponent
|
### TextElementComponent
|
||||||
|
|
||||||
If you want to render an arbitrary TextElement, ranging from a single InlineTextElement to a
|
If you want to render an arbitrary TextElement, ranging from a single InlineTextElement to a
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/palette.dart';
|
import 'package:flame/palette.dart';
|
||||||
@ -11,52 +13,87 @@ class TextExample extends FlameGame {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
addAll([
|
addAll(
|
||||||
TextComponent(text: 'Hello, Flame', textRenderer: _regular)
|
[
|
||||||
..anchor = Anchor.topCenter
|
TextComponent(text: 'Hello, Flame', textRenderer: _regular)
|
||||||
..x = size.x / 2
|
..anchor = Anchor.topCenter
|
||||||
..y = 32.0,
|
..x = size.x / 2
|
||||||
TextComponent(text: 'Text with shade', textRenderer: _shaded)
|
..y = 32.0,
|
||||||
..anchor = Anchor.topRight
|
TextComponent(text: 'Text with shade', textRenderer: _shaded)
|
||||||
..position = size - Vector2.all(100),
|
..anchor = Anchor.topRight
|
||||||
TextComponent(text: 'center', textRenderer: _tiny)
|
..position = size - Vector2.all(100),
|
||||||
..anchor = Anchor.center
|
TextComponent(text: 'center', textRenderer: _tiny)
|
||||||
..position.setFrom(size / 2),
|
..anchor = Anchor.center
|
||||||
TextComponent(text: 'bottomRight', textRenderer: _tiny)
|
..position.setFrom(size / 2),
|
||||||
..anchor = Anchor.bottomRight
|
TextComponent(text: 'bottomRight', textRenderer: _tiny)
|
||||||
..position.setFrom(size),
|
..anchor = Anchor.bottomRight
|
||||||
MyTextBox(
|
..position.setFrom(size),
|
||||||
'"This is our world now. The world of the electron and the switch; '
|
MyTextBox(
|
||||||
'the beauty of the baud. We exist without nationality, skin color, '
|
'"This is our world now. The world of the electron and the switch; '
|
||||||
'or religious bias. You wage wars, murder, cheat, lie to us and try '
|
'the beauty of the baud. We exist without nationality, skin color, '
|
||||||
"to make us believe it's for our own good, yet we're the "
|
'or religious bias. You wage wars, murder, cheat, lie to us and try '
|
||||||
'criminals. Yes, I am a criminal. My crime is that of curiosity."',
|
"to make us believe it's for our own good, yet we're the "
|
||||||
)
|
'criminals. Yes, I am a criminal. My crime is that of curiosity."',
|
||||||
..anchor = Anchor.bottomLeft
|
)
|
||||||
..y = size.y,
|
..anchor = Anchor.bottomLeft
|
||||||
MyTextBox(
|
..y = size.y,
|
||||||
'Let A be a finitely generated torsion-free abelian group. Then '
|
MyTextBox(
|
||||||
'A is free.',
|
'Let A be a finitely generated torsion-free abelian group. Then '
|
||||||
align: Anchor.center,
|
'A is free.',
|
||||||
size: Vector2(300, 200),
|
align: Anchor.center,
|
||||||
timePerChar: 0,
|
size: Vector2(300, 200),
|
||||||
margins: 10,
|
timePerChar: 0,
|
||||||
)..position = Vector2(10, 50),
|
margins: 10,
|
||||||
MyTextBox(
|
)..position = Vector2(10, 50),
|
||||||
'Let A be a torsion abelian group. Then A is the direct sum of its '
|
MyTextBox(
|
||||||
'subgroups A(p) for all primes p such that A(p) ≠ 0.',
|
'Let A be a torsion abelian group. Then A is the direct sum of its '
|
||||||
align: Anchor.bottomRight,
|
'subgroups A(p) for all primes p such that A(p) ≠ 0.',
|
||||||
size: Vector2(300, 200),
|
align: Anchor.bottomRight,
|
||||||
timePerChar: 0,
|
size: Vector2(300, 200),
|
||||||
margins: 10,
|
timePerChar: 0,
|
||||||
)..position = Vector2(10, 260),
|
margins: 10,
|
||||||
]);
|
)..position = Vector2(10, 260),
|
||||||
|
TextComponent(
|
||||||
|
text: 'Scroll me when finished:',
|
||||||
|
position: Vector2(size.x / 2, size.y / 2 + 100),
|
||||||
|
anchor: Anchor.bottomCenter,
|
||||||
|
),
|
||||||
|
MyScrollTextBox(
|
||||||
|
'In a bustling city, a small team of developers set out to create '
|
||||||
|
'a mobile game using the Flame engine for Flutter. Their goal was '
|
||||||
|
'simple: to create an engaging, easy-to-play game that could reach '
|
||||||
|
'a wide audience on both iOS and Android platforms. '
|
||||||
|
'After weeks of brainstorming, they decided on a concept: '
|
||||||
|
'a fast-paced, endless runner game set in a whimsical, '
|
||||||
|
'ever-changing world. They named it "Swift Dash." '
|
||||||
|
"Using Flutter's versatility and the Flame engine's "
|
||||||
|
'capabilities, the team crafted a game with vibrant graphics, '
|
||||||
|
'smooth animations, and responsive controls. '
|
||||||
|
'The game featured a character dashing through various landscapes, '
|
||||||
|
'dodging obstacles, and collecting points. '
|
||||||
|
'As they launched "Swift Dash," the team was anxious but hopeful. '
|
||||||
|
'To their delight, the game was well-received. Players loved its '
|
||||||
|
'simplicity and charm, and the game quickly gained popularity.',
|
||||||
|
size: Vector2(200, 150),
|
||||||
|
position: Vector2(size.x / 2, size.y / 2 + 100),
|
||||||
|
anchor: Anchor.topCenter,
|
||||||
|
boxConfig: TextBoxConfig(
|
||||||
|
timePerChar: 0.005,
|
||||||
|
margins: const EdgeInsets.fromLTRB(10, 10, 10, 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _regularTextStyle =
|
final _regularTextStyle = TextStyle(
|
||||||
TextStyle(fontSize: 18, color: BasicPalette.white.color);
|
fontSize: 18,
|
||||||
final _regular = TextPaint(style: _regularTextStyle);
|
color: BasicPalette.white.color,
|
||||||
|
);
|
||||||
|
final _regular = TextPaint(
|
||||||
|
style: _regularTextStyle,
|
||||||
|
);
|
||||||
final _tiny = TextPaint(style: _regularTextStyle.copyWith(fontSize: 14.0));
|
final _tiny = TextPaint(style: _regularTextStyle.copyWith(fontSize: 14.0));
|
||||||
final _box = _regular.copyWith(
|
final _box = _regular.copyWith(
|
||||||
(style) => style.copyWith(
|
(style) => style.copyWith(
|
||||||
@ -77,6 +114,9 @@ final _shaded = TextPaint(
|
|||||||
);
|
);
|
||||||
|
|
||||||
class MyTextBox extends TextBoxComponent {
|
class MyTextBox extends TextBoxComponent {
|
||||||
|
late Paint paint;
|
||||||
|
late Rect bgRect;
|
||||||
|
|
||||||
MyTextBox(
|
MyTextBox(
|
||||||
String text, {
|
String text, {
|
||||||
super.align,
|
super.align,
|
||||||
@ -94,10 +134,46 @@ class MyTextBox extends TextBoxComponent {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() {
|
||||||
|
paint = Paint();
|
||||||
|
bgRect = Rect.fromLTWH(0, 0, width, height);
|
||||||
|
|
||||||
|
paint.color = Colors.white10;
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
final rect = Rect.fromLTWH(0, 0, width, height);
|
canvas.drawRect(bgRect, paint);
|
||||||
canvas.drawRect(rect, Paint()..color = Colors.white10);
|
super.render(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyScrollTextBox extends ScrollTextBoxComponent {
|
||||||
|
late Paint paint;
|
||||||
|
late Rect bgRect;
|
||||||
|
|
||||||
|
MyScrollTextBox(
|
||||||
|
String text, {
|
||||||
|
required super.size,
|
||||||
|
super.boxConfig,
|
||||||
|
super.position,
|
||||||
|
super.anchor,
|
||||||
|
}) : super(text: text, textRenderer: _box);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> onLoad() {
|
||||||
|
paint = Paint();
|
||||||
|
bgRect = Rect.fromLTWH(0, 0, width, height);
|
||||||
|
|
||||||
|
paint.color = Colors.white10;
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {
|
||||||
|
canvas.drawRect(bgRect, paint);
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export 'src/components/nine_tile_box_component.dart';
|
|||||||
export 'src/components/parallax_component.dart';
|
export 'src/components/parallax_component.dart';
|
||||||
export 'src/components/particle_system_component.dart';
|
export 'src/components/particle_system_component.dart';
|
||||||
export 'src/components/position_component.dart';
|
export 'src/components/position_component.dart';
|
||||||
|
export 'src/components/scroll_text_box_component.dart';
|
||||||
export 'src/components/spawn_component.dart';
|
export 'src/components/spawn_component.dart';
|
||||||
export 'src/components/sprite_animation_component.dart';
|
export 'src/components/sprite_animation_component.dart';
|
||||||
export 'src/components/sprite_animation_group_component.dart';
|
export 'src/components/sprite_animation_group_component.dart';
|
||||||
|
|||||||
158
packages/flame/lib/src/components/scroll_text_box_component.dart
Normal file
158
packages/flame/lib/src/components/scroll_text_box_component.dart
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/events.dart';
|
||||||
|
import 'package:flame/text.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
/// [ScrollTextBoxComponent] configures the layout and interactivity of a
|
||||||
|
/// scrollable text box.
|
||||||
|
/// It focuses on the box's size, scrolling mechanics, padding, and alignment,
|
||||||
|
/// contrasting with [TextRenderer], which handles text appearance like font and
|
||||||
|
/// color.
|
||||||
|
///
|
||||||
|
/// This component uses [TextBoxComponent] to provide scrollable text
|
||||||
|
/// capabilities.
|
||||||
|
class ScrollTextBoxComponent<T extends TextRenderer> extends PositionComponent {
|
||||||
|
late final _ScrollTextBoxComponent<T> _scrollTextBoxComponent;
|
||||||
|
|
||||||
|
/// Constructor for [ScrollTextBoxComponent].
|
||||||
|
/// - [size]: Specifies the size of the text box.
|
||||||
|
/// Must have positive dimensions.
|
||||||
|
/// - [text]: The text content to be displayed.
|
||||||
|
/// - [textRenderer]: Handles the rendering of the text.
|
||||||
|
/// - [boxConfig]: Configuration for the text box appearance.
|
||||||
|
/// - Other parameters include alignment, pixel ratio, and positioning
|
||||||
|
/// settings.
|
||||||
|
/// An assertion ensures that the [size] has positive dimensions.
|
||||||
|
ScrollTextBoxComponent({
|
||||||
|
required Vector2 size,
|
||||||
|
String? text,
|
||||||
|
T? textRenderer,
|
||||||
|
TextBoxConfig? boxConfig,
|
||||||
|
Anchor align = Anchor.topLeft,
|
||||||
|
double pixelRatio = 1.0,
|
||||||
|
super.position,
|
||||||
|
super.scale,
|
||||||
|
double angle = 0.0,
|
||||||
|
super.anchor = Anchor.topLeft,
|
||||||
|
super.priority,
|
||||||
|
super.key,
|
||||||
|
List<Component>? children,
|
||||||
|
}) : assert(
|
||||||
|
size.x > 0 && size.y > 0,
|
||||||
|
'size must have positive dimensions: $size',
|
||||||
|
),
|
||||||
|
super(size: size) {
|
||||||
|
final marginTop = boxConfig?.margins.top ?? 0;
|
||||||
|
final marginBottom = boxConfig?.margins.bottom ?? 0;
|
||||||
|
final innerMargins = EdgeInsets.fromLTRB(0, marginTop, 0, marginBottom);
|
||||||
|
|
||||||
|
boxConfig ??= TextBoxConfig();
|
||||||
|
boxConfig = TextBoxConfig(
|
||||||
|
timePerChar: boxConfig.timePerChar,
|
||||||
|
dismissDelay: boxConfig.dismissDelay,
|
||||||
|
growingBox: boxConfig.growingBox,
|
||||||
|
maxWidth: size.x,
|
||||||
|
margins: EdgeInsets.fromLTRB(
|
||||||
|
boxConfig.margins.left,
|
||||||
|
0,
|
||||||
|
boxConfig.margins.right,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_scrollTextBoxComponent = _ScrollTextBoxComponent<T>(
|
||||||
|
text: text,
|
||||||
|
textRenderer: textRenderer,
|
||||||
|
boxConfig: boxConfig,
|
||||||
|
align: align,
|
||||||
|
pixelRatio: pixelRatio,
|
||||||
|
);
|
||||||
|
_scrollTextBoxComponent.setOwnerComponent = this;
|
||||||
|
// Integrates the [ClipComponent] for managing
|
||||||
|
// the text box's scrollable area.
|
||||||
|
add(
|
||||||
|
ClipComponent.rectangle(
|
||||||
|
size: size - Vector2(0, innerMargins.vertical),
|
||||||
|
position: Vector2(0, innerMargins.top),
|
||||||
|
angle: angle,
|
||||||
|
scale: scale,
|
||||||
|
priority: priority,
|
||||||
|
children: children,
|
||||||
|
)..add(_scrollTextBoxComponent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override this method to provide a custom background to the text box.
|
||||||
|
///
|
||||||
|
/// Note: The background is designed to stretch across the entire scrollable
|
||||||
|
/// area of the text box. This ensures that as the user scrolls through the
|
||||||
|
/// text, the background moves in sync with the text. As an alternative,
|
||||||
|
/// consider adding [ScrollTextBoxComponent] to a [SpriteComponent].
|
||||||
|
void drawBackground(Canvas canvas) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private class handling the internal workings of [ScrollTextBoxComponent].
|
||||||
|
///
|
||||||
|
/// Extends [TextBoxComponent] and incorporates drag callbacks for text
|
||||||
|
/// scrolling. It manages the rendering and user interaction for the text within
|
||||||
|
/// the box.
|
||||||
|
class _ScrollTextBoxComponent<T extends TextRenderer> extends TextBoxComponent
|
||||||
|
with DragCallbacks {
|
||||||
|
double scrollBoundsY = 0.0;
|
||||||
|
int _linesScrolled = 0;
|
||||||
|
|
||||||
|
late final ClipComponent clipComponent;
|
||||||
|
|
||||||
|
late ScrollTextBoxComponent<TextRenderer> _owner;
|
||||||
|
|
||||||
|
_ScrollTextBoxComponent({
|
||||||
|
String? text,
|
||||||
|
T? textRenderer,
|
||||||
|
TextBoxConfig? boxConfig,
|
||||||
|
Anchor super.align = Anchor.topLeft,
|
||||||
|
double super.pixelRatio = 1.0,
|
||||||
|
super.position,
|
||||||
|
super.scale,
|
||||||
|
double super.angle = 0.0,
|
||||||
|
}) : super(
|
||||||
|
text: text ?? '',
|
||||||
|
textRenderer: textRenderer ?? TextPaint(),
|
||||||
|
boxConfig: boxConfig ?? TextBoxConfig(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() {
|
||||||
|
clipComponent = parent! as ClipComponent;
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> redraw() async {
|
||||||
|
if ((currentLine + 1 - _linesScrolled) * lineHeight >
|
||||||
|
clipComponent.size.y) {
|
||||||
|
_linesScrolled++;
|
||||||
|
position.y -= lineHeight;
|
||||||
|
scrollBoundsY = -position.y;
|
||||||
|
}
|
||||||
|
await super.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDragUpdate(DragUpdateEvent event) {
|
||||||
|
if (finished && _linesScrolled > 0) {
|
||||||
|
position.y += event.localDelta.y;
|
||||||
|
position.y = position.y.clamp(-scrollBoundsY, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawBackground(Canvas canvas) {
|
||||||
|
_owner.drawBackground(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
set setOwnerComponent(ScrollTextBoxComponent scrollTextBoxComponent) {
|
||||||
|
_owner = scrollTextBoxComponent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -65,6 +65,7 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
|
|||||||
Image? cache;
|
Image? cache;
|
||||||
|
|
||||||
TextBoxConfig get boxConfig => _boxConfig;
|
TextBoxConfig get boxConfig => _boxConfig;
|
||||||
|
double get lineHeight => _lineHeight;
|
||||||
|
|
||||||
TextBoxComponent({
|
TextBoxComponent({
|
||||||
super.text,
|
super.text,
|
||||||
|
|||||||
Reference in New Issue
Block a user