Flutter Engine
The Flutter Engine
FlutterTextUtils.java
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package io.flutter.plugin.editing;
6
7import io.flutter.embedding.engine.FlutterJNI;
8
10 public static final int LINE_FEED = 0x0A;
11 public static final int CARRIAGE_RETURN = 0x0D;
12 public static final int COMBINING_ENCLOSING_KEYCAP = 0x20E3;
13 public static final int CANCEL_TAG = 0xE007F;
14 public static final int ZERO_WIDTH_JOINER = 0x200D;
15 private final FlutterJNI flutterJNI;
16
17 public FlutterTextUtils(FlutterJNI flutterJNI) {
18 this.flutterJNI = flutterJNI;
19 }
20
21 public boolean isEmoji(int codePoint) {
22 return flutterJNI.isCodePointEmoji(codePoint);
23 }
24
25 public boolean isEmojiModifier(int codePoint) {
26 return flutterJNI.isCodePointEmojiModifier(codePoint);
27 }
28
29 public boolean isEmojiModifierBase(int codePoint) {
30 return flutterJNI.isCodePointEmojiModifierBase(codePoint);
31 }
32
33 public boolean isVariationSelector(int codePoint) {
34 return flutterJNI.isCodePointVariantSelector(codePoint);
35 }
36
37 public boolean isRegionalIndicatorSymbol(int codePoint) {
38 return flutterJNI.isCodePointRegionalIndicator(codePoint);
39 }
40
41 public boolean isTagSpecChar(int codePoint) {
42 return 0xE0020 <= codePoint && codePoint <= 0xE007E;
43 }
44
45 public boolean isKeycapBase(int codePoint) {
46 return ('0' <= codePoint && codePoint <= '9') || codePoint == '#' || codePoint == '*';
47 }
48
49 /**
50 * Start offset for backspace key or moving left from the current offset. Same methods are also
51 * included in Android APIs but they don't work as expected in API Levels lower than 24. Reference
52 * for the logic in this code is the Android source code.
53 *
54 * @see <a target="_new"
55 * href="https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111">https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111</a>
56 */
57 public int getOffsetBefore(CharSequence text, int offset) {
58 if (offset <= 1) {
59 return 0;
60 }
61
62 int codePoint = Character.codePointBefore(text, offset);
63 int deleteCharCount = Character.charCount(codePoint);
64 int lastOffset = offset - deleteCharCount;
65
66 if (lastOffset == 0) {
67 return 0;
68 }
69
70 // Line Feed
71 if (codePoint == LINE_FEED) {
72 codePoint = Character.codePointBefore(text, lastOffset);
73 if (codePoint == CARRIAGE_RETURN) {
74 ++deleteCharCount;
75 }
76 return offset - deleteCharCount;
77 }
78
79 // Flags
80 if (isRegionalIndicatorSymbol(codePoint)) {
81 codePoint = Character.codePointBefore(text, lastOffset);
82 lastOffset -= Character.charCount(codePoint);
83 int regionalIndicatorSymbolCount = 1;
84 while (lastOffset > 0 && isRegionalIndicatorSymbol(codePoint)) {
85 codePoint = Character.codePointBefore(text, lastOffset);
86 lastOffset -= Character.charCount(codePoint);
87 regionalIndicatorSymbolCount++;
88 }
89 if (regionalIndicatorSymbolCount % 2 == 0) {
90 deleteCharCount += 2;
91 }
92 return offset - deleteCharCount;
93 }
94
95 // Keycaps
96 if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
97 codePoint = Character.codePointBefore(text, lastOffset);
98 lastOffset -= Character.charCount(codePoint);
99 if (lastOffset > 0 && isVariationSelector(codePoint)) {
100 int tmpCodePoint = Character.codePointBefore(text, lastOffset);
101 if (isKeycapBase(tmpCodePoint)) {
102 deleteCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
103 }
104 } else if (isKeycapBase(codePoint)) {
105 deleteCharCount += Character.charCount(codePoint);
106 }
107 return offset - deleteCharCount;
108 }
109
110 /**
111 * Following if statements for Emoji tag sequence and Variation selector are skipping these
112 * modifiers for going through the last statement that is for handling emojis. They return the
113 * offset if they don't find proper base characters
114 */
115 // Emoji Tag Sequence
116 if (codePoint == CANCEL_TAG) { // tag_end
117 codePoint = Character.codePointBefore(text, lastOffset);
118 lastOffset -= Character.charCount(codePoint);
119 while (lastOffset > 0 && isTagSpecChar(codePoint)) { // tag_spec
120 deleteCharCount += Character.charCount(codePoint);
121 codePoint = Character.codePointBefore(text, lastOffset);
122 lastOffset -= Character.charCount(codePoint);
123 }
124 if (!isEmoji(codePoint)) { // tag_base not found. Just delete the end.
125 return offset - 2;
126 }
127 deleteCharCount += Character.charCount(codePoint);
128 }
129
130 if (isVariationSelector(codePoint)) {
131 codePoint = Character.codePointBefore(text, lastOffset);
132 if (!isEmoji(codePoint)) {
133 return offset - deleteCharCount;
134 }
135 deleteCharCount += Character.charCount(codePoint);
136
137 lastOffset -= deleteCharCount;
138 }
139
140 if (isEmoji(codePoint)) {
141 boolean isZwj = false;
142 int lastSeenVariantSelectorCharCount = 0;
143 do {
144 if (isZwj) {
145 deleteCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1;
146 isZwj = false;
147 }
148 lastSeenVariantSelectorCharCount = 0;
149 if (isEmojiModifier(codePoint)) {
150 codePoint = Character.codePointBefore(text, lastOffset);
151 lastOffset -= Character.charCount(codePoint);
152 if (lastOffset > 0 && isVariationSelector(codePoint)) {
153 codePoint = Character.codePointBefore(text, lastOffset);
154 if (!isEmoji(codePoint)) {
155 return offset - deleteCharCount;
156 }
157 lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
158 lastOffset -= Character.charCount(codePoint);
159 }
160 if (isEmojiModifierBase(codePoint)) {
161 deleteCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
162 }
163 break;
164 }
165
166 if (lastOffset > 0) {
167 codePoint = Character.codePointBefore(text, lastOffset);
168 lastOffset -= Character.charCount(codePoint);
169 if (codePoint == ZERO_WIDTH_JOINER) {
170 isZwj = true;
171 codePoint = Character.codePointBefore(text, lastOffset);
172 lastOffset -= Character.charCount(codePoint);
173 if (lastOffset > 0 && isVariationSelector(codePoint)) {
174 codePoint = Character.codePointBefore(text, lastOffset);
175 lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
176 lastOffset -= Character.charCount(codePoint);
177 }
178 }
179 }
180
181 if (lastOffset == 0) {
182 break;
183 }
184 } while (isZwj && isEmoji(codePoint));
185 }
186
187 return offset - deleteCharCount;
188 }
189
190 /**
191 * Gets the offset of the next character following the given offset, with consideration for
192 * multi-byte characters.
193 *
194 * @see <a target="_new"
195 * href="https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111">https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-s3-release/core/java/android/text/method/BaseKeyListener.java#111</a>
196 */
197 public int getOffsetAfter(CharSequence text, int offset) {
198 final int len = text.length();
199
200 if (offset >= len - 1) {
201 return len;
202 }
203
204 int codePoint = Character.codePointAt(text, offset);
205 int nextCharCount = Character.charCount(codePoint);
206 int nextOffset = offset + nextCharCount;
207
208 if (nextOffset == 0) {
209 return 0;
210 }
211
212 // Line Feed
213 if (codePoint == LINE_FEED) {
214 codePoint = Character.codePointAt(text, nextOffset);
215 if (codePoint == CARRIAGE_RETURN) {
216 ++nextCharCount;
217 }
218 return offset + nextCharCount;
219 }
220
221 // Flags
222 if (isRegionalIndicatorSymbol(codePoint)) {
223 if (nextOffset >= len - 1
224 || !isRegionalIndicatorSymbol(Character.codePointAt(text, nextOffset))) {
225 return offset + nextCharCount;
226 }
227 // In this case there are at least two regional indicator symbols ahead of
228 // offset. If those two regional indicator symbols are a pair that
229 // represent a region together, the next offset should be after both of
230 // them.
231 int regionalIndicatorSymbolCount = 0;
232 int regionOffset = offset;
233 while (regionOffset > 0
234 && isRegionalIndicatorSymbol(Character.codePointBefore(text, offset))) {
235 regionOffset -= Character.charCount(Character.codePointBefore(text, offset));
236 regionalIndicatorSymbolCount++;
237 }
238 if (regionalIndicatorSymbolCount % 2 == 0) {
239 nextCharCount += 2;
240 }
241 return offset + nextCharCount;
242 }
243
244 // Keycaps
245 if (isKeycapBase(codePoint)) {
246 nextCharCount += Character.charCount(codePoint);
247 }
248 if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
249 codePoint = Character.codePointBefore(text, nextOffset);
250 nextOffset += Character.charCount(codePoint);
251 if (nextOffset < len && isVariationSelector(codePoint)) {
252 int tmpCodePoint = Character.codePointAt(text, nextOffset);
253 if (isKeycapBase(tmpCodePoint)) {
254 nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
255 }
256 } else if (isKeycapBase(codePoint)) {
257 nextCharCount += Character.charCount(codePoint);
258 }
259 return offset + nextCharCount;
260 }
261
262 if (isEmoji(codePoint)) {
263 boolean isZwj = false;
264 int lastSeenVariantSelectorCharCount = 0;
265 do {
266 if (isZwj) {
267 nextCharCount += Character.charCount(codePoint) + lastSeenVariantSelectorCharCount + 1;
268 isZwj = false;
269 }
270 lastSeenVariantSelectorCharCount = 0;
271 if (isEmojiModifier(codePoint)) {
272 break;
273 }
274
275 if (nextOffset < len) {
276 codePoint = Character.codePointAt(text, nextOffset);
277 nextOffset += Character.charCount(codePoint);
278 if (codePoint == COMBINING_ENCLOSING_KEYCAP) {
279 codePoint = Character.codePointBefore(text, nextOffset);
280 nextOffset += Character.charCount(codePoint);
281 if (nextOffset < len && isVariationSelector(codePoint)) {
282 int tmpCodePoint = Character.codePointAt(text, nextOffset);
283 if (isKeycapBase(tmpCodePoint)) {
284 nextCharCount += Character.charCount(codePoint) + Character.charCount(tmpCodePoint);
285 }
286 } else if (isKeycapBase(codePoint)) {
287 nextCharCount += Character.charCount(codePoint);
288 }
289 return offset + nextCharCount;
290 }
291 if (isEmojiModifier(codePoint)) {
292 nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
293 break;
294 }
295 if (isVariationSelector(codePoint)) {
296 nextCharCount += lastSeenVariantSelectorCharCount + Character.charCount(codePoint);
297 break;
298 }
299 if (codePoint == ZERO_WIDTH_JOINER) {
300 isZwj = true;
301 codePoint = Character.codePointAt(text, nextOffset);
302 nextOffset += Character.charCount(codePoint);
303 if (nextOffset < len && isVariationSelector(codePoint)) {
304 codePoint = Character.codePointAt(text, nextOffset);
305 lastSeenVariantSelectorCharCount = Character.charCount(codePoint);
306 nextOffset += Character.charCount(codePoint);
307 }
308 }
309 }
310
311 if (nextOffset >= len) {
312 break;
313 }
314 } while (isZwj && isEmoji(codePoint));
315 }
316
317 return offset + nextCharCount;
318 }
319}
boolean isCodePointEmojiModifier(int codePoint)
boolean isCodePointEmojiModifierBase(int codePoint)
boolean isCodePointRegionalIndicator(int codePoint)
boolean isCodePointEmoji(int codePoint)
boolean isCodePointVariantSelector(int codePoint)
int getOffsetAfter(CharSequence text, int offset)
int getOffsetBefore(CharSequence text, int offset)
std::u16string text
SeparatedVector2 offset