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