/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mail.ui; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import com.android.mail.R; import com.android.mail.utils.Utils; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * DividedImageCanvas creates a canvas that can display into a minimum of 1 * and maximum of 4 images. As images are added, they * are laid out according to the following algorithm: * 1 Image: Draw the bitmap filling the entire canvas. * 2 Images: Draw 2 bitmaps split vertically down the middle. * 3 Images: Draw 3 bitmaps: the first takes up all vertical space; the 2nd and 3rd are stacked in * the second vertical position. * 4 Images: Divide the Canvas into 4 equal quadrants and draws 1 bitmap in each. */ public class DividedImageCanvas implements ImageCanvas { public static final int MAX_DIVISIONS = 4; private final Map mDivisionMap = Maps .newHashMapWithExpectedSize(MAX_DIVISIONS); private Bitmap mDividedBitmap; private Canvas mCanvas; private int mWidth; private int mHeight; private final Context mContext; private final InvalidateCallback mCallback; private final ArrayList mDivisionImages = new ArrayList(MAX_DIVISIONS); /** * Ignore any request to draw final output when not yet ready. This prevents partially drawn * canvases from appearing. */ private boolean mBitmapValid = false; private int mGeneration; private static final Paint sPaint = new Paint(); private static final Paint sClearPaint = new Paint(); private static final Rect sSrc = new Rect(); private static final Rect sDest = new Rect(); private static int sDividerLineWidth = -1; private static int sDividerColor; static { sClearPaint.setXfermode(new PorterDuffXfermode(Mode.CLEAR)); } public DividedImageCanvas(Context context, InvalidateCallback callback) { mContext = context; mCallback = callback; setupDividerLines(); } /** * Get application context for this object. */ public Context getContext() { return mContext; } @Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append(super.toString()); sb.append(" mDivisionMap="); sb.append(mDivisionMap); sb.append(" mDivisionImages="); sb.append(mDivisionImages); sb.append(" mWidth="); sb.append(mWidth); sb.append(" mHeight="); sb.append(mHeight); sb.append("}"); return sb.toString(); } /** * Set the id associated with each quadrant. The quadrants are laid out: * TopLeft, TopRight, Bottom Left, Bottom Right * @param keys */ public void setDivisionIds(List keys) { if (keys.size() > MAX_DIVISIONS) { throw new IllegalArgumentException("too many divisionIds: " + keys); } boolean needClear = getDivisionCount() != keys.size(); if (!needClear) { for (int i = 0; i < keys.size(); i++) { String divisionId = transformKeyToDivisionId(keys.get(i)); // different item or different place if (!mDivisionMap.containsKey(divisionId) || mDivisionMap.get(divisionId) != i) { needClear = true; break; } } } if (needClear) { mDivisionMap.clear(); mDivisionImages.clear(); int i = 0; for (Object key : keys) { String divisionId = transformKeyToDivisionId(key); mDivisionMap.put(divisionId, i); mDivisionImages.add(null); i++; } } } private void draw(Bitmap b, int left, int top, int right, int bottom) { if (b != null) { // Some times we load taller images compared to the destination rect on the canvas int srcTop = 0; int srcBottom = b.getHeight(); int destHeight = bottom - top; if (b.getHeight() > bottom - top) { srcTop = b.getHeight() / 2 - destHeight/2; srcBottom = b.getHeight() / 2 + destHeight/2; } // todo:markwei do not scale very small bitmaps // l t r b sSrc.set(0, srcTop, b.getWidth(), srcBottom); sDest.set(left, top, right, bottom); mCanvas.drawRect(sDest, sClearPaint); mCanvas.drawBitmap(b, sSrc, sDest, sPaint); } else { // clear mCanvas.drawRect(left, top, right, bottom, sClearPaint); } } /** * Get the desired dimensions and scale for the bitmap to be placed in the * location corresponding to id. Caller must allocate the Dimensions object. * @param key * @param outDim a {@link ImageCanvas.Dimensions} object to write results into */ @Override public void getDesiredDimensions(Object key, Dimensions outDim) { Utils.traceBeginSection("get desired dimensions"); int w = 0, h = 0; float scale = 0; final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); if (pos != null && pos >= 0) { final int size = mDivisionMap.size(); switch (size) { case 0: break; case 1: w = mWidth; h = mHeight; scale = Dimensions.SCALE_ONE; break; case 2: w = mWidth / 2; h = mHeight; scale = Dimensions.SCALE_HALF; break; case 3: switch (pos) { case 0: w = mWidth / 2; h = mHeight; scale = Dimensions.SCALE_HALF; break; default: w = mWidth / 2; h = mHeight / 2; scale = Dimensions.SCALE_QUARTER; } break; case 4: w = mWidth / 2; h = mHeight / 2; scale = Dimensions.SCALE_QUARTER; break; } } outDim.width = w; outDim.height = h; outDim.scale = scale; Utils.traceEndSection(); } @Override public void drawImage(Bitmap b, Object key) { addDivisionImage(b, key); } /** * Add a bitmap to this view in the quadrant matching its id. * @param b Bitmap * @param key Id to look for that was previously set in setDivisionIds. */ public void addDivisionImage(Bitmap b, Object key) { if (b != null) { addOrClearDivisionImage(b, key); } } public void clearDivisionImage(Object key) { addOrClearDivisionImage(null, key); } private void addOrClearDivisionImage(Bitmap b, Object key) { Utils.traceBeginSection("add or clear division image"); final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); if (pos != null && pos >= 0) { mDivisionImages.set(pos, b); boolean complete = false; final int width = mWidth; final int height = mHeight; // Different layouts depending on count. final int size = mDivisionMap.size(); switch (size) { case 0: // Do nothing. break; case 1: // Draw the bitmap filling the entire canvas. draw(mDivisionImages.get(0), 0, 0, width, height); complete = true; break; case 2: // Draw 2 bitmaps split vertically down the middle switch (pos) { case 0: draw(mDivisionImages.get(0), 0, 0, width / 2, height); break; case 1: draw(mDivisionImages.get(1), width / 2, 0, width, height); break; } complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null || isPartialBitmapComplete(); if (complete) { // Draw dividers drawVerticalDivider(width, height); } break; case 3: // Draw 3 bitmaps: the first takes up all vertical // space, the 2nd and 3rd are stacked in the second vertical // position. switch (pos) { case 0: draw(mDivisionImages.get(0), 0, 0, width / 2, height); break; case 1: draw(mDivisionImages.get(1), width / 2, 0, width, height / 2); break; case 2: draw(mDivisionImages.get(2), width / 2, height / 2, width, height); break; } complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null && mDivisionImages.get(2) != null || isPartialBitmapComplete(); if (complete) { // Draw dividers drawVerticalDivider(width, height); drawHorizontalDivider(width / 2, height / 2, width, height / 2); } break; default: // Draw all 4 bitmaps in a grid switch (pos) { case 0: draw(mDivisionImages.get(0), 0, 0, width / 2, height / 2); break; case 1: draw(mDivisionImages.get(1), width / 2, 0, width, height / 2); break; case 2: draw(mDivisionImages.get(2), 0, height / 2, width / 2, height); break; case 3: draw(mDivisionImages.get(3), width / 2, height / 2, width, height); break; } complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null || isPartialBitmapComplete(); if (complete) { // Draw dividers drawVerticalDivider(width, height); drawHorizontalDivider(0, height / 2, width, height / 2); } break; } // Create the new image bitmap. if (complete) { mBitmapValid = true; mCallback.invalidate(); } } Utils.traceEndSection(); } public boolean hasImageFor(Object key) { final Integer pos = mDivisionMap.get(transformKeyToDivisionId(key)); return pos != null && mDivisionImages.get(pos) != null; } private void setupDividerLines() { if (sDividerLineWidth == -1) { Resources res = getContext().getResources(); sDividerLineWidth = res .getDimensionPixelSize(R.dimen.tile_divider_width); sDividerColor = res.getColor(R.color.tile_divider_color); } } private static void setupPaint() { sPaint.setStrokeWidth(sDividerLineWidth); sPaint.setColor(sDividerColor); } protected void drawVerticalDivider(int width, int height) { int x1 = width / 2, y1 = 0, x2 = width/2, y2 = height; setupPaint(); mCanvas.drawLine(x1, y1, x2, y2, sPaint); } protected void drawHorizontalDivider(int x1, int y1, int x2, int y2) { setupPaint(); mCanvas.drawLine(x1, y1, x2, y2, sPaint); } protected boolean isPartialBitmapComplete() { return false; } protected String transformKeyToDivisionId(Object key) { return key.toString(); } /** * Draw the contents of the DividedImageCanvas to the supplied canvas. */ public void draw(Canvas canvas) { if (mDividedBitmap != null && mBitmapValid) { canvas.drawBitmap(mDividedBitmap, 0, 0, null); } } /** * Draw the contents of the DividedImageCanvas to the supplied canvas. */ public void draw(final Canvas canvas, final Matrix matrix) { if (mDividedBitmap != null && mBitmapValid) { canvas.drawBitmap(mDividedBitmap, matrix, null); } } @Override public void reset() { if (mCanvas != null && mDividedBitmap != null) { mBitmapValid = false; } mDivisionMap.clear(); mDivisionImages.clear(); mGeneration++; } @Override public int getGeneration() { return mGeneration; } /** * Set the width and height of the canvas. * @param width * @param height */ public void setDimensions(int width, int height) { Utils.traceBeginSection("set dimensions"); if (mWidth == width && mHeight == height) { Utils.traceEndSection(); return; } mWidth = width; mHeight = height; mDividedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mDividedBitmap); for (int i = 0; i < getDivisionCount(); i++) { mDivisionImages.set(i, null); } mBitmapValid = false; Utils.traceEndSection(); } /** * Get the resulting canvas width. */ public int getWidth() { return mWidth; } /** * Get the resulting canvas height. */ public int getHeight() { return mHeight; } /** * The class that will provided the canvas to which the DividedImageCanvas * should render its contents must implement this interface. */ public interface InvalidateCallback { public void invalidate(); } public int getDivisionCount() { return mDivisionMap.size(); } /** * Get the division ids currently associated with this DivisionImageCanvas. */ public ArrayList getDivisionIds() { return Lists.newArrayList(mDivisionMap.keySet()); } }