1 /* 2 * Copyright (C) 2006 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.content.res; 18 19 import android.graphics.Color; 20 import android.text.*; 21 import android.text.style.*; 22 import android.util.Log; 23 import android.util.SparseArray; 24 import android.graphics.Paint; 25 import android.graphics.Rect; 26 import android.graphics.Typeface; 27 28 import java.util.Arrays; 29 30 /** 31 * Conveniences for retrieving data out of a compiled string resource. 32 * 33 * {@hide} 34 */ 35 final class StringBlock { 36 private static final String TAG = "AssetManager"; 37 private static final boolean localLOGV = false; 38 39 private final long mNative; 40 private final boolean mUseSparse; 41 private final boolean mOwnsNative; 42 private CharSequence[] mStrings; 43 private SparseArray<CharSequence> mSparseStrings; 44 StyleIDs mStyleIDs = null; 45 StringBlock(byte[] data, boolean useSparse)46 public StringBlock(byte[] data, boolean useSparse) { 47 mNative = nativeCreate(data, 0, data.length); 48 mUseSparse = useSparse; 49 mOwnsNative = true; 50 if (localLOGV) Log.v(TAG, "Created string block " + this 51 + ": " + nativeGetSize(mNative)); 52 } 53 StringBlock(byte[] data, int offset, int size, boolean useSparse)54 public StringBlock(byte[] data, int offset, int size, boolean useSparse) { 55 mNative = nativeCreate(data, offset, size); 56 mUseSparse = useSparse; 57 mOwnsNative = true; 58 if (localLOGV) Log.v(TAG, "Created string block " + this 59 + ": " + nativeGetSize(mNative)); 60 } 61 get(int idx)62 public CharSequence get(int idx) { 63 synchronized (this) { 64 if (mStrings != null) { 65 CharSequence res = mStrings[idx]; 66 if (res != null) { 67 return res; 68 } 69 } else if (mSparseStrings != null) { 70 CharSequence res = mSparseStrings.get(idx); 71 if (res != null) { 72 return res; 73 } 74 } else { 75 final int num = nativeGetSize(mNative); 76 if (mUseSparse && num > 250) { 77 mSparseStrings = new SparseArray<CharSequence>(); 78 } else { 79 mStrings = new CharSequence[num]; 80 } 81 } 82 String str = nativeGetString(mNative, idx); 83 CharSequence res = str; 84 int[] style = nativeGetStyle(mNative, idx); 85 if (localLOGV) Log.v(TAG, "Got string: " + str); 86 if (localLOGV) Log.v(TAG, "Got styles: " + Arrays.toString(style)); 87 if (style != null) { 88 if (mStyleIDs == null) { 89 mStyleIDs = new StyleIDs(); 90 } 91 92 // the style array is a flat array of <type, start, end> hence 93 // the magic constant 3. 94 for (int styleIndex = 0; styleIndex < style.length; styleIndex += 3) { 95 int styleId = style[styleIndex]; 96 97 if (styleId == mStyleIDs.boldId || styleId == mStyleIDs.italicId 98 || styleId == mStyleIDs.underlineId || styleId == mStyleIDs.ttId 99 || styleId == mStyleIDs.bigId || styleId == mStyleIDs.smallId 100 || styleId == mStyleIDs.subId || styleId == mStyleIDs.supId 101 || styleId == mStyleIDs.strikeId || styleId == mStyleIDs.listItemId 102 || styleId == mStyleIDs.marqueeId) { 103 // id already found skip to next style 104 continue; 105 } 106 107 String styleTag = nativeGetString(mNative, styleId); 108 109 if (styleTag.equals("b")) { 110 mStyleIDs.boldId = styleId; 111 } else if (styleTag.equals("i")) { 112 mStyleIDs.italicId = styleId; 113 } else if (styleTag.equals("u")) { 114 mStyleIDs.underlineId = styleId; 115 } else if (styleTag.equals("tt")) { 116 mStyleIDs.ttId = styleId; 117 } else if (styleTag.equals("big")) { 118 mStyleIDs.bigId = styleId; 119 } else if (styleTag.equals("small")) { 120 mStyleIDs.smallId = styleId; 121 } else if (styleTag.equals("sup")) { 122 mStyleIDs.supId = styleId; 123 } else if (styleTag.equals("sub")) { 124 mStyleIDs.subId = styleId; 125 } else if (styleTag.equals("strike")) { 126 mStyleIDs.strikeId = styleId; 127 } else if (styleTag.equals("li")) { 128 mStyleIDs.listItemId = styleId; 129 } else if (styleTag.equals("marquee")) { 130 mStyleIDs.marqueeId = styleId; 131 } 132 } 133 134 res = applyStyles(str, style, mStyleIDs); 135 } 136 if (mStrings != null) mStrings[idx] = res; 137 else mSparseStrings.put(idx, res); 138 return res; 139 } 140 } 141 finalize()142 protected void finalize() throws Throwable { 143 try { 144 super.finalize(); 145 } finally { 146 if (mOwnsNative) { 147 nativeDestroy(mNative); 148 } 149 } 150 } 151 152 static final class StyleIDs { 153 private int boldId = -1; 154 private int italicId = -1; 155 private int underlineId = -1; 156 private int ttId = -1; 157 private int bigId = -1; 158 private int smallId = -1; 159 private int subId = -1; 160 private int supId = -1; 161 private int strikeId = -1; 162 private int listItemId = -1; 163 private int marqueeId = -1; 164 } 165 applyStyles(String str, int[] style, StyleIDs ids)166 private CharSequence applyStyles(String str, int[] style, StyleIDs ids) { 167 if (style.length == 0) 168 return str; 169 170 SpannableString buffer = new SpannableString(str); 171 int i=0; 172 while (i < style.length) { 173 int type = style[i]; 174 if (localLOGV) Log.v(TAG, "Applying style span id=" + type 175 + ", start=" + style[i+1] + ", end=" + style[i+2]); 176 177 178 if (type == ids.boldId) { 179 buffer.setSpan(new StyleSpan(Typeface.BOLD), 180 style[i+1], style[i+2]+1, 181 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 182 } else if (type == ids.italicId) { 183 buffer.setSpan(new StyleSpan(Typeface.ITALIC), 184 style[i+1], style[i+2]+1, 185 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 186 } else if (type == ids.underlineId) { 187 buffer.setSpan(new UnderlineSpan(), 188 style[i+1], style[i+2]+1, 189 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 190 } else if (type == ids.ttId) { 191 buffer.setSpan(new TypefaceSpan("monospace"), 192 style[i+1], style[i+2]+1, 193 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 194 } else if (type == ids.bigId) { 195 buffer.setSpan(new RelativeSizeSpan(1.25f), 196 style[i+1], style[i+2]+1, 197 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 198 } else if (type == ids.smallId) { 199 buffer.setSpan(new RelativeSizeSpan(0.8f), 200 style[i+1], style[i+2]+1, 201 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 202 } else if (type == ids.subId) { 203 buffer.setSpan(new SubscriptSpan(), 204 style[i+1], style[i+2]+1, 205 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 206 } else if (type == ids.supId) { 207 buffer.setSpan(new SuperscriptSpan(), 208 style[i+1], style[i+2]+1, 209 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 210 } else if (type == ids.strikeId) { 211 buffer.setSpan(new StrikethroughSpan(), 212 style[i+1], style[i+2]+1, 213 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 214 } else if (type == ids.listItemId) { 215 addParagraphSpan(buffer, new BulletSpan(10), 216 style[i+1], style[i+2]+1); 217 } else if (type == ids.marqueeId) { 218 buffer.setSpan(TextUtils.TruncateAt.MARQUEE, 219 style[i+1], style[i+2]+1, 220 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 221 } else { 222 String tag = nativeGetString(mNative, type); 223 224 if (tag.startsWith("font;")) { 225 String sub; 226 227 sub = subtag(tag, ";height="); 228 if (sub != null) { 229 int size = Integer.parseInt(sub); 230 addParagraphSpan(buffer, new Height(size), 231 style[i+1], style[i+2]+1); 232 } 233 234 sub = subtag(tag, ";size="); 235 if (sub != null) { 236 int size = Integer.parseInt(sub); 237 buffer.setSpan(new AbsoluteSizeSpan(size, true), 238 style[i+1], style[i+2]+1, 239 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 240 } 241 242 sub = subtag(tag, ";fgcolor="); 243 if (sub != null) { 244 buffer.setSpan(getColor(sub, true), 245 style[i+1], style[i+2]+1, 246 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 247 } 248 249 sub = subtag(tag, ";color="); 250 if (sub != null) { 251 buffer.setSpan(getColor(sub, true), 252 style[i+1], style[i+2]+1, 253 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 254 } 255 256 sub = subtag(tag, ";bgcolor="); 257 if (sub != null) { 258 buffer.setSpan(getColor(sub, false), 259 style[i+1], style[i+2]+1, 260 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 261 } 262 263 sub = subtag(tag, ";face="); 264 if (sub != null) { 265 buffer.setSpan(new TypefaceSpan(sub), 266 style[i+1], style[i+2]+1, 267 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 268 } 269 } else if (tag.startsWith("a;")) { 270 String sub; 271 272 sub = subtag(tag, ";href="); 273 if (sub != null) { 274 buffer.setSpan(new URLSpan(sub), 275 style[i+1], style[i+2]+1, 276 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 277 } 278 } else if (tag.startsWith("annotation;")) { 279 int len = tag.length(); 280 int next; 281 282 for (int t = tag.indexOf(';'); t < len; t = next) { 283 int eq = tag.indexOf('=', t); 284 if (eq < 0) { 285 break; 286 } 287 288 next = tag.indexOf(';', eq); 289 if (next < 0) { 290 next = len; 291 } 292 293 String key = tag.substring(t + 1, eq); 294 String value = tag.substring(eq + 1, next); 295 296 buffer.setSpan(new Annotation(key, value), 297 style[i+1], style[i+2]+1, 298 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 299 } 300 } 301 } 302 303 i += 3; 304 } 305 return new SpannedString(buffer); 306 } 307 308 /** 309 * Returns a span for the specified color string representation. 310 * If the specified string does not represent a color (null, empty, etc.) 311 * the color black is returned instead. 312 * 313 * @param color The color as a string. Can be a resource reference, 314 * HTML hexadecimal, octal or a name 315 * @param foreground True if the color will be used as the foreground color, 316 * false otherwise 317 * 318 * @return A CharacterStyle 319 * 320 * @see Color#getHtmlColor(String) 321 */ getColor(String color, boolean foreground)322 private static CharacterStyle getColor(String color, boolean foreground) { 323 int c = 0xff000000; 324 325 if (!TextUtils.isEmpty(color)) { 326 if (color.startsWith("@")) { 327 Resources res = Resources.getSystem(); 328 String name = color.substring(1); 329 int colorRes = res.getIdentifier(name, "color", "android"); 330 if (colorRes != 0) { 331 ColorStateList colors = res.getColorStateList(colorRes); 332 if (foreground) { 333 return new TextAppearanceSpan(null, 0, 0, colors, null); 334 } else { 335 c = colors.getDefaultColor(); 336 } 337 } 338 } else { 339 c = Color.getHtmlColor(color); 340 } 341 } 342 343 if (foreground) { 344 return new ForegroundColorSpan(c); 345 } else { 346 return new BackgroundColorSpan(c); 347 } 348 } 349 350 /** 351 * If a translator has messed up the edges of paragraph-level markup, 352 * fix it to actually cover the entire paragraph that it is attached to 353 * instead of just whatever range they put it on. 354 */ addParagraphSpan(Spannable buffer, Object what, int start, int end)355 private static void addParagraphSpan(Spannable buffer, Object what, 356 int start, int end) { 357 int len = buffer.length(); 358 359 if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { 360 for (start--; start > 0; start--) { 361 if (buffer.charAt(start - 1) == '\n') { 362 break; 363 } 364 } 365 } 366 367 if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { 368 for (end++; end < len; end++) { 369 if (buffer.charAt(end - 1) == '\n') { 370 break; 371 } 372 } 373 } 374 375 buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); 376 } 377 subtag(String full, String attribute)378 private static String subtag(String full, String attribute) { 379 int start = full.indexOf(attribute); 380 if (start < 0) { 381 return null; 382 } 383 384 start += attribute.length(); 385 int end = full.indexOf(';', start); 386 387 if (end < 0) { 388 return full.substring(start); 389 } else { 390 return full.substring(start, end); 391 } 392 } 393 394 /** 395 * Forces the text line to be the specified height, shrinking/stretching 396 * the ascent if possible, or the descent if shrinking the ascent further 397 * will make the text unreadable. 398 */ 399 private static class Height implements LineHeightSpan.WithDensity { 400 private int mSize; 401 private static float sProportion = 0; 402 Height(int size)403 public Height(int size) { 404 mSize = size; 405 } 406 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)407 public void chooseHeight(CharSequence text, int start, int end, 408 int spanstartv, int v, 409 Paint.FontMetricsInt fm) { 410 // Should not get called, at least not by StaticLayout. 411 chooseHeight(text, start, end, spanstartv, v, fm, null); 412 } 413 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)414 public void chooseHeight(CharSequence text, int start, int end, 415 int spanstartv, int v, 416 Paint.FontMetricsInt fm, TextPaint paint) { 417 int size = mSize; 418 if (paint != null) { 419 size *= paint.density; 420 } 421 422 if (fm.bottom - fm.top < size) { 423 fm.top = fm.bottom - size; 424 fm.ascent = fm.ascent - size; 425 } else { 426 if (sProportion == 0) { 427 /* 428 * Calculate what fraction of the nominal ascent 429 * the height of a capital letter actually is, 430 * so that we won't reduce the ascent to less than 431 * that unless we absolutely have to. 432 */ 433 434 Paint p = new Paint(); 435 p.setTextSize(100); 436 Rect r = new Rect(); 437 p.getTextBounds("ABCDEFG", 0, 7, r); 438 439 sProportion = (r.top) / p.ascent(); 440 } 441 442 int need = (int) Math.ceil(-fm.top * sProportion); 443 444 if (size - fm.descent >= need) { 445 /* 446 * It is safe to shrink the ascent this much. 447 */ 448 449 fm.top = fm.bottom - size; 450 fm.ascent = fm.descent - size; 451 } else if (size >= need) { 452 /* 453 * We can't show all the descent, but we can at least 454 * show all the ascent. 455 */ 456 457 fm.top = fm.ascent = -need; 458 fm.bottom = fm.descent = fm.top + size; 459 } else { 460 /* 461 * Show as much of the ascent as we can, and no descent. 462 */ 463 464 fm.top = fm.ascent = -size; 465 fm.bottom = fm.descent = 0; 466 } 467 } 468 } 469 } 470 471 /** 472 * Create from an existing string block native object. This is 473 * -extremely- dangerous -- only use it if you absolutely know what you 474 * are doing! The given native object must exist for the entire lifetime 475 * of this newly creating StringBlock. 476 */ StringBlock(long obj, boolean useSparse)477 StringBlock(long obj, boolean useSparse) { 478 mNative = obj; 479 mUseSparse = useSparse; 480 mOwnsNative = false; 481 if (localLOGV) Log.v(TAG, "Created string block " + this 482 + ": " + nativeGetSize(mNative)); 483 } 484 nativeCreate(byte[] data, int offset, int size)485 private static native long nativeCreate(byte[] data, 486 int offset, 487 int size); nativeGetSize(long obj)488 private static native int nativeGetSize(long obj); nativeGetString(long obj, int idx)489 private static native String nativeGetString(long obj, int idx); nativeGetStyle(long obj, int idx)490 private static native int[] nativeGetStyle(long obj, int idx); nativeDestroy(long obj)491 private static native void nativeDestroy(long obj); 492 } 493