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.camera.ui;
     18 
     19 import android.content.Context;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.graphics.BitmapRegionDecoder;
     23 import android.graphics.Matrix;
     24 import android.graphics.Point;
     25 import android.graphics.Rect;
     26 import android.graphics.RectF;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.view.View;
     30 import android.widget.ImageView;
     31 
     32 import com.android.camera.data.FilmstripItemUtils;
     33 import com.android.camera.debug.Log;
     34 
     35 import java.io.FileNotFoundException;
     36 import java.io.IOException;
     37 import java.io.InputStream;
     38 
     39 public class ZoomView extends ImageView {
     40 
     41     private static final Log.Tag TAG = new Log.Tag("ZoomView");
     42 
     43     private int mViewportWidth = 0;
     44     private int mViewportHeight = 0;
     45 
     46     private BitmapRegionDecoder mRegionDecoder;
     47     // This is null when there's no decoding going on.
     48     private DecodePartialBitmap mPartialDecodingTask;
     49 
     50     private Uri mUri;
     51     private int mOrientation;
     52 
     53     private class DecodePartialBitmap extends AsyncTask<RectF, Void, Bitmap> {
     54         BitmapRegionDecoder mDecoder;
     55 
     56         @Override
     57         protected void onPreExecute() {
     58             mDecoder = mRegionDecoder;
     59         }
     60 
     61         @Override
     62         protected Bitmap doInBackground(RectF... params) {
     63             RectF endRect = params[0];
     64 
     65             // Calculate the rotation matrix to apply orientation on the original image
     66             // rect.
     67             InputStream isForDimensions = getInputStream();
     68             if (isForDimensions == null) {
     69                 return null;
     70             }
     71 
     72             Point imageSize = FilmstripItemUtils.decodeBitmapDimension(isForDimensions);
     73             try {
     74                 isForDimensions.close();
     75             } catch (IOException e) {
     76                 Log.e(TAG, "exception closing dimensions inputstream", e);
     77             }
     78             if (imageSize == null) {
     79                 return null;
     80             }
     81 
     82             RectF fullResRect = new RectF(0, 0, imageSize.x - 1, imageSize.y - 1);
     83             Matrix rotationMatrix = new Matrix();
     84             rotationMatrix.setRotate(mOrientation, 0, 0);
     85             rotationMatrix.mapRect(fullResRect);
     86             // Set the translation of the matrix so that after rotation, the top left
     87             // of the image rect is at (0, 0)
     88             rotationMatrix.postTranslate(-fullResRect.left, -fullResRect.top);
     89             rotationMatrix.mapRect(fullResRect, new RectF(0, 0, imageSize.x - 1,
     90                     imageSize.y - 1));
     91 
     92             // Find intersection with the screen
     93             RectF visibleRect = new RectF(endRect);
     94             visibleRect.intersect(0, 0, mViewportWidth - 1, mViewportHeight - 1);
     95             // Calculate the mapping (i.e. transform) between current low res rect
     96             // and full res image rect, and apply the mapping on current visible rect
     97             // to find out the partial region in the full res image that we need
     98             // to decode.
     99             Matrix mapping = new Matrix();
    100             mapping.setRectToRect(endRect, fullResRect, Matrix.ScaleToFit.CENTER);
    101             RectF visibleAfterRotation = new RectF();
    102             mapping.mapRect(visibleAfterRotation, visibleRect);
    103 
    104             // Now the visible region we have is rotated, we need to reverse the
    105             // rotation to find out the region in the original image
    106             RectF visibleInImage = new RectF();
    107             Matrix invertRotation = new Matrix();
    108             rotationMatrix.invert(invertRotation);
    109             invertRotation.mapRect(visibleInImage, visibleAfterRotation);
    110 
    111             // Decode region
    112             Rect region = new Rect();
    113             visibleInImage.round(region);
    114 
    115             // Make sure region to decode is inside the image.
    116             region.intersect(0, 0, imageSize.x - 1, imageSize.y - 1);
    117 
    118             if (region.width() == 0 || region.height() == 0) {
    119                 Log.e(TAG, "Invalid size for partial region. Region: " + region.toString());
    120                 return null;
    121             }
    122 
    123             if (isCancelled()) {
    124                 return null;
    125             }
    126 
    127             BitmapFactory.Options options = new BitmapFactory.Options();
    128             if ((mOrientation + 360) % 180 == 0) {
    129                 options.inSampleSize = getSampleFactor(region.width(), region.height());
    130             } else {
    131                 // The decoded region will be rotated 90/270 degrees before showing
    132                 // on screen. In other words, the width and height will be swapped.
    133                 // Therefore, sample factor should be calculated using swapped width
    134                 // and height.
    135                 options.inSampleSize = getSampleFactor(region.height(), region.width());
    136             }
    137 
    138             if (mDecoder == null) {
    139                 InputStream is = getInputStream();
    140                 if (is == null) {
    141                     return null;
    142                 }
    143 
    144                 try {
    145                     mDecoder = BitmapRegionDecoder.newInstance(is, false);
    146                     is.close();
    147                 } catch (IOException e) {
    148                     Log.e(TAG, "Failed to instantiate region decoder");
    149                 }
    150             }
    151             if (mDecoder == null) {
    152                 return null;
    153             }
    154             Bitmap b = mDecoder.decodeRegion(region, options);
    155             if (isCancelled()) {
    156                 return null;
    157             }
    158             Matrix rotation = new Matrix();
    159             rotation.setRotate(mOrientation);
    160             return Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), rotation, false);
    161         }
    162 
    163         @Override
    164         protected void onPostExecute(Bitmap b) {
    165             mPartialDecodingTask = null;
    166             if (mDecoder != mRegionDecoder) {
    167                 // This decoder will no longer be used, recycle it.
    168                 mDecoder.recycle();
    169             }
    170             if (b != null) {
    171                 setImageBitmap(b);
    172                 showPartiallyDecodedImage(true);
    173             }
    174         }
    175     }
    176 
    177     public ZoomView(Context context) {
    178         super(context);
    179         setScaleType(ScaleType.FIT_CENTER);
    180         addOnLayoutChangeListener(new OnLayoutChangeListener() {
    181             @Override
    182             public void onLayoutChange(View v, int left, int top, int right, int bottom,
    183                                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
    184                 int w = right - left;
    185                 int h = bottom - top;
    186                 if (mViewportHeight != h || mViewportWidth != w) {
    187                     mViewportWidth = w;
    188                     mViewportHeight = h;
    189                 }
    190             }
    191         });
    192     }
    193 
    194     public void resetDecoder() {
    195         if (mRegionDecoder != null) {
    196             cancelPartialDecodingTask();
    197             if (mPartialDecodingTask == null) {
    198                 // No ongoing decoding task, safe to recycle the decoder.
    199                 mRegionDecoder.recycle();
    200             }
    201             mRegionDecoder = null;
    202         }
    203     }
    204 
    205     public void loadBitmap(Uri uri, int orientation, RectF imageRect) {
    206         if (!uri.equals(mUri)) {
    207             resetDecoder();
    208             mUri = uri;
    209             mOrientation = orientation;
    210         }
    211         startPartialDecodingTask(imageRect);
    212     }
    213 
    214     private void showPartiallyDecodedImage(boolean show) {
    215         if (show) {
    216             setVisibility(View.VISIBLE);
    217         } else {
    218             setVisibility(View.GONE);
    219         }
    220     }
    221 
    222     public void cancelPartialDecodingTask() {
    223         if (mPartialDecodingTask != null && !mPartialDecodingTask.isCancelled()) {
    224             mPartialDecodingTask.cancel(true);
    225             setVisibility(GONE);
    226         }
    227     }
    228 
    229     /**
    230      * If the given rect is smaller than viewport on x or y axis, center rect within
    231      * viewport on the corresponding axis. Otherwise, make sure viewport is within
    232      * the bounds of the rect.
    233      */
    234     public static RectF adjustToFitInBounds(RectF rect, int viewportWidth, int viewportHeight) {
    235         float dx = 0, dy = 0;
    236         RectF newRect = new RectF(rect);
    237         if (newRect.width() < viewportWidth) {
    238             dx = viewportWidth / 2 - (newRect.left + newRect.right) / 2;
    239         } else {
    240             if (newRect.left > 0) {
    241                 dx = -newRect.left;
    242             } else if (newRect.right < viewportWidth) {
    243                 dx = viewportWidth - newRect.right;
    244             }
    245         }
    246 
    247         if (newRect.height() < viewportHeight) {
    248             dy = viewportHeight / 2 - (newRect.top + newRect.bottom) / 2;
    249         } else {
    250             if (newRect.top > 0) {
    251                 dy = -newRect.top;
    252             } else if (newRect.bottom < viewportHeight) {
    253                 dy = viewportHeight - newRect.bottom;
    254             }
    255         }
    256 
    257         if (dx != 0 || dy != 0) {
    258             newRect.offset(dx, dy);
    259         }
    260         return newRect;
    261     }
    262 
    263     private void startPartialDecodingTask(RectF endRect) {
    264         // Cancel on-going partial decoding tasks
    265         cancelPartialDecodingTask();
    266         mPartialDecodingTask = new DecodePartialBitmap();
    267         mPartialDecodingTask.execute(endRect);
    268     }
    269 
    270     // TODO: Cache the inputstream
    271     private InputStream getInputStream() {
    272         InputStream is = null;
    273         try {
    274             is = getContext().getContentResolver().openInputStream(mUri);
    275         } catch (FileNotFoundException e) {
    276             Log.e(TAG, "File not found at: " + mUri);
    277         }
    278         return is;
    279     }
    280 
    281     /**
    282      * Find closest sample factor that is power of 2, based on the given width and height
    283      *
    284      * @param width width of the partial region to decode
    285      * @param height height of the partial region to decode
    286      * @return sample factor
    287      */
    288     private int getSampleFactor(int width, int height) {
    289 
    290         float fitWidthScale = ((float) mViewportWidth) / ((float) width);
    291         float fitHeightScale = ((float) mViewportHeight) / ((float) height);
    292 
    293         float scale = Math.min(fitHeightScale, fitWidthScale);
    294 
    295         // Find the closest sample factor that is power of 2
    296         int sampleFactor = (int) (1f / scale);
    297         if (sampleFactor <=1) {
    298             return 1;
    299         }
    300         for (int i = 0; i < 32; i++) {
    301             if ((1 << (i + 1)) > sampleFactor) {
    302                 sampleFactor = (1 << i);
    303                 break;
    304             }
    305         }
    306         return sampleFactor;
    307     }
    308 }
    309