WidgetPreviewLoader.java revision e5919c5574ff09b88173b44558c6d325841511d6
1package com.android.launcher2; 2 3import android.appwidget.AppWidgetProviderInfo; 4import android.content.ComponentName; 5import android.content.ContentValues; 6import android.content.Context; 7import android.content.pm.PackageManager; 8import android.content.pm.ResolveInfo; 9import android.content.res.Resources; 10import android.database.Cursor; 11import android.database.sqlite.SQLiteDatabase; 12import android.database.sqlite.SQLiteOpenHelper; 13import android.graphics.Bitmap; 14import android.graphics.Bitmap.Config; 15import android.graphics.BitmapFactory; 16import android.graphics.Canvas; 17import android.graphics.ColorMatrix; 18import android.graphics.ColorMatrixColorFilter; 19import android.graphics.Paint; 20import android.graphics.PorterDuff; 21import android.graphics.Rect; 22import android.graphics.Shader; 23import android.graphics.drawable.BitmapDrawable; 24import android.graphics.drawable.Drawable; 25import android.os.AsyncTask; 26import android.util.Log; 27 28import com.android.launcher.R; 29 30import java.io.ByteArrayOutputStream; 31import java.io.File; 32import java.lang.ref.SoftReference; 33import java.lang.ref.WeakReference; 34import java.util.ArrayList; 35import java.util.HashMap; 36import java.util.HashSet; 37 38abstract class SoftReferenceThreadLocal<T> { 39 private ThreadLocal<SoftReference<T>> mThreadLocal; 40 public SoftReferenceThreadLocal() { 41 mThreadLocal = new ThreadLocal<SoftReference<T>>(); 42 } 43 44 abstract T initialValue(); 45 46 public void set(T t) { 47 mThreadLocal.set(new SoftReference<T>(t)); 48 } 49 50 public T get() { 51 SoftReference<T> reference = mThreadLocal.get(); 52 T obj; 53 if (reference == null) { 54 obj = initialValue(); 55 mThreadLocal.set(new SoftReference<T>(obj)); 56 return obj; 57 } else { 58 obj = reference.get(); 59 if (obj == null) { 60 obj = initialValue(); 61 mThreadLocal.set(new SoftReference<T>(obj)); 62 } 63 return obj; 64 } 65 } 66} 67 68class CanvasCache extends SoftReferenceThreadLocal<Canvas> { 69 @Override 70 protected Canvas initialValue() { 71 return new Canvas(); 72 } 73} 74 75class PaintCache extends SoftReferenceThreadLocal<Paint> { 76 @Override 77 protected Paint initialValue() { 78 return null; 79 } 80} 81 82class BitmapCache extends SoftReferenceThreadLocal<Bitmap> { 83 @Override 84 protected Bitmap initialValue() { 85 return null; 86 } 87} 88 89class RectCache extends SoftReferenceThreadLocal<Rect> { 90 @Override 91 protected Rect initialValue() { 92 return new Rect(); 93 } 94} 95 96class BitmapFactoryOptionsCache extends SoftReferenceThreadLocal<BitmapFactory.Options> { 97 @Override 98 protected BitmapFactory.Options initialValue() { 99 return new BitmapFactory.Options(); 100 } 101} 102 103public class WidgetPreviewLoader { 104 static final String TAG = "WidgetPreviewLoader"; 105 106 private int mPreviewBitmapWidth; 107 private int mPreviewBitmapHeight; 108 private String mSize; 109 private Context mContext; 110 private Launcher mLauncher; 111 private PackageManager mPackageManager; 112 private PagedViewCellLayout mWidgetSpacingLayout; 113 114 // Used for drawing shortcut previews 115 private BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache(); 116 private PaintCache mCachedShortcutPreviewPaint = new PaintCache(); 117 private CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache(); 118 119 // Used for drawing widget previews 120 private CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache(); 121 private RectCache mCachedAppWidgetPreviewSrcRect = new RectCache(); 122 private RectCache mCachedAppWidgetPreviewDestRect = new RectCache(); 123 private PaintCache mCachedAppWidgetPreviewPaint = new PaintCache(); 124 private String mCachedSelectQuery; 125 private BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache(); 126 127 private int mAppIconSize; 128 private IconCache mIconCache; 129 130 private final float sWidgetPreviewIconPaddingPercentage = 0.25f; 131 132 private WidgetPreviewCacheDb mDb; 133 134 private HashMap<String, WeakReference<Bitmap>> mLoadedPreviews; 135 private ArrayList<SoftReference<Bitmap>> mUnusedBitmaps; 136 private static HashSet<String> sInvalidPackages; 137 138 static { 139 sInvalidPackages = new HashSet<String>(); 140 } 141 142 public WidgetPreviewLoader(Launcher launcher) { 143 mContext = mLauncher = launcher; 144 mPackageManager = mContext.getPackageManager(); 145 mAppIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.app_icon_size); 146 mIconCache = ((LauncherApplication) launcher.getApplicationContext()).getIconCache(); 147 mDb = new WidgetPreviewCacheDb(mContext); 148 mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>(); 149 mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>(); 150 } 151 152 public void setPreviewSize(int previewWidth, int previewHeight, 153 PagedViewCellLayout widgetSpacingLayout) { 154 mPreviewBitmapWidth = previewWidth; 155 mPreviewBitmapHeight = previewHeight; 156 mSize = previewWidth + "x" + previewHeight; 157 mWidgetSpacingLayout = widgetSpacingLayout; 158 } 159 160 public Bitmap getPreview(final Object o) { 161 String name = getObjectName(o); 162 // check if the package is valid 163 boolean packageValid = true; 164 synchronized(sInvalidPackages) { 165 packageValid = !sInvalidPackages.contains(getObjectPackage(o)); 166 } 167 if (!packageValid) { 168 return null; 169 } 170 if (packageValid) { 171 synchronized(mLoadedPreviews) { 172 // check if it exists in our existing cache 173 if (mLoadedPreviews.containsKey(name) && mLoadedPreviews.get(name).get() != null) { 174 return mLoadedPreviews.get(name).get(); 175 } 176 } 177 } 178 179 Bitmap unusedBitmap = null; 180 synchronized(mUnusedBitmaps) { 181 // not in cache; we need to load it from the db 182 while ((unusedBitmap == null || !unusedBitmap.isMutable() || 183 unusedBitmap.getWidth() != mPreviewBitmapWidth || 184 unusedBitmap.getHeight() != mPreviewBitmapHeight) 185 && mUnusedBitmaps.size() > 0) { 186 unusedBitmap = mUnusedBitmaps.remove(0).get(); 187 } 188 if (unusedBitmap != null) { 189 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 190 c.setBitmap(unusedBitmap); 191 c.drawColor(0, PorterDuff.Mode.CLEAR); 192 c.setBitmap(null); 193 } 194 } 195 196 if (unusedBitmap == null) { 197 unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight, 198 Bitmap.Config.ARGB_8888); 199 } 200 201 Bitmap preview = null; 202 203 if (packageValid) { 204 preview = readFromDb(name, unusedBitmap); 205 } 206 207 if (preview != null) { 208 synchronized(mLoadedPreviews) { 209 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 210 } 211 return preview; 212 } else { 213 // it's not in the db... we need to generate it 214 final Bitmap generatedPreview = generatePreview(o, unusedBitmap); 215 preview = generatedPreview; 216 if (preview != unusedBitmap) { 217 throw new RuntimeException("generatePreview is not recycling the bitmap " + o); 218 } 219 220 synchronized(mLoadedPreviews) { 221 mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview)); 222 } 223 224 // write to db on a thread pool... this can be done lazily and improves the performance 225 // of the first time widget previews are loaded 226 new AsyncTask<Void, Void, Void>() { 227 public Void doInBackground(Void ... args) { 228 writeToDb(o, generatedPreview); 229 return null; 230 } 231 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 232 233 return preview; 234 } 235 } 236 237 public void recycleBitmap(Object o, Bitmap bitmapToRecycle) { 238 String name = getObjectName(o); 239 synchronized (mLoadedPreviews) { 240 if (mLoadedPreviews.containsKey(name)) { 241 Bitmap b = mLoadedPreviews.get(name).get(); 242 if (b == bitmapToRecycle) { 243 mLoadedPreviews.remove(name); 244 if (bitmapToRecycle.isMutable()) { 245 synchronized (mUnusedBitmaps) { 246 mUnusedBitmaps.add(new SoftReference<Bitmap>(b)); 247 } 248 } 249 } else { 250 throw new RuntimeException("Bitmap passed in doesn't match up"); 251 } 252 } 253 } 254 } 255 256 static class WidgetPreviewCacheDb extends SQLiteOpenHelper { 257 final static int DB_VERSION = 2; 258 final static String DB_NAME = "widgetpreviews.db"; 259 final static String TABLE_NAME = "shortcut_and_widget_previews"; 260 final static String COLUMN_NAME = "name"; 261 final static String COLUMN_SIZE = "size"; 262 final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap"; 263 Context mContext; 264 265 public WidgetPreviewCacheDb(Context context) { 266 super(context, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); 267 // Store the context for later use 268 mContext = context; 269 } 270 271 @Override 272 public void onCreate(SQLiteDatabase database) { 273 database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 274 COLUMN_NAME + " TEXT NOT NULL, " + 275 COLUMN_SIZE + " TEXT NOT NULL, " + 276 COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " + 277 "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " + 278 ");"); 279 } 280 281 @Override 282 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 283 if (oldVersion != newVersion) { 284 // Delete all the records; they'll be repopulated as this is a cache 285 db.execSQL("DELETE FROM " + TABLE_NAME); 286 } 287 } 288 } 289 290 private static final String WIDGET_PREFIX = "Widget:"; 291 private static final String SHORTCUT_PREFIX = "Shortcut:"; 292 293 private static String getObjectName(Object o) { 294 // should cache the string builder 295 StringBuilder sb = new StringBuilder(); 296 String output; 297 if (o instanceof AppWidgetProviderInfo) { 298 sb.append(WIDGET_PREFIX); 299 sb.append(((AppWidgetProviderInfo) o).provider.flattenToString()); 300 output = sb.toString(); 301 sb.setLength(0); 302 } else { 303 sb.append(SHORTCUT_PREFIX); 304 305 ResolveInfo info = (ResolveInfo) o; 306 sb.append(new ComponentName(info.activityInfo.packageName, 307 info.activityInfo.name).flattenToString()); 308 output = sb.toString(); 309 sb.setLength(0); 310 } 311 return output; 312 } 313 314 private String getObjectPackage(Object o) { 315 if (o instanceof AppWidgetProviderInfo) { 316 return ((AppWidgetProviderInfo) o).provider.getPackageName(); 317 } else { 318 ResolveInfo info = (ResolveInfo) o; 319 return info.activityInfo.packageName; 320 } 321 } 322 323 private void writeToDb(Object o, Bitmap preview) { 324 String name = getObjectName(o); 325 SQLiteDatabase db = mDb.getWritableDatabase(); 326 ContentValues values = new ContentValues(); 327 328 values.put(WidgetPreviewCacheDb.COLUMN_NAME, name); 329 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 330 preview.compress(Bitmap.CompressFormat.PNG, 100, stream); 331 values.put(WidgetPreviewCacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray()); 332 values.put(WidgetPreviewCacheDb.COLUMN_SIZE, mSize); 333 db.insert(WidgetPreviewCacheDb.TABLE_NAME, null, values); 334 } 335 336 public static void removeFromDb(final Context mContext, final String packageName) { 337 synchronized(sInvalidPackages) { 338 sInvalidPackages.add(packageName); 339 } 340 new AsyncTask<Void, Void, Void>() { 341 public Void doInBackground(Void ... args) { 342 SQLiteDatabase db = new WidgetPreviewCacheDb(mContext).getWritableDatabase(); 343 db.delete(WidgetPreviewCacheDb.TABLE_NAME, 344 WidgetPreviewCacheDb.COLUMN_NAME + " LIKE ? OR " + 345 WidgetPreviewCacheDb.COLUMN_NAME + " LIKE ?", // SELECT query 346 new String[] { 347 WIDGET_PREFIX + packageName + "/%", 348 SHORTCUT_PREFIX + packageName + "/%"} // args to SELECT query 349 ); 350 db.close(); 351 synchronized(sInvalidPackages) { 352 sInvalidPackages.remove(packageName); 353 } 354 return null; 355 } 356 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); 357 } 358 359 public void closeDb() { 360 if (mDb != null) { 361 mDb.close(); 362 } 363 } 364 365 private Bitmap readFromDb(String name, Bitmap b) { 366 if (mCachedSelectQuery == null) { 367 mCachedSelectQuery = WidgetPreviewCacheDb.COLUMN_NAME + " = ? AND " + 368 WidgetPreviewCacheDb.COLUMN_SIZE + " = ?"; 369 } 370 SQLiteDatabase db = mDb.getReadableDatabase(); 371 Cursor result = db.query(WidgetPreviewCacheDb.TABLE_NAME, 372 new String[] { WidgetPreviewCacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return 373 mCachedSelectQuery, // select query 374 new String[] { name, mSize }, // args to select query 375 null, 376 null, 377 null, 378 null); 379 if (result.getCount() > 0) { 380 result.moveToFirst(); 381 byte[] blob = result.getBlob(0); 382 result.close(); 383 final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get(); 384 opts.inBitmap = b; 385 opts.inSampleSize = 1; 386 Bitmap out = BitmapFactory.decodeByteArray(blob, 0, blob.length, opts); 387 return out; 388 } else { 389 result.close(); 390 return null; 391 } 392 } 393 394 public Bitmap generatePreview(Object info, Bitmap preview) { 395 if (preview != null && 396 (preview.getWidth() != mPreviewBitmapWidth || 397 preview.getHeight() != mPreviewBitmapHeight)) { 398 throw new RuntimeException("Improperly sized bitmap passed as argument"); 399 } 400 if (info instanceof AppWidgetProviderInfo) { 401 return generateWidgetPreview((AppWidgetProviderInfo) info, preview); 402 } else { 403 return generateShortcutPreview( 404 (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview); 405 } 406 } 407 408 public Bitmap generateWidgetPreview(AppWidgetProviderInfo info, Bitmap preview) { 409 int[] cellSpans = Launcher.getSpanForWidget(mLauncher, info); 410 int maxWidth = maxWidthForWidgetPreview(cellSpans[0]); 411 int maxHeight = maxHeightForWidgetPreview(cellSpans[1]); 412 return generateWidgetPreview(info.provider, info.previewImage, info.icon, 413 cellSpans[0], cellSpans[1], maxWidth, maxHeight, preview, null); 414 } 415 416 public int maxWidthForWidgetPreview(int spanX) { 417 return Math.min(mPreviewBitmapWidth, 418 mWidgetSpacingLayout.estimateCellWidth(spanX)); 419 } 420 421 public int maxHeightForWidgetPreview(int spanY) { 422 return Math.min(mPreviewBitmapHeight, 423 mWidgetSpacingLayout.estimateCellHeight(spanY)); 424 } 425 426 public Bitmap generateWidgetPreview(ComponentName provider, int previewImage, 427 int iconId, int cellHSpan, int cellVSpan, int maxPreviewWidth, int maxPreviewHeight, 428 Bitmap preview, int[] preScaledWidthOut) { 429 // Load the preview image if possible 430 String packageName = provider.getPackageName(); 431 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; 432 if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE; 433 434 Drawable drawable = null; 435 if (previewImage != 0) { 436 drawable = mPackageManager.getDrawable(packageName, previewImage, null); 437 if (drawable == null) { 438 Log.w(TAG, "Can't load widget preview drawable 0x" + 439 Integer.toHexString(previewImage) + " for provider: " + provider); 440 } 441 } 442 443 int previewWidth; 444 int previewHeight; 445 Bitmap defaultPreview = null; 446 boolean widgetPreviewExists = (drawable != null); 447 if (widgetPreviewExists) { 448 previewWidth = drawable.getIntrinsicWidth(); 449 previewHeight = drawable.getIntrinsicHeight(); 450 } else { 451 // Generate a preview image if we couldn't load one 452 if (cellHSpan < 1) cellHSpan = 1; 453 if (cellVSpan < 1) cellVSpan = 1; 454 455 BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources() 456 .getDrawable(R.drawable.widget_preview_tile); 457 final int previewDrawableWidth = previewDrawable 458 .getIntrinsicWidth(); 459 final int previewDrawableHeight = previewDrawable 460 .getIntrinsicHeight(); 461 previewWidth = previewDrawableWidth * cellHSpan; // subtract 2 dips 462 previewHeight = previewDrawableHeight * cellVSpan; 463 464 defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, 465 Config.ARGB_8888); 466 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 467 c.setBitmap(defaultPreview); 468 previewDrawable.setBounds(0, 0, previewWidth, previewHeight); 469 previewDrawable.setTileModeXY(Shader.TileMode.REPEAT, 470 Shader.TileMode.REPEAT); 471 previewDrawable.draw(c); 472 c.setBitmap(null); 473 474 // Draw the icon in the top left corner 475 int minOffset = (int) (mAppIconSize * sWidgetPreviewIconPaddingPercentage); 476 int smallestSide = Math.min(previewWidth, previewHeight); 477 float iconScale = Math.min((float) smallestSide 478 / (mAppIconSize + 2 * minOffset), 1f); 479 480 try { 481 Drawable icon = null; 482 int hoffset = 483 (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2); 484 int yoffset = 485 (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2); 486 if (iconId > 0) 487 icon = mIconCache.getFullResIcon(packageName, iconId); 488 if (icon != null) { 489 renderDrawableToBitmap(icon, defaultPreview, hoffset, 490 yoffset, (int) (mAppIconSize * iconScale), 491 (int) (mAppIconSize * iconScale)); 492 } 493 } catch (Resources.NotFoundException e) { 494 } 495 } 496 497 // Scale to fit width only - let the widget preview be clipped in the 498 // vertical dimension 499 float scale = 1f; 500 if (preScaledWidthOut != null) { 501 preScaledWidthOut[0] = previewWidth; 502 } 503 if (previewWidth > maxPreviewWidth) { 504 scale = maxPreviewWidth / (float) previewWidth; 505 } 506 if (scale != 1f) { 507 previewWidth = (int) (scale * previewWidth); 508 previewHeight = (int) (scale * previewHeight); 509 } 510 511 // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size 512 if (preview == null) { 513 preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888); 514 } 515 516 // Draw the scaled preview into the final bitmap 517 int x = (preview.getWidth() - previewWidth) / 2; 518 if (widgetPreviewExists) { 519 renderDrawableToBitmap(drawable, preview, x, 0, previewWidth, 520 previewHeight); 521 } else { 522 final Canvas c = mCachedAppWidgetPreviewCanvas.get(); 523 final Rect src = mCachedAppWidgetPreviewSrcRect.get(); 524 final Rect dest = mCachedAppWidgetPreviewDestRect.get(); 525 c.setBitmap(preview); 526 src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight()); 527 dest.set(x, 0, x + previewWidth, previewHeight); 528 529 Paint p = mCachedAppWidgetPreviewPaint.get(); 530 if (p == null) { 531 p = new Paint(); 532 p.setFilterBitmap(true); 533 mCachedAppWidgetPreviewPaint.set(p); 534 } 535 c.drawBitmap(defaultPreview, src, dest, p); 536 c.setBitmap(null); 537 } 538 return preview; 539 } 540 541 private Bitmap generateShortcutPreview( 542 ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) { 543 Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get(); 544 final Canvas c = mCachedShortcutPreviewCanvas.get(); 545 if (tempBitmap == null || 546 tempBitmap.getWidth() != maxWidth || 547 tempBitmap.getHeight() != maxHeight) { 548 tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 549 mCachedShortcutPreviewBitmap.set(tempBitmap); 550 } else { 551 c.setBitmap(tempBitmap); 552 c.drawColor(0, PorterDuff.Mode.CLEAR); 553 c.setBitmap(null); 554 } 555 // Render the icon 556 Drawable icon = mIconCache.getFullResIcon(info); 557 558 int paddingTop = mContext. 559 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top); 560 int paddingLeft = mContext. 561 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left); 562 int paddingRight = mContext. 563 getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right); 564 565 int scaledIconWidth = (maxWidth - paddingLeft - paddingRight); 566 567 renderDrawableToBitmap( 568 icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth); 569 570 if (preview != null && 571 (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) { 572 throw new RuntimeException("Improperly sized bitmap passed as argument"); 573 } else if (preview == null) { 574 preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888); 575 } 576 577 c.setBitmap(preview); 578 // Draw a desaturated/scaled version of the icon in the background as a watermark 579 Paint p = mCachedShortcutPreviewPaint.get(); 580 if (p == null) { 581 p = new Paint(); 582 ColorMatrix colorMatrix = new ColorMatrix(); 583 colorMatrix.setSaturation(0); 584 p.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); 585 p.setAlpha((int) (255 * 0.06f)); 586 mCachedShortcutPreviewPaint.set(p); 587 } 588 c.drawBitmap(tempBitmap, 0, 0, p); 589 c.setBitmap(null); 590 591 renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize); 592 593 return preview; 594 } 595 596 597 public static void renderDrawableToBitmap( 598 Drawable d, Bitmap bitmap, int x, int y, int w, int h) { 599 renderDrawableToBitmap(d, bitmap, x, y, w, h, 1f); 600 } 601 602 private static void renderDrawableToBitmap( 603 Drawable d, Bitmap bitmap, int x, int y, int w, int h, 604 float scale) { 605 if (bitmap != null) { 606 Canvas c = new Canvas(bitmap); 607 c.scale(scale, scale); 608 Rect oldBounds = d.copyBounds(); 609 d.setBounds(x, y, x + w, y + h); 610 d.draw(c); 611 d.setBounds(oldBounds); // Restore the bounds 612 c.setBitmap(null); 613 } 614 } 615 616} 617