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
17package com.android.mail.ui;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.graphics.PorterDuff.Mode;
26import android.graphics.PorterDuffXfermode;
27import android.graphics.Rect;
28
29import com.android.mail.R;
30import com.android.mail.utils.Utils;
31import com.google.common.collect.Lists;
32import com.google.common.collect.Maps;
33
34import java.util.ArrayList;
35import java.util.List;
36import 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 */
48public 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