Skip to content

Commit 1362778

Browse files
committed
Setup annotated text with actions and highlights
1 parent 6e8264b commit 1362778

File tree

9 files changed

+124
-10
lines changed

9 files changed

+124
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ unlinked_spec.ds
8484
**/android/**/GeneratedPluginRegistrant.java
8585
**/android/key.properties
8686
*.jks
87+
.cxx/
8788

8889
# iOS/XCode related
8990
**/ios/**/*.mode1v3

example/android/app/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ android {
1111
ndkVersion = flutter.ndkVersion
1212

1313
compileOptions {
14-
sourceCompatibility = JavaVersion.VERSION_1_8
15-
targetCompatibility = JavaVersion.VERSION_1_8
14+
sourceCompatibility = JavaVersion.VERSION_17
15+
targetCompatibility = JavaVersion.VERSION_17
1616
}
1717

1818
kotlinOptions {
19-
jvmTarget = JavaVersion.VERSION_1_8
19+
jvmTarget = JavaVersion.VERSION_17
2020
}
2121

2222
defaultConfig {

example/android/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ allprojects {
88
rootProject.buildDir = "../build"
99
subprojects {
1010
project.buildDir = "${rootProject.buildDir}/${project.name}"
11-
}
12-
subprojects {
1311
project.evaluationDependsOn(":app")
1412
}
1513

example/android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
1+
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
22
android.useAndroidX=true
33
android.enableJetifier=true

example/android/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
33
zipStoreBase=GRADLE_USER_HOME
44
zipStorePath=wrapper/dists
5-
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
5+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

example/android/settings.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ pluginManagement {
1818

1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21-
id "com.android.application" version "7.3.0" apply false
22-
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
21+
id "com.android.application" version "8.7.3" apply false
22+
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
2323
}
2424

2525
include ":app"

example/lib/main.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:dcc_toolkit/ui/annotated_text/annotated_text.dart';
12
import 'package:example/core/injectable/injectable.dart';
23
import 'package:example/profile/presentation/cubit/user_cubit.dart';
34
import 'package:example/profile/presentation/user_page.dart';
@@ -33,6 +34,31 @@ class MyHomePage extends StatelessWidget {
3334
body: SafeArea(
3435
child: Column(
3536
children: [
37+
Padding(
38+
padding: const EdgeInsets.all(16),
39+
child: AnnotatedText(
40+
text:
41+
'|Flutter(onFlutterTap)| example of using a Rich Text with annotations with multiple |tap(onTap)| actions.\nThis tap |action(action)| does nothing. And |action| without () does nothing as well',
42+
defaultStyle: const TextStyle(color: Colors.black),
43+
annotationStyle: const TextStyle(color: Colors.blue),
44+
actions: {
45+
'onFlutterTap':
46+
() =>
47+
ScaffoldMessenger.of(context)
48+
..hideCurrentSnackBar()
49+
..showSnackBar(
50+
const SnackBar(content: Text('Flutter')),
51+
),
52+
'onTap':
53+
() =>
54+
ScaffoldMessenger.of(context)
55+
..hideCurrentSnackBar()
56+
..showSnackBar(
57+
const SnackBar(content: Text('Tap')),
58+
),
59+
},
60+
),
61+
),
3662
ElevatedButton(
3763
onPressed:
3864
() => Navigator.of(context).push(

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ packages:
212212
path: ".."
213213
relative: true
214214
source: path
215-
version: "0.0.14"
215+
version: "0.0.15"
216216
equatable:
217217
dependency: transitive
218218
description:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import 'package:flutter/gestures.dart';
2+
import 'package:flutter/material.dart';
3+
4+
/// A widget that displays a text with inline actions.
5+
/// Supported formats: |text(action)| or |text|
6+
/// This way translations can be done with inline actions.
7+
///
8+
/// The text is displayed as a [RichText] widget.
9+
/// The annotations are displayed as a [TextSpan] widget with optionally a [TapGestureRecognizer] attached to it (if the action is not null).
10+
/// The [actions] map is used to map the action name to the action to perform when the text is tapped.
11+
/// The [defaultStyle] is the style of the default text.
12+
/// The [annotationStyle] is the style of the annotated text.
13+
class AnnotatedText extends StatelessWidget {
14+
/// Creates a widget that displays a text with annotations.
15+
const AnnotatedText({
16+
required this.text,
17+
required this.actions,
18+
required this.defaultStyle,
19+
required this.annotationStyle,
20+
super.key,
21+
});
22+
23+
/// The complete text to display.
24+
final String text;
25+
26+
/// A map {actionName: action} of actions to perform when the text is tapped.
27+
final Map<String, VoidCallback>? actions;
28+
29+
/// The style of the default text.
30+
final TextStyle defaultStyle;
31+
32+
/// The style of the annotated text.
33+
final TextStyle annotationStyle;
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
return RichText(
38+
text: _buildTextSpan(
39+
text: text,
40+
defaultStyle: defaultStyle,
41+
annotationStyle: annotationStyle,
42+
actions: actions,
43+
),
44+
);
45+
}
46+
}
47+
48+
TextSpan _buildTextSpan({
49+
required String text,
50+
required TextStyle defaultStyle,
51+
required TextStyle annotationStyle,
52+
Map<String, VoidCallback>? actions,
53+
}) {
54+
/// matches |text(function)| with an action, or |text| without an action
55+
final regex = RegExp(r'\|(.+?)(?:\((.*?)\))?\|');
56+
final spans = <TextSpan>[];
57+
var currentIndex = 0;
58+
59+
for (final match in regex.allMatches(text)) {
60+
final matchStart = match.start;
61+
final matchEnd = match.end;
62+
63+
// Add normal text before match
64+
if (matchStart > currentIndex) {
65+
spans.add(TextSpan(text: text.substring(currentIndex, matchStart), style: defaultStyle));
66+
}
67+
68+
final displayText = match.group(1)!;
69+
final actionKey = match.group(2);
70+
final action = (actionKey != null && actionKey.isNotEmpty && actions != null) ? actions[actionKey] : null;
71+
72+
spans.add(
73+
TextSpan(
74+
text: displayText,
75+
style: annotationStyle,
76+
recognizer: action != null ? (TapGestureRecognizer()..onTap = action) : null,
77+
),
78+
);
79+
80+
currentIndex = matchEnd;
81+
}
82+
83+
// Add remaining text
84+
if (currentIndex < text.length) {
85+
spans.add(TextSpan(text: text.substring(currentIndex), style: defaultStyle));
86+
}
87+
88+
return TextSpan(children: spans);
89+
}

0 commit comments

Comments
 (0)