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 * 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 */ 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, 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 */ addParagraphSpan(Spannable buffer, Object what, int start, int end)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 subtag(String full, String attribute)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 Height(int size)407 public Height(int size) { 408 mSize = size; 409 } 410 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)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 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)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 */ StringBlock(long obj, boolean useSparse)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 nativeCreate(byte[] data, int offset, int size)489 private static native long nativeCreate(byte[] data, 490 int offset, 491 int size); nativeGetSize(long obj)492 private static native int nativeGetSize(long obj); nativeGetString(long obj, int idx)493 private static native String nativeGetString(long obj, int idx); nativeGetStyle(long obj, int idx)494 private static native int[] nativeGetStyle(long obj, int idx); nativeDestroy(long obj)495 private static native void nativeDestroy(long obj); 496 } 497