1 /* 2 * Copyright (C) 2008 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 com.android.launcher3.dragndrop; 18 19 import static android.view.View.MeasureSpec.EXACTLY; 20 import static android.view.View.MeasureSpec.makeMeasureSpec; 21 22 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; 23 import static com.android.launcher3.Utilities.getBadge; 24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 25 26 import android.animation.AnimatorSet; 27 import android.animation.ObjectAnimator; 28 import android.animation.ValueAnimator; 29 import android.animation.ValueAnimator.AnimatorUpdateListener; 30 import android.annotation.TargetApi; 31 import android.content.Context; 32 import android.graphics.Bitmap; 33 import android.graphics.Canvas; 34 import android.graphics.Color; 35 import android.graphics.Path; 36 import android.graphics.Picture; 37 import android.graphics.Point; 38 import android.graphics.Rect; 39 import android.graphics.drawable.AdaptiveIconDrawable; 40 import android.graphics.drawable.ColorDrawable; 41 import android.graphics.drawable.Drawable; 42 import android.graphics.drawable.PictureDrawable; 43 import android.os.Build; 44 import android.os.Handler; 45 import android.os.Looper; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.widget.FrameLayout; 49 import android.widget.ImageView; 50 51 import androidx.annotation.Nullable; 52 import androidx.dynamicanimation.animation.FloatPropertyCompat; 53 import androidx.dynamicanimation.animation.SpringAnimation; 54 import androidx.dynamicanimation.animation.SpringForce; 55 56 import com.android.launcher3.Launcher; 57 import com.android.launcher3.LauncherSettings; 58 import com.android.launcher3.LauncherState; 59 import com.android.launcher3.R; 60 import com.android.launcher3.Utilities; 61 import com.android.launcher3.anim.Interpolators; 62 import com.android.launcher3.icons.FastBitmapDrawable; 63 import com.android.launcher3.icons.LauncherIcons; 64 import com.android.launcher3.model.data.ItemInfo; 65 import com.android.launcher3.statemanager.StateManager.StateListener; 66 import com.android.launcher3.util.RunnableList; 67 import com.android.launcher3.util.Thunk; 68 import com.android.launcher3.views.BaseDragLayer; 69 import com.android.launcher3.widget.LauncherAppWidgetHostView; 70 71 /** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */ 72 public class DragView extends FrameLayout implements StateListener<LauncherState> { 73 74 public static final int VIEW_ZOOM_DURATION = 150; 75 76 private final View mContent; 77 // The following are only used for rendering mContent directly during drag-n-drop. 78 @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams; 79 @Nullable private ViewGroup mContentViewParent; 80 private int mContentViewInParentViewIndex = -1; 81 private final int mWidth; 82 private final int mHeight; 83 84 private final int mBlurSizeOutline; 85 private final int mRegistrationX; 86 private final int mRegistrationY; 87 private final float mInitialScale; 88 private final float mScaleOnDrop; 89 private final int[] mTempLoc = new int[2]; 90 91 private final RunnableList mOnDragStartCallback = new RunnableList(); 92 93 private Point mDragVisualizeOffset = null; 94 private Rect mDragRegion = null; 95 private final Launcher mLauncher; 96 private final DragLayer mDragLayer; 97 @Thunk final DragController mDragController; 98 private boolean mHasDrawn = false; 99 100 final ValueAnimator mAnim; 101 102 private int mLastTouchX; 103 private int mLastTouchY; 104 private int mAnimatedShiftX; 105 private int mAnimatedShiftY; 106 107 // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true} 108 private Drawable mBgSpringDrawable, mFgSpringDrawable; 109 private SpringFloatValue mTranslateX, mTranslateY; 110 private Path mScaledMaskPath; 111 private Drawable mBadge; 112 DragView(Launcher launcher, Drawable drawable, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)113 public DragView(Launcher launcher, Drawable drawable, int registrationX, 114 int registrationY, final float initialScale, final float scaleOnDrop, 115 final float finalScaleDps) { 116 this(launcher, getViewFromDrawable(launcher, drawable), 117 drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), 118 registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps); 119 } 120 121 /** 122 * Construct the drag view. 123 * <p> 124 * The registration point is the point inside our view that the touch events should 125 * be centered upon. 126 * @param launcher The Launcher instance 127 * @param content the view content that is attached to the drag view. 128 * @param width the width of the dragView 129 * @param height the height of the dragView 130 * @param initialScale The view that we're dragging around. We scale it up when we draw it. 131 * @param registrationX The x coordinate of the registration point. 132 * @param registrationY The y coordinate of the registration point. 133 * @param scaleOnDrop the scale used in the drop animation. 134 * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown. 135 */ DragView(Launcher launcher, View content, int width, int height, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)136 public DragView(Launcher launcher, View content, int width, int height, int registrationX, 137 int registrationY, final float initialScale, final float scaleOnDrop, 138 final float finalScaleDps) { 139 super(launcher); 140 mLauncher = launcher; 141 mDragLayer = launcher.getDragLayer(); 142 mDragController = launcher.getDragController(); 143 144 mContent = content; 145 mWidth = width; 146 mHeight = height; 147 mContentViewLayoutParams = mContent.getLayoutParams(); 148 if (mContent.getParent() instanceof ViewGroup) { 149 mContentViewParent = (ViewGroup) mContent.getParent(); 150 mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent); 151 mContentViewParent.removeView(mContent); 152 } 153 154 addView(content, new LayoutParams(width, height)); 155 156 final float scale = (width + finalScaleDps) / width; 157 158 // Set the initial scale to avoid any jumps 159 setScaleX(initialScale); 160 setScaleY(initialScale); 161 162 // Animate the view into the correct position 163 mAnim = ValueAnimator.ofFloat(0f, 1f); 164 mAnim.setDuration(VIEW_ZOOM_DURATION); 165 mAnim.addUpdateListener(animation -> { 166 final float value = (Float) animation.getAnimatedValue(); 167 setScaleX(initialScale + (value * (scale - initialScale))); 168 setScaleY(initialScale + (value * (scale - initialScale))); 169 if (!isAttachedToWindow()) { 170 animation.cancel(); 171 } 172 }); 173 174 setDragRegion(new Rect(0, 0, width, height)); 175 176 // The point in our scaled bitmap that the touch events are located 177 mRegistrationX = registrationX; 178 mRegistrationY = registrationY; 179 180 mInitialScale = initialScale; 181 mScaleOnDrop = scaleOnDrop; 182 183 // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass 184 measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); 185 186 mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 187 setElevation(getResources().getDimension(R.dimen.drag_elevation)); 188 setWillNotDraw(false); 189 } 190 191 @Override onAttachedToWindow()192 protected void onAttachedToWindow() { 193 super.onAttachedToWindow(); 194 mLauncher.getStateManager().addStateListener(this); 195 } 196 197 @Override onDetachedFromWindow()198 protected void onDetachedFromWindow() { 199 super.onDetachedFromWindow(); 200 mLauncher.getStateManager().removeStateListener(this); 201 } 202 203 @Override onStateTransitionComplete(LauncherState finalState)204 public void onStateTransitionComplete(LauncherState finalState) { 205 setVisibility((finalState == LauncherState.NORMAL 206 || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE); 207 } 208 209 /** 210 * Initialize {@code #mIconDrawable} if the item can be represented using 211 * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}. 212 */ 213 @TargetApi(Build.VERSION_CODES.O) setItemInfo(final ItemInfo info)214 public void setItemInfo(final ItemInfo info) { 215 if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION 216 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT 217 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 218 return; 219 } 220 // Load the adaptive icon on a background thread and add the view in ui thread. 221 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> { 222 Object[] outObj = new Object[1]; 223 int w = mWidth; 224 int h = mHeight; 225 Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj); 226 227 if (dr instanceof AdaptiveIconDrawable) { 228 int blurMargin = (int) mLauncher.getResources() 229 .getDimension(R.dimen.blur_size_medium_outline) / 2; 230 231 Rect bounds = new Rect(0, 0, w, h); 232 bounds.inset(blurMargin, blurMargin); 233 // Badge is applied after icon normalization so the bounds for badge should not 234 // be scaled down due to icon normalization. 235 Rect badgeBounds = new Rect(bounds); 236 mBadge = getBadge(mLauncher, info, outObj[0]); 237 mBadge.setBounds(badgeBounds); 238 239 // Do not draw the background in case of folder as its translucent 240 final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon); 241 242 try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) { 243 Drawable nDr; // drawable to be normalized 244 if (shouldDrawBackground) { 245 nDr = dr; 246 } else { 247 // Since we just want the scale, avoid heavy drawing operations 248 nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); 249 } 250 Utilities.scaleRectAboutCenter(bounds, 251 li.getNormalizer().getScale(nDr, null, null, null)); 252 } 253 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr; 254 255 // Shrink very tiny bit so that the clip path is smaller than the original bitmap 256 // that has anti aliased edges and shadows. 257 Rect shrunkBounds = new Rect(bounds); 258 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f); 259 adaptiveIcon.setBounds(shrunkBounds); 260 final Path mask = adaptiveIcon.getIconMask(); 261 262 mTranslateX = new SpringFloatValue(DragView.this, 263 w * AdaptiveIconDrawable.getExtraInsetFraction()); 264 mTranslateY = new SpringFloatValue(DragView.this, 265 h * AdaptiveIconDrawable.getExtraInsetFraction()); 266 267 bounds.inset( 268 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 269 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 270 ); 271 mBgSpringDrawable = adaptiveIcon.getBackground(); 272 if (mBgSpringDrawable == null) { 273 mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 274 } 275 mBgSpringDrawable.setBounds(bounds); 276 mFgSpringDrawable = adaptiveIcon.getForeground(); 277 if (mFgSpringDrawable == null) { 278 mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 279 } 280 mFgSpringDrawable.setBounds(bounds); 281 282 new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> { 283 // TODO: Consider fade-in animation 284 // Assign the variable on the UI thread to avoid race conditions. 285 mScaledMaskPath = mask; 286 // Avoid relayout as we do not care about children affecting layout 287 removeAllViewsInLayout(); 288 289 if (info.isDisabled()) { 290 FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); 291 d.setIsDisabled(true); 292 mBgSpringDrawable.setColorFilter(d.getColorFilter()); 293 mFgSpringDrawable.setColorFilter(d.getColorFilter()); 294 mBadge.setColorFilter(d.getColorFilter()); 295 } 296 invalidate(); 297 })); 298 } 299 }); 300 } 301 302 /** 303 * Called when pre-drag finishes for an icon 304 */ onDragStart()305 public void onDragStart() { 306 mOnDragStartCallback.executeAllAndDestroy(); 307 } 308 309 // TODO(b/183609936): This is only for LauncherAppWidgetHostView that is rendered in a drawable. 310 // Once LauncherAppWidgetHostView is directly rendered in this view, removes this method. 311 @Override invalidate()312 public void invalidate() { 313 super.invalidate(); 314 if (mContent instanceof ImageView) { 315 mContent.invalidate(); 316 } 317 } 318 319 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)320 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 321 super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 322 } 323 getDragRegionWidth()324 public int getDragRegionWidth() { 325 return mDragRegion.width(); 326 } 327 getDragRegionHeight()328 public int getDragRegionHeight() { 329 return mDragRegion.height(); 330 } 331 setDragVisualizeOffset(Point p)332 public void setDragVisualizeOffset(Point p) { 333 mDragVisualizeOffset = p; 334 } 335 getDragVisualizeOffset()336 public Point getDragVisualizeOffset() { 337 return mDragVisualizeOffset; 338 } 339 setDragRegion(Rect r)340 public void setDragRegion(Rect r) { 341 mDragRegion = r; 342 } 343 getDragRegion()344 public Rect getDragRegion() { 345 return mDragRegion; 346 } 347 348 @Override draw(Canvas canvas)349 public void draw(Canvas canvas) { 350 super.draw(canvas); 351 352 // Draw after the content 353 mHasDrawn = true; 354 if (mScaledMaskPath != null) { 355 int cnt = canvas.save(); 356 canvas.clipPath(mScaledMaskPath); 357 mBgSpringDrawable.draw(canvas); 358 canvas.translate(mTranslateX.mValue, mTranslateY.mValue); 359 mFgSpringDrawable.draw(canvas); 360 canvas.restoreToCount(cnt); 361 mBadge.draw(canvas); 362 } 363 } 364 crossFadeContent(Drawable crossFadeDrawable, int duration)365 public void crossFadeContent(Drawable crossFadeDrawable, int duration) { 366 if (mContent.getParent() == null) { 367 // If the content is already removed, ignore 368 return; 369 } 370 View newContent = getViewFromDrawable(getContext(), crossFadeDrawable); 371 newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 372 newContent.layout(0, 0, mWidth, mHeight); 373 addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight)); 374 375 AnimatorSet anim = new AnimatorSet(); 376 anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1)); 377 anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0)); 378 anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5); 379 anim.start(); 380 } 381 hasDrawn()382 public boolean hasDrawn() { 383 return mHasDrawn; 384 } 385 386 /** 387 * Create a window containing this view and show it. 388 * 389 * @param touchX the x coordinate the user touched in DragLayer coordinates 390 * @param touchY the y coordinate the user touched in DragLayer coordinates 391 */ show(int touchX, int touchY)392 public void show(int touchX, int touchY) { 393 mDragLayer.addView(this); 394 395 // Start the pick-up animation 396 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight); 397 lp.customPosition = true; 398 setLayoutParams(lp); 399 400 if (mContent != null) { 401 // At the drag start, the source view visibility is set to invisible. 402 mContent.setVisibility(VISIBLE); 403 } 404 405 move(touchX, touchY); 406 // Post the animation to skip other expensive work happening on the first frame 407 post(mAnim::start); 408 } 409 cancelAnimation()410 public void cancelAnimation() { 411 if (mAnim != null && mAnim.isRunning()) { 412 mAnim.cancel(); 413 } 414 } 415 416 /** 417 * Move the window containing this view. 418 * 419 * @param touchX the x coordinate the user touched in DragLayer coordinates 420 * @param touchY the y coordinate the user touched in DragLayer coordinates 421 */ move(int touchX, int touchY)422 public void move(int touchX, int touchY) { 423 if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0 424 && mScaledMaskPath != null) { 425 mTranslateX.animateToPos(mLastTouchX - touchX); 426 mTranslateY.animateToPos(mLastTouchY - touchY); 427 } 428 mLastTouchX = touchX; 429 mLastTouchY = touchY; 430 applyTranslation(); 431 } 432 animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)433 public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) { 434 mTempLoc[0] = toTouchX - mRegistrationX; 435 mTempLoc[1] = toTouchY - mRegistrationY; 436 mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop, 437 DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); 438 } 439 animateShift(final int shiftX, final int shiftY)440 public void animateShift(final int shiftX, final int shiftY) { 441 if (mAnim.isStarted()) { 442 return; 443 } 444 mAnimatedShiftX = shiftX; 445 mAnimatedShiftY = shiftY; 446 applyTranslation(); 447 mAnim.addUpdateListener(new AnimatorUpdateListener() { 448 @Override 449 public void onAnimationUpdate(ValueAnimator animation) { 450 float fraction = 1 - animation.getAnimatedFraction(); 451 mAnimatedShiftX = (int) (fraction * shiftX); 452 mAnimatedShiftY = (int) (fraction * shiftY); 453 applyTranslation(); 454 } 455 }); 456 } 457 applyTranslation()458 private void applyTranslation() { 459 setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX); 460 setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY); 461 } 462 463 /** 464 * Detaches {@link #mContent}, if previously attached, from this view. 465 * 466 * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to 467 * {@code true} to attach the {@link #mContent} back to its previous parent. 468 */ detachContentView(boolean reattachToPreviousParent)469 public void detachContentView(boolean reattachToPreviousParent) { 470 if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) { 471 Picture picture = new Picture(); 472 mContent.draw(picture.beginRecording(mWidth, mHeight)); 473 picture.endRecording(); 474 View view = new View(mLauncher); 475 view.setBackground(new PictureDrawable(picture)); 476 view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 477 view.layout(mContent.getLeft(), mContent.getTop(), 478 mContent.getRight(), mContent.getBottom()); 479 setClipToOutline(mContent.getClipToOutline()); 480 setOutlineProvider(mContent.getOutlineProvider()); 481 addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true); 482 483 removeViewInLayout(mContent); 484 mContent.setVisibility(INVISIBLE); 485 mContent.setLayoutParams(mContentViewLayoutParams); 486 if (reattachToPreviousParent) { 487 mContentViewParent.addView(mContent, mContentViewInParentViewIndex); 488 } 489 mContentViewParent = null; 490 mContentViewInParentViewIndex = -1; 491 } 492 } 493 494 /** 495 * If the drag view uses color extraction, block it. 496 */ disableColorExtraction()497 public void disableColorExtraction() { 498 if (mContent instanceof LauncherAppWidgetHostView) { 499 ((LauncherAppWidgetHostView) mContent).disableColorExtraction(); 500 } 501 } 502 503 /** 504 * If the drag view uses color extraction, restores it. 505 */ resumeColorExtraction()506 public void resumeColorExtraction() { 507 if (mContent instanceof LauncherAppWidgetHostView) { 508 ((LauncherAppWidgetHostView) mContent).enableColorExtraction(/* updateColors= */ false); 509 } 510 } 511 512 /** 513 * Removes this view from the {@link DragLayer}. 514 * 515 * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the 516 * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller 517 * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true 518 * before this call. 519 */ remove()520 public void remove() { 521 if (getParent() != null) { 522 mDragLayer.removeView(DragView.this); 523 } 524 } 525 getBlurSizeOutline()526 public int getBlurSizeOutline() { 527 return mBlurSizeOutline; 528 } 529 getInitialScale()530 public float getInitialScale() { 531 return mInitialScale; 532 } 533 534 @Override hasOverlappingRendering()535 public boolean hasOverlappingRendering() { 536 return false; 537 } 538 539 /** Returns the current content view that is rendered in the drag view. */ getContentView()540 public View getContentView() { 541 return mContent; 542 } 543 544 /** 545 * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag 546 * content is attached to this view. 547 */ 548 @Nullable getContentViewParent()549 public ViewGroup getContentViewParent() { 550 return mContentViewParent; 551 } 552 553 private static class SpringFloatValue { 554 555 private static final FloatPropertyCompat<SpringFloatValue> VALUE = 556 new FloatPropertyCompat<SpringFloatValue>("value") { 557 @Override 558 public float getValue(SpringFloatValue object) { 559 return object.mValue; 560 } 561 562 @Override 563 public void setValue(SpringFloatValue object, float value) { 564 object.mValue = value; 565 object.mView.invalidate(); 566 } 567 }; 568 569 // Following three values are fine tuned with motion ux designer 570 private static final int STIFFNESS = 4000; 571 private static final float DAMPENING_RATIO = 1f; 572 private static final int PARALLAX_MAX_IN_DP = 8; 573 574 private final View mView; 575 private final SpringAnimation mSpring; 576 private final float mDelta; 577 578 private float mValue; 579 SpringFloatValue(View view, float range)580 public SpringFloatValue(View view, float range) { 581 mView = view; 582 mSpring = new SpringAnimation(this, VALUE, 0) 583 .setMinValue(-range).setMaxValue(range) 584 .setSpring(new SpringForce(0) 585 .setDampingRatio(DAMPENING_RATIO) 586 .setStiffness(STIFFNESS)); 587 mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP; 588 } 589 animateToPos(float value)590 public void animateToPos(float value) { 591 mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta)); 592 } 593 } 594 getViewFromDrawable(Context context, Drawable drawable)595 private static View getViewFromDrawable(Context context, Drawable drawable) { 596 ImageView iv = new ImageView(context); 597 iv.setImageDrawable(drawable); 598 return iv; 599 } 600 } 601