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:
KurtLa
2023-12-08 20:53:45 +01:00
committed by GitHub
parent 969a84f804
commit 8c3cb72541
5 changed files with 315 additions and 48 deletions

View File

@ -10,10 +10,15 @@ components:
- `TextComponent` for rendering a single line of text
- `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
example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/text_example.dart).
Use the `onFinished` callback to get notified when the text is completely printed.
All components are showcased in
[this example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/text_example.dart).
### TextComponent
@ -109,10 +114,36 @@ class MyTextBox extends TextBoxComponent {
}
```
You can find all the options under [TextBoxComponent's
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
If you want to render an arbitrary TextElement, ranging from a single InlineTextElement to a

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
@ -11,7 +13,8 @@ class TextExample extends FlameGame {
@override
Future<void> onLoad() async {
addAll([
addAll(
[
TextComponent(text: 'Hello, Flame', textRenderer: _regular)
..anchor = Anchor.topCenter
..x = size.x / 2
@ -50,13 +53,47 @@ class TextExample extends FlameGame {
timePerChar: 0,
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 =
TextStyle(fontSize: 18, color: BasicPalette.white.color);
final _regular = TextPaint(style: _regularTextStyle);
final _regularTextStyle = TextStyle(
fontSize: 18,
color: BasicPalette.white.color,
);
final _regular = TextPaint(
style: _regularTextStyle,
);
final _tiny = TextPaint(style: _regularTextStyle.copyWith(fontSize: 14.0));
final _box = _regular.copyWith(
(style) => style.copyWith(
@ -77,6 +114,9 @@ final _shaded = TextPaint(
);
class MyTextBox extends TextBoxComponent {
late Paint paint;
late Rect bgRect;
MyTextBox(
String text, {
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
void render(Canvas canvas) {
final rect = Rect.fromLTWH(0, 0, width, height);
canvas.drawRect(rect, Paint()..color = Colors.white10);
canvas.drawRect(bgRect, paint);
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);
}
}

View File

@ -39,6 +39,7 @@ export 'src/components/nine_tile_box_component.dart';
export 'src/components/parallax_component.dart';
export 'src/components/particle_system_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/sprite_animation_component.dart';
export 'src/components/sprite_animation_group_component.dart';

View 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;
}
}

View File

@ -65,6 +65,7 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
Image? cache;
TextBoxConfig get boxConfig => _boxConfig;
double get lineHeight => _lineHeight;
TextBoxComponent({
super.text,