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
onPreExecute()57         protected void onPreExecute() {
58             mDecoder = mRegionDecoder;
59         }
60 
61         @Override
doInBackground(RectF... params)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
onPostExecute(Bitmap b)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 
ZoomView(Context context)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 
resetDecoder()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 
loadBitmap(Uri uri, int orientation, RectF imageRect)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 
showPartiallyDecodedImage(boolean show)214     private void showPartiallyDecodedImage(boolean show) {
215         if (show) {
216             setVisibility(View.VISIBLE);
217         } else {
218             setVisibility(View.GONE);
219         }
220     }
221 
cancelPartialDecodingTask()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      */
adjustToFitInBounds(RectF rect, int viewportWidth, int viewportHeight)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 
startPartialDecodingTask(RectF endRect)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
getInputStream()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      */
getSampleFactor(int width, int height)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