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
|
||||
- `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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
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;
|
||||
|
||||
TextBoxConfig get boxConfig => _boxConfig;
|
||||
double get lineHeight => _lineHeight;
|
||||
|
||||
TextBoxComponent({
|
||||
super.text,
|
||||
|
||||
Reference in New Issue
Block a user