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 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 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 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 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 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 * 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#parseColor(String) 321 */ 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, null); 332 if (foreground) { 333 return new TextAppearanceSpan(null, 0, 0, colors, null); 334 } else { 335 c = colors.getDefaultColor(); 336 } 337 } 338 } else { 339 try { 340 c = Color.parseColor(color); 341 } catch (IllegalArgumentException e) { 342 c = Color.BLACK; 343 } 344 } 345 } 346 347 if (foreground) { 348 return new ForegroundColorSpan(c); 349 } else { 350 return new BackgroundColorSpan(c); 351 } 352 } 353 354 /** 355 * If a translator has messed up the edges of paragraph-level markup, 356 * fix it to actually cover the entire paragraph that it is attached to 357 * instead of just whatever range they put it on. 358 */ 359 private static void addParagraphSpan(Spannable buffer, Object what, 360 int start, int end) { 361 int len = buffer.length(); 362 363 if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { 364 for (start--; start > 0; start--) { 365 if (buffer.charAt(start - 1) == '\n') { 366 break; 367 } 368 } 369 } 370 371 if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { 372 for (end++; end < len; end++) { 373 if (buffer.charAt(end - 1) == '\n') { 374 break; 375 } 376 } 377 } 378 379 buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); 380 } 381 382 private static String subtag(String full, String attribute) { 383 int start = full.indexOf(attribute); 384 if (start < 0) { 385 return null; 386 } 387 388 start += attribute.length(); 389 int end = full.indexOf(';', start); 390 391 if (end < 0) { 392 return full.substring(start); 393 } else { 394 return full.substring(start, end); 395 } 396 } 397 398 /** 399 * Forces the text line to be the specified height, shrinking/stretching 400 * the ascent if possible, or the descent if shrinking the ascent further 401 * will make the text unreadable. 402 */ 403 private static class Height implements LineHeightSpan.WithDensity { 404 private int mSize; 405 private static float sProportion = 0; 406 407 public Height(int size) { 408 mSize = size; 409 } 410 411 public void chooseHeight(CharSequence text, int start, int end, 412 int spanstartv, int v, 413 Paint.FontMetricsInt fm) { 414 // Should not get called, at least not by StaticLayout. 415 chooseHeight(text, start, end, spanstartv, v, fm, null); 416 } 417 418 public void chooseHeight(CharSequence text, int start, int end, 419 int spanstartv, int v, 420 Paint.FontMetricsInt fm, TextPaint paint) { 421 int size = mSize; 422 if (paint != null) { 423 size *= paint.density; 424 } 425 426 if (fm.bottom - fm.top < size) { 427 fm.top = fm.bottom - size; 428 fm.ascent = fm.ascent - size; 429 } else { 430 if (sProportion == 0) { 431 /* 432 * Calculate what fraction of the nominal ascent 433 * the height of a capital letter actually is, 434 * so that we won't reduce the ascent to less than 435 * that unless we absolutely have to. 436 */ 437 438 Paint p = new Paint(); 439 p.setTextSize(100); 440 Rect r = new Rect(); 441 p.getTextBounds("ABCDEFG", 0, 7, r); 442 443 sProportion = (r.top) / p.ascent(); 444 } 445 446 int need = (int) Math.ceil(-fm.top * sProportion); 447 448 if (size - fm.descent >= need) { 449 /* 450 * It is safe to shrink the ascent this much. 451 */ 452 453 fm.top = fm.bottom - size; 454 fm.ascent = fm.descent - size; 455 } else if (size >= need) { 456 /* 457 * We can't show all the descent, but we can at least 458 * show all the ascent. 459 */ 460 461 fm.top = fm.ascent = -need; 462 fm.bottom = fm.descent = fm.top + size; 463 } else { 464 /* 465 * Show as much of the ascent as we can, and no descent. 466 */ 467 468 fm.top = fm.ascent = -size; 469 fm.bottom = fm.descent = 0; 470 } 471 } 472 } 473 } 474 475 /** 476 * Create from an existing string block native object. This is 477 * -extremely- dangerous -- only use it if you absolutely know what you 478 * are doing! The given native object must exist for the entire lifetime 479 * of this newly creating StringBlock. 480 */ 481 StringBlock(long obj, boolean useSparse) { 482 mNative = obj; 483 mUseSparse = useSparse; 484 mOwnsNative = false; 485 if (localLOGV) Log.v(TAG, "Created string block " + this 486 + ": " + nativeGetSize(mNative)); 487 } 488 489 private static native long nativeCreate(byte[] data, 490 int offset, 491 int size); 492 private static native int nativeGetSize(long obj); 493 private static native String nativeGetString(long obj, int idx); 494 private static native int[] nativeGetStyle(long obj, int idx); 495 private static native void nativeDestroy(long obj); 496 } 497