blob: 274327e306b9554b4ce48baddf6b810aa5efabbc [file] [log] [blame]
/*
* 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<String, Integer> 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<Bitmap> mDivisionImages = new ArrayList<Bitmap>(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<Object> 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<String> getDivisionIds() {
return Lists.newArrayList(mDivisionMap.keySet());
}
}