Home | History | Annotate | Download | only in ui
      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