1 /* 2 * Copyright (C) 2013 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.mail.ui; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.PorterDuff.Mode; 26 import android.graphics.PorterDuffXfermode; 27 import android.graphics.Rect; 28 29 import com.android.mail.R; 30 import com.android.mail.utils.Utils; 31 import com.google.common.collect.Lists; 32 import com.google.common.collect.Maps; 33 34 import java.util.ArrayList; 35 import java.util.List; 36 import java.util.Map; 37 38 /** 39 * DividedImageCanvas creates a canvas that can display into a minimum of 1 40 * and maximum of 4 images. As images are added, they 41 * are laid out according to the following algorithm: 42 * 1 Image: Draw the bitmap filling the entire canvas. 43 * 2 Images: Draw 2 bitmaps split vertically down the middle. 44 * 3 Images: Draw 3 bitmaps: the first takes up all vertical space; the 2nd and 3rd are stacked in 45 * the second vertical position. 46 * 4 Images: Divide the Canvas into 4 equal quadrants and draws 1 bitmap in each. 47 */ 48 public class DividedImageCanvas implements ImageCanvas { 49 public static final int MAX_DIVISIONS = 4; 50 51 private final Map<String, Integer> mDivisionMap = Maps 52 .newHashMapWithExpectedSize(MAX_DIVISIONS); 53 private Bitmap mDividedBitmap; 54 private Canvas mCanvas; 55 private int mWidth; 56 private int mHeight; 57 58 private final Context mContext; 59 private final InvalidateCallback mCallback; 60 private final ArrayList<Bitmap> mDivisionImages = new ArrayList<Bitmap>(MAX_DIVISIONS); 61 62 /** 63 * Ignore any request to draw final output when not yet ready. This prevents partially drawn 64 * canvases from appearing. 65 */ 66 private boolean mBitmapValid = false; 67 68 private int mGeneration; 69 70 private static final Paint sPaint = new Paint(); 71 private static final Paint sClearPaint = new Paint(); 72 private static final Rect sSrc = new Rect(); 73 private static final Rect sDest = new Rect(); 74 75 private static int sDividerLineWidth = -1; 76 private static int sDividerColor; 77 78 static { 79 sClearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR)); 80 } 81 82 public DividedImageCanvas(Context context, InvalidateCallback callback) { 83 mContext = context; 84 mCallback = callback; 85 setupDividerLines(); 86 } 87 88 /** 89 * Get application context for this object. 90 */ 91 public Context getContext() { 92 return mContext; 93 } 94 95 @Override 96 public String toString() { 97 final StringBuilder sb = new StringBuilder("{"); 98 sb.append(super.toString()); 99 sb.append(" mDivisionMap="); 100 sb.append(mDivisionMap); 101 sb.append(" mDivisionImages="); 102 sb.append(mDivisionImages); 103 sb.append(" mWidth="); 104 sb.append(mWidth); 105 sb.append(" mHeight="); 106 sb.append(mHeight); 107 sb.append("}"); 108 return sb.toString(); 109 } 110 111 /** 112 * Set the id associated with each quadrant. The quadrants are laid out: 113 * TopLeft, TopRight, Bottom Left, Bottom Right 114 * @param keys 115 */ 116 public void setDivisionIds(List<Object> keys) { 117 if (keys.size() > MAX_DIVISIONS) { 118 throw new IllegalArgumentException("too many divisionIds: " + keys); 119 } 120 121 boolean needClear = getDivisionCount() != keys.size(); 122 if (!needClear) { 123 for (int i = 0; i < keys.size(); i++) { 124 String divisionId = transformKeyToDivisionId(keys.get(i)); 125 // different item or different place 126 if (!mDivisionMap.containsKey(divisionId) || mDivisionMap.get(divisionId) != i) { 127 needClear = true; 128 break; 129 } 130 } 131 } 132 133 if (needClear) { 134 mDivisionMap.clear(); 135 mDivisionImages.clear(); 136 int i = 0; 137 for (Object key : keys) { 138 String divisionId = transformKeyToDivisionId(key); 139 mDivisionMap.put(divisionId, i); 140 mDivisionImages.add(null); 141 i++; 142 } 143 } 144 } 145 146 private void draw(Bitmap b, int left, int top, int right, int bottom) { 147 if (b != null) { 148 // Some times we load taller images compared to the destination rect on the canvas 149 int srcTop = 0; 150 int srcBottom = b.getHeight(); 151 int destHeight = bottom - top; 152 if (b.getHeight() > bottom - top) { 153 srcTop = b.getHeight() / 2 - destHeight/2; 154 srcBottom = b.getHeight() / 2 + destHeight/2; 155 } 156 157 // todo:markwei do not scale very small bitmaps 158 // l t r b 159 sSrc.set(0, srcTop, b.getWidth(), srcBottom); 160 sDest.set(left, top, right, bottom); 161 mCanvas.drawRect(sDest, sClearPaint); 162 mCanvas.drawBitmap(b, sSrc, sDest, sPaint); 163 } else { 164 // clear 165 mCanvas.drawRect(left, top, right, bottom, sClearPaint); 166 } 167 } 168 169 /** 170 * Get the desired dimensions and scale for the bitmap to be placed in the 171 * location corresponding to id. Caller must allocate the Dimensions object. 172 * @param key 173 * @param outDim a {@link ImageCanvas.Dimensions} object to write results into 174 */ 175 @Override 176 public void getDesiredDimensions(Object key, Dimensions outDim) { 177 Utils.traceBeginSection("get desired dimensions"); 178 int w = 0, h = 0; 179 float scale = 0; 180 final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); 181 if (pos != null && pos >= 0) { 182 final int size = mDivisionMap.size(); 183 switch (size) { 184 case 0: 185 break; 186 case 1: 187 w = mWidth; 188 h = mHeight; 189 scale = Dimensions.SCALE_ONE; 190 break; 191 case 2: 192 w = mWidth / 2; 193 h = mHeight; 194 scale = Dimensions.SCALE_HALF; 195 break; 196 case 3: 197 switch (pos) { 198 case 0: 199 w = mWidth / 2; 200 h = mHeight; 201 scale = Dimensions.SCALE_HALF; 202 break; 203 default: 204 w = mWidth / 2; 205 h = mHeight / 2; 206 scale = Dimensions.SCALE_QUARTER; 207 } 208 break; 209 case 4: 210 w = mWidth / 2; 211 h = mHeight / 2; 212 scale = Dimensions.SCALE_QUARTER; 213 break; 214 } 215 } 216 outDim.width = w; 217 outDim.height = h; 218 outDim.scale = scale; 219 Utils.traceEndSection(); 220 } 221 222 @Override 223 public void drawImage(Bitmap b, Object key) { 224 addDivisionImage(b, key); 225 } 226 227 /** 228 * Add a bitmap to this view in the quadrant matching its id. 229 * @param b Bitmap 230 * @param key Id to look for that was previously set in setDivisionIds. 231 */ 232 public void addDivisionImage(Bitmap b, Object key) { 233 if (b != null) { 234 addOrClearDivisionImage(b, key); 235 } 236 } 237 238 public void clearDivisionImage(Object key) { 239 addOrClearDivisionImage(null, key); 240 } 241 private void addOrClearDivisionImage(Bitmap b, Object key) { 242 Utils.traceBeginSection("add or clear division image"); 243 final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); 244 if (pos != null && pos >= 0) { 245 mDivisionImages.set(pos, b); 246 boolean complete = false; 247 final int width = mWidth; 248 final int height = mHeight; 249 // Different layouts depending on count. 250 final int size = mDivisionMap.size(); 251 switch (size) { 252 case 0: 253 // Do nothing. 254 break; 255 case 1: 256 // Draw the bitmap filling the entire canvas. 257 draw(mDivisionImages.get(0), 0, 0, width, height); 258 complete = true; 259 break; 260 case 2: 261 // Draw 2 bitmaps split vertically down the middle 262 switch (pos) { 263 case 0: 264 draw(mDivisionImages.get(0), 0, 0, width / 2, height); 265 break; 266 case 1: 267 draw(mDivisionImages.get(1), width / 2, 0, width, height); 268 break; 269 } 270 complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null 271 || isPartialBitmapComplete(); 272 if (complete) { 273 // Draw dividers 274 drawVerticalDivider(width, height); 275 } 276 break; 277 case 3: 278 // Draw 3 bitmaps: the first takes up all vertical 279 // space, the 2nd and 3rd are stacked in the second vertical 280 // position. 281 switch (pos) { 282 case 0: 283 draw(mDivisionImages.get(0), 0, 0, width / 2, height); 284 break; 285 case 1: 286 draw(mDivisionImages.get(1), width / 2, 0, width, height / 2); 287 break; 288 case 2: 289 draw(mDivisionImages.get(2), width / 2, height / 2, width, height); 290 break; 291 } 292 complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null 293 && mDivisionImages.get(2) != null || isPartialBitmapComplete(); 294 if (complete) { 295 // Draw dividers 296 drawVerticalDivider(width, height); 297 drawHorizontalDivider(width / 2, height / 2, width, height / 2); 298 } 299 break; 300 default: 301 // Draw all 4 bitmaps in a grid 302 switch (pos) { 303 case 0: 304 draw(mDivisionImages.get(0), 0, 0, width / 2, height / 2); 305 break; 306 case 1: 307 draw(mDivisionImages.get(1), width / 2, 0, width, height / 2); 308 break; 309 case 2: 310 draw(mDivisionImages.get(2), 0, height / 2, width / 2, height); 311 break; 312 case 3: 313 draw(mDivisionImages.get(3), width / 2, height / 2, width, height); 314 break; 315 } 316 complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null 317 && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null 318 || isPartialBitmapComplete(); 319 if (complete) { 320 // Draw dividers 321 drawVerticalDivider(width, height); 322 drawHorizontalDivider(0, height / 2, width, height / 2); 323 } 324 break; 325 } 326 // Create the new image bitmap. 327 if (complete) { 328 mBitmapValid = true; 329 mCallback.invalidate(); 330 } 331 } 332 Utils.traceEndSection(); 333 } 334 335 public boolean hasImageFor(Object key) { 336 final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); 337 return pos != null && mDivisionImages.get(pos) != null; 338 } 339 340 private void setupDividerLines() { 341 if (sDividerLineWidth == -1) { 342 Resources res = getContext().getResources(); 343 sDividerLineWidth = res 344 .getDimensionPixelSize(R.dimen.tile_divider_width); 345 sDividerColor = res.getColor(R.color.tile_divider_color); 346 } 347 } 348 349 private static void setupPaint() { 350 sPaint.setStrokeWidth(sDividerLineWidth); 351 sPaint.setColor(sDividerColor); 352 } 353 354 protected void drawVerticalDivider(int width, int height) { 355 int x1 = width / 2, y1 = 0, x2 = width/2, y2 = height; 356 setupPaint(); 357 mCanvas.drawLine(x1, y1, x2, y2, sPaint); 358 } 359 360 protected void drawHorizontalDivider(int x1, int y1, int x2, int y2) { 361 setupPaint(); 362 mCanvas.drawLine(x1, y1, x2, y2, sPaint); 363 } 364 365 protected boolean isPartialBitmapComplete() { 366 return false; 367 } 368 369 protected String transformKeyToDivisionId(Object key) { 370 return key.toString(); 371 } 372 373 /** 374 * Draw the contents of the DividedImageCanvas to the supplied canvas. 375 */ 376 public void draw(Canvas canvas) { 377 if (mDividedBitmap != null && mBitmapValid) { 378 canvas.drawBitmap(mDividedBitmap, 0, 0, null); 379 } 380 } 381 382 /** 383 * Draw the contents of the DividedImageCanvas to the supplied canvas. 384 */ 385 public void draw(final Canvas canvas, final Matrix matrix) { 386 if (mDividedBitmap != null && mBitmapValid) { 387 canvas.drawBitmap(mDividedBitmap, matrix, null); 388 } 389 } 390 391 @Override 392 public void reset() { 393 if (mCanvas != null && mDividedBitmap != null) { 394 mBitmapValid = false; 395 } 396 mDivisionMap.clear(); 397 mDivisionImages.clear(); 398 mGeneration++; 399 } 400 401 @Override 402 public int getGeneration() { 403 return mGeneration; 404 } 405 406 /** 407 * Set the width and height of the canvas. 408 * @param width 409 * @param height 410 */ 411 public void setDimensions(int width, int height) { 412 Utils.traceBeginSection("set dimensions"); 413 if (mWidth == width && mHeight == height) { 414 Utils.traceEndSection(); 415 return; 416 } 417 418 mWidth = width; 419 mHeight = height; 420 421 mDividedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 422 mCanvas = new Canvas(mDividedBitmap); 423 424 for (int i = 0; i < getDivisionCount(); i++) { 425 mDivisionImages.set(i, null); 426 } 427 mBitmapValid = false; 428 Utils.traceEndSection(); 429 } 430 431 /** 432 * Get the resulting canvas width. 433 */ 434 public int getWidth() { 435 return mWidth; 436 } 437 438 /** 439 * Get the resulting canvas height. 440 */ 441 public int getHeight() { 442 return mHeight; 443 } 444 445 /** 446 * The class that will provided the canvas to which the DividedImageCanvas 447 * should render its contents must implement this interface. 448 */ 449 public interface InvalidateCallback { 450 public void invalidate(); 451 } 452 453 public int getDivisionCount() { 454 return mDivisionMap.size(); 455 } 456 457 /** 458 * Get the division ids currently associated with this DivisionImageCanvas. 459 */ 460 public ArrayList<String> getDivisionIds() { 461 return Lists.newArrayList(mDivisionMap.keySet()); 462 } 463 } 464