Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package android.text; |
| 18 | |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 19 | import android.annotation.FloatRange; |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 20 | import android.annotation.IntDef; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 21 | import android.annotation.IntRange; |
| 22 | import android.annotation.NonNull; |
| 23 | import android.annotation.Nullable; |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 24 | import android.graphics.Rect; |
| 25 | import android.text.style.MetricAffectingSpan; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 26 | |
| 27 | import com.android.internal.util.Preconditions; |
| 28 | |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 29 | import java.lang.annotation.Retention; |
| 30 | import java.lang.annotation.RetentionPolicy; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 31 | import java.util.ArrayList; |
| 32 | import java.util.Objects; |
| 33 | |
| 34 | /** |
| 35 | * A text which has the character metrics data. |
| 36 | * |
| 37 | * A text object that contains the character metrics data and can be used to improve the performance |
| 38 | * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence}, |
| 39 | * it will measure the text metrics during the creation. This PrecomputedText instance can be set on |
| 40 | * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will |
| 41 | * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not |
| 42 | * have to recalculate this information. |
| 43 | * |
| 44 | * Note that the {@link PrecomputedText} created from different parameters of the target {@link |
| 45 | * android.widget.TextView} will be rejected internally and compute the text layout again with the |
| 46 | * current {@link android.widget.TextView} parameters. |
| 47 | * |
| 48 | * <pre> |
| 49 | * An example usage is: |
| 50 | * <code> |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 51 | * static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) { |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 52 | * // construct precompute related parameters using the TextView that we will set the text on. |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 53 | * final PrecomputedText.Params params = textView.getTextMetricsParams(); |
| 54 | * final Reference textViewRef = new WeakReference<>(textView); |
| 55 | * bgExecutor.submit(() -> { |
| 56 | * TextView textView = textViewRef.get(); |
| 57 | * if (textView == null) return; |
| 58 | * final PrecomputedText precomputedText = PrecomputedText.create(longString, params); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 59 | * textView.post(() -> { |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 60 | * TextView textView = textViewRef.get(); |
| 61 | * if (textView == null) return; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 62 | * textView.setText(precomputedText); |
| 63 | * }); |
| 64 | * }); |
| 65 | * } |
| 66 | * </code> |
| 67 | * </pre> |
| 68 | * |
| 69 | * Note that the {@link PrecomputedText} created from different parameters of the target |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 70 | * {@link android.widget.TextView} will be rejected. |
| 71 | * |
| 72 | * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to |
| 73 | * PrecomputedText. |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 74 | */ |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 75 | public class PrecomputedText implements Spannable { |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 76 | private static final char LINE_FEED = '\n'; |
| 77 | |
| 78 | /** |
| 79 | * The information required for building {@link PrecomputedText}. |
| 80 | * |
| 81 | * Contains information required for precomputing text measurement metadata, so it can be done |
| 82 | * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout |
| 83 | * constraints are not known. |
| 84 | */ |
| 85 | public static final class Params { |
| 86 | // The TextPaint used for measurement. |
| 87 | private final @NonNull TextPaint mPaint; |
| 88 | |
| 89 | // The requested text direction. |
| 90 | private final @NonNull TextDirectionHeuristic mTextDir; |
| 91 | |
| 92 | // The break strategy for this measured text. |
| 93 | private final @Layout.BreakStrategy int mBreakStrategy; |
| 94 | |
| 95 | // The hyphenation frequency for this measured text. |
| 96 | private final @Layout.HyphenationFrequency int mHyphenationFrequency; |
| 97 | |
| 98 | /** |
| 99 | * A builder for creating {@link Params}. |
| 100 | */ |
| 101 | public static class Builder { |
| 102 | // The TextPaint used for measurement. |
| 103 | private final @NonNull TextPaint mPaint; |
| 104 | |
| 105 | // The requested text direction. |
| 106 | private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; |
| 107 | |
| 108 | // The break strategy for this measured text. |
| 109 | private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY; |
| 110 | |
| 111 | // The hyphenation frequency for this measured text. |
| 112 | private @Layout.HyphenationFrequency int mHyphenationFrequency = |
| 113 | Layout.HYPHENATION_FREQUENCY_NORMAL; |
| 114 | |
| 115 | /** |
| 116 | * Builder constructor. |
| 117 | * |
| 118 | * @param paint the paint to be used for drawing |
| 119 | */ |
| 120 | public Builder(@NonNull TextPaint paint) { |
| 121 | mPaint = paint; |
| 122 | } |
| 123 | |
| 124 | /** |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 125 | * Builder constructor from existing params. |
| 126 | */ |
| 127 | public Builder(@NonNull Params params) { |
| 128 | mPaint = params.mPaint; |
| 129 | mTextDir = params.mTextDir; |
| 130 | mBreakStrategy = params.mBreakStrategy; |
| 131 | mHyphenationFrequency = params.mHyphenationFrequency; |
| 132 | } |
| 133 | |
| 134 | /** |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 135 | * Set the line break strategy. |
| 136 | * |
| 137 | * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}. |
| 138 | * |
| 139 | * @param strategy the break strategy |
| 140 | * @return this builder, useful for chaining |
| 141 | * @see StaticLayout.Builder#setBreakStrategy |
| 142 | * @see android.widget.TextView#setBreakStrategy |
| 143 | */ |
| 144 | public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) { |
| 145 | mBreakStrategy = strategy; |
| 146 | return this; |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Set the hyphenation frequency. |
| 151 | * |
| 152 | * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}. |
| 153 | * |
| 154 | * @param frequency the hyphenation frequency |
| 155 | * @return this builder, useful for chaining |
| 156 | * @see StaticLayout.Builder#setHyphenationFrequency |
| 157 | * @see android.widget.TextView#setHyphenationFrequency |
| 158 | */ |
| 159 | public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) { |
| 160 | mHyphenationFrequency = frequency; |
| 161 | return this; |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Set the text direction heuristic. |
| 166 | * |
| 167 | * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. |
| 168 | * |
| 169 | * @param textDir the text direction heuristic for resolving bidi behavior |
| 170 | * @return this builder, useful for chaining |
| 171 | * @see StaticLayout.Builder#setTextDirection |
| 172 | */ |
| 173 | public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { |
| 174 | mTextDir = textDir; |
| 175 | return this; |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Build the {@link Params}. |
| 180 | * |
| 181 | * @return the layout parameter |
| 182 | */ |
| 183 | public @NonNull Params build() { |
| 184 | return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | // This is public hidden for internal use. |
| 189 | // For the external developers, use Builder instead. |
| 190 | /** @hide */ |
| 191 | public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, |
| 192 | @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { |
| 193 | mPaint = paint; |
| 194 | mTextDir = textDir; |
| 195 | mBreakStrategy = strategy; |
| 196 | mHyphenationFrequency = frequency; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Returns the {@link TextPaint} for this text. |
| 201 | * |
| 202 | * @return A {@link TextPaint} |
| 203 | */ |
| 204 | public @NonNull TextPaint getTextPaint() { |
| 205 | return mPaint; |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Returns the {@link TextDirectionHeuristic} for this text. |
| 210 | * |
| 211 | * @return A {@link TextDirectionHeuristic} |
| 212 | */ |
| 213 | public @NonNull TextDirectionHeuristic getTextDirection() { |
| 214 | return mTextDir; |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Returns the break strategy for this text. |
| 219 | * |
| 220 | * @return A line break strategy |
| 221 | */ |
| 222 | public @Layout.BreakStrategy int getBreakStrategy() { |
| 223 | return mBreakStrategy; |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Returns the hyphenation frequency for this text. |
| 228 | * |
| 229 | * @return A hyphenation frequency |
| 230 | */ |
| 231 | public @Layout.HyphenationFrequency int getHyphenationFrequency() { |
| 232 | return mHyphenationFrequency; |
| 233 | } |
| 234 | |
Seigo Nonaka | e1ffb54 | 2018-02-28 18:21:29 -0800 | [diff] [blame] | 235 | /** @hide */ |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 236 | @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE }) |
| 237 | @Retention(RetentionPolicy.SOURCE) |
| 238 | public @interface CheckResultUsableResult {} |
| 239 | |
| 240 | /** |
| 241 | * Constant for returning value of checkResultUsable indicating that given parameter is not |
| 242 | * compatible. |
| 243 | * @hide |
| 244 | */ |
| 245 | public static final int UNUSABLE = 0; |
| 246 | |
| 247 | /** |
| 248 | * Constant for returning value of checkResultUsable indicating that given parameter is not |
| 249 | * compatible but partially usable for creating new PrecomputedText. |
| 250 | * @hide |
| 251 | */ |
| 252 | public static final int NEED_RECOMPUTE = 1; |
| 253 | |
| 254 | /** |
| 255 | * Constant for returning value of checkResultUsable indicating that given parameter is |
| 256 | * compatible. |
| 257 | * @hide |
| 258 | */ |
| 259 | public static final int USABLE = 2; |
| 260 | |
| 261 | /** @hide */ |
| 262 | public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint, |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 263 | @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, |
| 264 | @Layout.HyphenationFrequency int frequency) { |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 265 | if (mBreakStrategy == strategy && mHyphenationFrequency == frequency |
| 266 | && mPaint.equalsForTextMeasurement(paint)) { |
| 267 | return mTextDir == textDir ? USABLE : NEED_RECOMPUTE; |
| 268 | } else { |
| 269 | return UNUSABLE; |
| 270 | } |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Check if the same text layout. |
| 275 | * |
| 276 | * @return true if this and the given param result in the same text layout |
| 277 | */ |
| 278 | @Override |
| 279 | public boolean equals(@Nullable Object o) { |
| 280 | if (o == this) { |
| 281 | return true; |
| 282 | } |
| 283 | if (o == null || !(o instanceof Params)) { |
| 284 | return false; |
| 285 | } |
| 286 | Params param = (Params) o; |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 287 | return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy, |
| 288 | param.mHyphenationFrequency) == Params.USABLE; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 289 | } |
| 290 | |
| 291 | @Override |
| 292 | public int hashCode() { |
| 293 | // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals. |
| 294 | return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(), |
| 295 | mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(), |
| 296 | mPaint.getTextLocales(), mPaint.getTypeface(), |
| 297 | mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir, |
| 298 | mBreakStrategy, mHyphenationFrequency); |
| 299 | } |
Seigo Nonaka | e1ffb54 | 2018-02-28 18:21:29 -0800 | [diff] [blame] | 300 | |
| 301 | @Override |
| 302 | public String toString() { |
| 303 | return "{" |
| 304 | + "textSize=" + mPaint.getTextSize() |
| 305 | + ", textScaleX=" + mPaint.getTextScaleX() |
| 306 | + ", textSkewX=" + mPaint.getTextSkewX() |
| 307 | + ", letterSpacing=" + mPaint.getLetterSpacing() |
| 308 | + ", textLocale=" + mPaint.getTextLocales() |
| 309 | + ", typeface=" + mPaint.getTypeface() |
| 310 | + ", variationSettings=" + mPaint.getFontVariationSettings() |
| 311 | + ", elegantTextHeight=" + mPaint.isElegantTextHeight() |
| 312 | + ", textDir=" + mTextDir |
| 313 | + ", breakStrategy=" + mBreakStrategy |
| 314 | + ", hyphenationFrequency=" + mHyphenationFrequency |
| 315 | + "}"; |
| 316 | } |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 317 | }; |
| 318 | |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 319 | /** @hide */ |
| 320 | public static class ParagraphInfo { |
| 321 | public final @IntRange(from = 0) int paragraphEnd; |
| 322 | public final @NonNull MeasuredParagraph measured; |
| 323 | |
| 324 | /** |
| 325 | * @param paraEnd the end offset of this paragraph |
| 326 | * @param measured a measured paragraph |
| 327 | */ |
| 328 | public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) { |
| 329 | this.paragraphEnd = paraEnd; |
| 330 | this.measured = measured; |
| 331 | } |
| 332 | }; |
| 333 | |
| 334 | |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 335 | // The original text. |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 336 | private final @NonNull SpannableString mText; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 337 | |
| 338 | // The inclusive start offset of the measuring target. |
| 339 | private final @IntRange(from = 0) int mStart; |
| 340 | |
| 341 | // The exclusive end offset of the measuring target. |
| 342 | private final @IntRange(from = 0) int mEnd; |
| 343 | |
| 344 | private final @NonNull Params mParams; |
| 345 | |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 346 | // The list of measured paragraph info. |
| 347 | private final @NonNull ParagraphInfo[] mParagraphInfo; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 348 | |
| 349 | /** |
| 350 | * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph |
| 351 | * positioning information. |
| 352 | * <p> |
| 353 | * This can be expensive, so computing this on a background thread before your text will be |
| 354 | * presented can save work on the UI thread. |
| 355 | * </p> |
| 356 | * |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 357 | * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the |
| 358 | * created PrecomputedText. |
| 359 | * |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 360 | * @param text the text to be measured |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 361 | * @param params parameters that define how text will be precomputed |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 362 | * @return A {@link PrecomputedText} |
| 363 | */ |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 364 | public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) { |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 365 | ParagraphInfo[] paraInfo = null; |
| 366 | if (text instanceof PrecomputedText) { |
| 367 | final PrecomputedText hintPct = (PrecomputedText) text; |
| 368 | final PrecomputedText.Params hintParams = hintPct.getParams(); |
| 369 | final @Params.CheckResultUsableResult int checkResult = |
| 370 | hintParams.checkResultUsable(params.mPaint, params.mTextDir, |
| 371 | params.mBreakStrategy, params.mHyphenationFrequency); |
| 372 | switch (checkResult) { |
| 373 | case Params.USABLE: |
| 374 | return hintPct; |
| 375 | case Params.NEED_RECOMPUTE: |
| 376 | // To be able to use PrecomputedText for new params, at least break strategy and |
| 377 | // hyphenation frequency must be the same. |
| 378 | if (params.getBreakStrategy() == hintParams.getBreakStrategy() |
| 379 | && params.getHyphenationFrequency() |
| 380 | == hintParams.getHyphenationFrequency()) { |
| 381 | paraInfo = createMeasuredParagraphsFromPrecomputedText( |
| 382 | hintPct, params, true /* compute layout */); |
| 383 | } |
| 384 | break; |
| 385 | case Params.UNUSABLE: |
| 386 | // Unable to use anything in PrecomputedText. Create PrecomputedText as the |
| 387 | // normal text input. |
| 388 | } |
| 389 | |
| 390 | } |
| 391 | if (paraInfo == null) { |
| 392 | paraInfo = createMeasuredParagraphs( |
| 393 | text, params, 0, text.length(), true /* computeLayout */); |
| 394 | } |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 395 | return new PrecomputedText(text, 0, text.length(), params, paraInfo); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 396 | } |
| 397 | |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 398 | private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText( |
| 399 | @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) { |
| 400 | final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE |
| 401 | && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; |
| 402 | ArrayList<ParagraphInfo> result = new ArrayList<>(); |
| 403 | for (int i = 0; i < pct.getParagraphCount(); ++i) { |
| 404 | final int paraStart = pct.getParagraphStart(i); |
| 405 | final int paraEnd = pct.getParagraphEnd(i); |
| 406 | result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( |
| 407 | params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(), |
| 408 | needHyphenation, computeLayout, pct.getMeasuredParagraph(i), |
| 409 | null /* no recycle */))); |
| 410 | } |
| 411 | return result.toArray(new ParagraphInfo[result.size()]); |
| 412 | } |
| 413 | |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 414 | /** @hide */ |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 415 | public static ParagraphInfo[] createMeasuredParagraphs( |
| 416 | @NonNull CharSequence text, @NonNull Params params, |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 417 | @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) { |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 418 | ArrayList<ParagraphInfo> result = new ArrayList<>(); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 419 | |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 420 | Preconditions.checkNotNull(text); |
| 421 | Preconditions.checkNotNull(params); |
| 422 | final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE |
| 423 | && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 424 | |
| 425 | int paraEnd = 0; |
| 426 | for (int paraStart = start; paraStart < end; paraStart = paraEnd) { |
| 427 | paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end); |
| 428 | if (paraEnd < 0) { |
| 429 | // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph |
| 430 | // end. |
| 431 | paraEnd = end; |
| 432 | } else { |
| 433 | paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph. |
| 434 | } |
| 435 | |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 436 | result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( |
| 437 | params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(), |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 438 | needHyphenation, computeLayout, null /* no hint */, |
| 439 | null /* no recycle */))); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 440 | } |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 441 | return result.toArray(new ParagraphInfo[result.size()]); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 442 | } |
| 443 | |
| 444 | // Use PrecomputedText.create instead. |
| 445 | private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start, |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 446 | @IntRange(from = 0) int end, @NonNull Params params, |
| 447 | @NonNull ParagraphInfo[] paraInfo) { |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 448 | mText = new SpannableString(text, true /* ignoreNoCopySpan */); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 449 | mStart = start; |
| 450 | mEnd = end; |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 451 | mParams = params; |
| 452 | mParagraphInfo = paraInfo; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Return the underlying text. |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 457 | * @hide |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 458 | */ |
| 459 | public @NonNull CharSequence getText() { |
| 460 | return mText; |
| 461 | } |
| 462 | |
| 463 | /** |
| 464 | * Returns the inclusive start offset of measured region. |
| 465 | * @hide |
| 466 | */ |
| 467 | public @IntRange(from = 0) int getStart() { |
| 468 | return mStart; |
| 469 | } |
| 470 | |
| 471 | /** |
| 472 | * Returns the exclusive end offset of measured region. |
| 473 | * @hide |
| 474 | */ |
| 475 | public @IntRange(from = 0) int getEnd() { |
| 476 | return mEnd; |
| 477 | } |
| 478 | |
| 479 | /** |
| 480 | * Returns the layout parameters used to measure this text. |
| 481 | */ |
| 482 | public @NonNull Params getParams() { |
| 483 | return mParams; |
| 484 | } |
| 485 | |
| 486 | /** |
| 487 | * Returns the count of paragraphs. |
| 488 | */ |
| 489 | public @IntRange(from = 0) int getParagraphCount() { |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 490 | return mParagraphInfo.length; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 491 | } |
| 492 | |
| 493 | /** |
| 494 | * Returns the paragraph start offset of the text. |
| 495 | */ |
| 496 | public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) { |
| 497 | Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 498 | return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1); |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 499 | } |
| 500 | |
| 501 | /** |
| 502 | * Returns the paragraph end offset of the text. |
| 503 | */ |
| 504 | public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) { |
| 505 | Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex"); |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 506 | return mParagraphInfo[paraIndex].paragraphEnd; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 507 | } |
| 508 | |
| 509 | /** @hide */ |
| 510 | public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) { |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 511 | return mParagraphInfo[paraIndex].measured; |
| 512 | } |
| 513 | |
| 514 | /** @hide */ |
| 515 | public @NonNull ParagraphInfo[] getParagraphInfo() { |
| 516 | return mParagraphInfo; |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 517 | } |
| 518 | |
| 519 | /** |
| 520 | * Returns true if the given TextPaint gives the same result of text layout for this text. |
| 521 | * @hide |
| 522 | */ |
Seigo Nonaka | 291ef05 | 2018-11-16 10:41:32 -0800 | [diff] [blame] | 523 | public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start, |
| 524 | @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, |
| 525 | @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, |
| 526 | @Layout.HyphenationFrequency int frequency) { |
| 527 | if (mStart != start || mEnd != end) { |
| 528 | return Params.UNUSABLE; |
| 529 | } else { |
| 530 | return mParams.checkResultUsable(paint, textDir, strategy, frequency); |
| 531 | } |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 532 | } |
| 533 | |
| 534 | /** @hide */ |
| 535 | public int findParaIndex(@IntRange(from = 0) int pos) { |
| 536 | // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring |
| 537 | // layout support to StaticLayout. |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 538 | for (int i = 0; i < mParagraphInfo.length; ++i) { |
| 539 | if (pos < mParagraphInfo[i].paragraphEnd) { |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 540 | return i; |
| 541 | } |
| 542 | } |
| 543 | throw new IndexOutOfBoundsException( |
Seigo Nonaka | c3328d6 | 2018-03-20 15:18:59 -0700 | [diff] [blame] | 544 | "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 545 | + ", gave " + pos); |
| 546 | } |
| 547 | |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 548 | /** |
| 549 | * Returns text width for the given range. |
| 550 | * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise |
| 551 | * IllegalArgumentException will be thrown. |
| 552 | * |
| 553 | * @param start the inclusive start offset in the text |
| 554 | * @param end the exclusive end offset in the text |
| 555 | * @return the text width |
| 556 | * @throws IllegalArgumentException if start and end offset are in the different paragraph. |
| 557 | */ |
| 558 | public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start, |
| 559 | @IntRange(from = 0) int end) { |
| 560 | Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); |
| 561 | Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); |
| 562 | Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); |
| 563 | |
| 564 | if (start == end) { |
| 565 | return 0; |
| 566 | } |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 567 | final int paraIndex = findParaIndex(start); |
| 568 | final int paraStart = getParagraphStart(paraIndex); |
| 569 | final int paraEnd = getParagraphEnd(paraIndex); |
| 570 | if (start < paraStart || paraEnd < end) { |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 571 | throw new IllegalArgumentException("Cannot measured across the paragraph:" |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 572 | + "para: (" + paraStart + ", " + paraEnd + "), " |
| 573 | + "request: (" + start + ", " + end + ")"); |
| 574 | } |
| 575 | return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart); |
| 576 | } |
| 577 | |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 578 | /** |
| 579 | * Retrieves the text bounding box for the given range. |
| 580 | * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise |
| 581 | * IllegalArgumentException will be thrown. |
| 582 | * |
| 583 | * @param start the inclusive start offset in the text |
| 584 | * @param end the exclusive end offset in the text |
| 585 | * @param bounds the output rectangle |
| 586 | * @throws IllegalArgumentException if start and end offset are in the different paragraph. |
| 587 | */ |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 588 | public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, |
| 589 | @NonNull Rect bounds) { |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 590 | Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset"); |
| 591 | Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset"); |
| 592 | Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset"); |
| 593 | Preconditions.checkNotNull(bounds); |
| 594 | if (start == end) { |
| 595 | bounds.set(0, 0, 0, 0); |
| 596 | return; |
| 597 | } |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 598 | final int paraIndex = findParaIndex(start); |
| 599 | final int paraStart = getParagraphStart(paraIndex); |
| 600 | final int paraEnd = getParagraphEnd(paraIndex); |
| 601 | if (start < paraStart || paraEnd < end) { |
Seigo Nonaka | 151108a | 2018-03-30 14:46:24 -0700 | [diff] [blame] | 602 | throw new IllegalArgumentException("Cannot measured across the paragraph:" |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 603 | + "para: (" + paraStart + ", " + paraEnd + "), " |
| 604 | + "request: (" + start + ", " + end + ")"); |
| 605 | } |
Seigo Nonaka | fb0abe1 | 2018-04-02 23:25:38 -0700 | [diff] [blame] | 606 | getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds); |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 607 | } |
| 608 | |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 609 | /** |
Seigo Nonaka | 6f11c6e | 2018-07-24 11:26:18 -0700 | [diff] [blame] | 610 | * Returns a width of a character at offset |
| 611 | * |
| 612 | * @param offset an offset of the text. |
| 613 | * @return a width of the character. |
| 614 | * @hide |
| 615 | */ |
| 616 | public float getCharWidthAt(@IntRange(from = 0) int offset) { |
| 617 | Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset"); |
| 618 | final int paraIndex = findParaIndex(offset); |
| 619 | final int paraStart = getParagraphStart(paraIndex); |
| 620 | final int paraEnd = getParagraphEnd(paraIndex); |
| 621 | return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart); |
| 622 | } |
| 623 | |
| 624 | /** |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 625 | * Returns the size of native PrecomputedText memory usage. |
| 626 | * |
| 627 | * Note that this is not guaranteed to be accurate. Must be used only for testing purposes. |
| 628 | * @hide |
| 629 | */ |
| 630 | public int getMemoryUsage() { |
| 631 | int r = 0; |
| 632 | for (int i = 0; i < getParagraphCount(); ++i) { |
| 633 | r += getMeasuredParagraph(i).getMemoryUsage(); |
| 634 | } |
| 635 | return r; |
| 636 | } |
| 637 | |
| 638 | /////////////////////////////////////////////////////////////////////////////////////////////// |
Seigo Nonaka | a553477 | 2018-03-15 00:22:20 -0700 | [diff] [blame] | 639 | // Spannable overrides |
| 640 | // |
| 641 | // Do not allow to modify MetricAffectingSpan |
| 642 | |
| 643 | /** |
| 644 | * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. |
| 645 | */ |
| 646 | @Override |
| 647 | public void setSpan(Object what, int start, int end, int flags) { |
| 648 | if (what instanceof MetricAffectingSpan) { |
| 649 | throw new IllegalArgumentException( |
| 650 | "MetricAffectingSpan can not be set to PrecomputedText."); |
| 651 | } |
| 652 | mText.setSpan(what, start, end, flags); |
| 653 | } |
| 654 | |
| 655 | /** |
| 656 | * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified. |
| 657 | */ |
| 658 | @Override |
| 659 | public void removeSpan(Object what) { |
| 660 | if (what instanceof MetricAffectingSpan) { |
| 661 | throw new IllegalArgumentException( |
| 662 | "MetricAffectingSpan can not be removed from PrecomputedText."); |
| 663 | } |
| 664 | mText.removeSpan(what); |
| 665 | } |
| 666 | |
| 667 | /////////////////////////////////////////////////////////////////////////////////////////////// |
Seigo Nonaka | beafa1f | 2018-02-01 21:39:24 -0800 | [diff] [blame] | 668 | // Spanned overrides |
| 669 | // |
| 670 | // Just proxy for underlying mText if appropriate. |
| 671 | |
| 672 | @Override |
| 673 | public <T> T[] getSpans(int start, int end, Class<T> type) { |
| 674 | return mText.getSpans(start, end, type); |
| 675 | } |
| 676 | |
| 677 | @Override |
| 678 | public int getSpanStart(Object tag) { |
| 679 | return mText.getSpanStart(tag); |
| 680 | } |
| 681 | |
| 682 | @Override |
| 683 | public int getSpanEnd(Object tag) { |
| 684 | return mText.getSpanEnd(tag); |
| 685 | } |
| 686 | |
| 687 | @Override |
| 688 | public int getSpanFlags(Object tag) { |
| 689 | return mText.getSpanFlags(tag); |
| 690 | } |
| 691 | |
| 692 | @Override |
| 693 | public int nextSpanTransition(int start, int limit, Class type) { |
| 694 | return mText.nextSpanTransition(start, limit, type); |
| 695 | } |
| 696 | |
| 697 | /////////////////////////////////////////////////////////////////////////////////////////////// |
| 698 | // CharSequence overrides. |
| 699 | // |
| 700 | // Just proxy for underlying mText. |
| 701 | |
| 702 | @Override |
| 703 | public int length() { |
| 704 | return mText.length(); |
| 705 | } |
| 706 | |
| 707 | @Override |
| 708 | public char charAt(int index) { |
| 709 | return mText.charAt(index); |
| 710 | } |
| 711 | |
| 712 | @Override |
| 713 | public CharSequence subSequence(int start, int end) { |
| 714 | return PrecomputedText.create(mText.subSequence(start, end), mParams); |
| 715 | } |
| 716 | |
| 717 | @Override |
| 718 | public String toString() { |
| 719 | return mText.toString(); |
| 720 | } |
| 721 | } |