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 StringBlock(byte[] data, boolean useSparse)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 StringBlock(byte[] data, int offset, int size, boolean useSparse)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 get(int idx)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 finalize()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 applyStyles(String str, int[] style, StyleIDs ids)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 */ addParagraphSpan(Spannable buffer, Object what, int start, int end)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 subtag(String full, String attribute)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 Height(int size)344 public Height(int size) { 345 mSize = size; 346 } 347 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm)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 chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm, TextPaint paint)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 */ StringBlock(int obj, boolean useSparse)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 nativeCreate(byte[] data, int offset, int size)426 private static final native int nativeCreate(byte[] data, 427 int offset, 428 int size); nativeGetSize(int obj)429 private static final native int nativeGetSize(int obj); nativeGetString(int obj, int idx)430 private static final native String nativeGetString(int obj, int idx); nativeGetStyle(int obj, int idx)431 private static final native int[] nativeGetStyle(int obj, int idx); nativeDestroy(int obj)432 private static final native void nativeDestroy(int obj); 433 } 434