blob: 252838d95c5259c560677ec88f3cab99b36575d3 [file] [log] [blame]
Doris Liuc6857032013-08-15 17:34:41 -07001/*
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.camera.ui;
18
Doris Liuc6857032013-08-15 17:34:41 -070019import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.BitmapRegionDecoder;
23import android.graphics.Matrix;
Sam Judd2666dc82014-03-21 16:02:12 -070024import android.graphics.Point;
Doris Liuc6857032013-08-15 17:34:41 -070025import android.graphics.Rect;
26import android.graphics.RectF;
27import android.net.Uri;
28import android.os.AsyncTask;
Doris Liuc6857032013-08-15 17:34:41 -070029import android.view.View;
Doris Liuc6857032013-08-15 17:34:41 -070030import android.widget.ImageView;
Angus Kong2bca2102014-03-11 16:27:30 -070031
Sam Judd2666dc82014-03-21 16:02:12 -070032import com.android.camera.data.LocalDataUtil;
Angus Kong2bca2102014-03-11 16:27:30 -070033import com.android.camera.debug.Log;
Doris Liuc6857032013-08-15 17:34:41 -070034
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.InputStream;
38
Doris Liu8de13112013-08-23 13:35:24 -070039public class ZoomView extends ImageView {
Doris Liuc6857032013-08-15 17:34:41 -070040
Angus Kong2bca2102014-03-11 16:27:30 -070041 private static final Log.Tag TAG = new Log.Tag("ZoomView");
Doris Liuc6857032013-08-15 17:34:41 -070042
Doris Liuc6857032013-08-15 17:34:41 -070043 private int mViewportWidth = 0;
44 private int mViewportHeight = 0;
45
Doris Liuc6857032013-08-15 17:34:41 -070046 private BitmapRegionDecoder mRegionDecoder;
47 private DecodePartialBitmap mPartialDecodingTask;
Doris Liuc6857032013-08-15 17:34:41 -070048
Doris Liuc6857032013-08-15 17:34:41 -070049 private Uri mUri;
Doris Liu87fc5e12013-09-16 13:38:24 -070050 private int mOrientation;
Doris Liuc6857032013-08-15 17:34:41 -070051
Doris Liuc6857032013-08-15 17:34:41 -070052 private class DecodePartialBitmap extends AsyncTask<RectF, Void, Bitmap> {
53
54 @Override
55 protected Bitmap doInBackground(RectF... params) {
56 RectF endRect = params[0];
Doris Liu87fc5e12013-09-16 13:38:24 -070057
58 // Calculate the rotation matrix to apply orientation on the original image
59 // rect.
Sam Judd2666dc82014-03-21 16:02:12 -070060 InputStream isForDimensions = getInputStream();
61 if (isForDimensions == null) {
62 return null;
63 }
64
65 Point imageSize = LocalDataUtil.decodeBitmapDimension(isForDimensions);
66 try {
67 isForDimensions.close();
68 } catch (IOException e) {
69 e.printStackTrace();
70 }
71 if (imageSize == null) {
72 return null;
73 }
74
75 RectF fullResRect = new RectF(0, 0, imageSize.x - 1, imageSize.y - 1);
Doris Liu87fc5e12013-09-16 13:38:24 -070076 Matrix rotationMatrix = new Matrix();
77 rotationMatrix.setRotate(mOrientation, 0, 0);
78 rotationMatrix.mapRect(fullResRect);
79 // Set the translation of the matrix so that after rotation, the top left
80 // of the image rect is at (0, 0)
81 rotationMatrix.postTranslate(-fullResRect.left, -fullResRect.top);
Sam Judd2666dc82014-03-21 16:02:12 -070082 rotationMatrix.mapRect(fullResRect, new RectF(0, 0, imageSize.x - 1,
83 imageSize.y - 1));
Doris Liu87fc5e12013-09-16 13:38:24 -070084
Doris Liuc6857032013-08-15 17:34:41 -070085 // Find intersection with the screen
86 RectF visibleRect = new RectF(endRect);
Doris Liu121022c2013-09-03 16:03:16 -070087 visibleRect.intersect(0, 0, mViewportWidth - 1, mViewportHeight - 1);
Doris Liu87fc5e12013-09-16 13:38:24 -070088 // Calculate the mapping (i.e. transform) between current low res rect
89 // and full res image rect, and apply the mapping on current visible rect
90 // to find out the partial region in the full res image that we need
91 // to decode.
92 Matrix mapping = new Matrix();
93 mapping.setRectToRect(endRect, fullResRect, Matrix.ScaleToFit.CENTER);
94 RectF visibleAfterRotation = new RectF();
95 mapping.mapRect(visibleAfterRotation, visibleRect);
Doris Liuc6857032013-08-15 17:34:41 -070096
Doris Liu87fc5e12013-09-16 13:38:24 -070097 // Now the visible region we have is rotated, we need to reverse the
98 // rotation to find out the region in the original image
Doris Liuc6857032013-08-15 17:34:41 -070099 RectF visibleInImage = new RectF();
Doris Liu87fc5e12013-09-16 13:38:24 -0700100 Matrix invertRotation = new Matrix();
101 rotationMatrix.invert(invertRotation);
102 invertRotation.mapRect(visibleInImage, visibleAfterRotation);
Doris Liuc6857032013-08-15 17:34:41 -0700103
104 // Decode region
Doris Liu87fc5e12013-09-16 13:38:24 -0700105 Rect region = new Rect();
106 visibleInImage.round(region);
Doris Liu121022c2013-09-03 16:03:16 -0700107
108 // Make sure region to decode is inside the image.
Sam Judd2666dc82014-03-21 16:02:12 -0700109 region.intersect(0, 0, imageSize.x - 1, imageSize.y - 1);
Doris Liu121022c2013-09-03 16:03:16 -0700110
Doris Liu1f69e362013-10-01 15:04:09 -0700111 if (region.width() == 0 || region.height() == 0) {
112 Log.e(TAG, "Invalid size for partial region. Region: " + region.toString());
113 return null;
114 }
115
Doris Liuc6857032013-08-15 17:34:41 -0700116 if (isCancelled()) {
117 return null;
118 }
119
120 BitmapFactory.Options options = new BitmapFactory.Options();
Doris Liuc502bc92013-10-07 18:39:26 -0700121
122 if ((mOrientation + 360) % 180 == 0) {
123 options.inSampleSize = getSampleFactor(region.width(), region.height());
124 } else {
125 // The decoded region will be rotated 90/270 degrees before showing
126 // on screen. In other words, the width and height will be swapped.
127 // Therefore, sample factor should be calculated using swapped width
128 // and height.
129 options.inSampleSize = getSampleFactor(region.height(), region.width());
130 }
131
Doris Liu121022c2013-09-03 16:03:16 -0700132 if (mRegionDecoder == null) {
133 InputStream is = getInputStream();
Sam Judd2666dc82014-03-21 16:02:12 -0700134 if (is == null) {
135 return null;
136 }
137
Doris Liu121022c2013-09-03 16:03:16 -0700138 try {
139 mRegionDecoder = BitmapRegionDecoder.newInstance(is, false);
140 is.close();
141 } catch (IOException e) {
142 Log.e(TAG, "Failed to instantiate region decoder");
143 }
144 }
145 if (mRegionDecoder == null) {
146 return null;
147 }
Doris Liu87fc5e12013-09-16 13:38:24 -0700148 Bitmap b = mRegionDecoder.decodeRegion(region, options);
149 if (isCancelled()) {
150 return null;
151 }
152 Matrix rotation = new Matrix();
153 rotation.setRotate(mOrientation);
154 return Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), rotation, false);
Doris Liuc6857032013-08-15 17:34:41 -0700155 }
156
157 @Override
158 protected void onPostExecute(Bitmap b) {
159 if (b == null) {
160 return;
161 }
Doris Liu8de13112013-08-23 13:35:24 -0700162 setImageBitmap(b);
Doris Liuc6857032013-08-15 17:34:41 -0700163 showPartiallyDecodedImage(true);
164 mPartialDecodingTask = null;
165 }
166 }
167
Doris Liu8de13112013-08-23 13:35:24 -0700168 public ZoomView(Context context) {
Doris Liuc6857032013-08-15 17:34:41 -0700169 super(context);
Doris Liu3179f6a2013-10-08 16:54:22 -0700170 setScaleType(ScaleType.FIT_CENTER);
Doris Liuc6857032013-08-15 17:34:41 -0700171 addOnLayoutChangeListener(new OnLayoutChangeListener() {
172 @Override
173 public void onLayoutChange(View v, int left, int top, int right, int bottom,
174 int oldLeft, int oldTop, int oldRight, int oldBottom) {
175 int w = right - left;
176 int h = bottom - top;
177 if (mViewportHeight != h || mViewportWidth != w) {
178 mViewportWidth = w;
179 mViewportHeight = h;
Doris Liuc6857032013-08-15 17:34:41 -0700180 }
181 }
182 });
Doris Liuc6857032013-08-15 17:34:41 -0700183 }
184
Doris Liu87fc5e12013-09-16 13:38:24 -0700185 public void loadBitmap(Uri uri, int orientation, RectF imageRect) {
Doris Liu121022c2013-09-03 16:03:16 -0700186 if (!uri.equals(mUri)) {
187 mUri = uri;
Doris Liu87fc5e12013-09-16 13:38:24 -0700188 mOrientation = orientation;
Doris Liu121022c2013-09-03 16:03:16 -0700189 mRegionDecoder = null;
Doris Liuc6857032013-08-15 17:34:41 -0700190 }
Doris Liu8de13112013-08-23 13:35:24 -0700191 startPartialDecodingTask(imageRect);
Doris Liuc6857032013-08-15 17:34:41 -0700192 }
193
194 private void showPartiallyDecodedImage(boolean show) {
195 if (show) {
Doris Liu8de13112013-08-23 13:35:24 -0700196 setVisibility(View.VISIBLE);
Doris Liuc6857032013-08-15 17:34:41 -0700197 } else {
Doris Liu8de13112013-08-23 13:35:24 -0700198 setVisibility(View.GONE);
Doris Liuc6857032013-08-15 17:34:41 -0700199 }
Doris Liu8de13112013-08-23 13:35:24 -0700200 mPartialDecodingTask = null;
201 }
202
Doris Liu8de13112013-08-23 13:35:24 -0700203 public void cancelPartialDecodingTask() {
204 if (mPartialDecodingTask != null && !mPartialDecodingTask.isCancelled()) {
205 mPartialDecodingTask.cancel(true);
206 setVisibility(GONE);
207 }
208 mPartialDecodingTask = null;
Doris Liuc6857032013-08-15 17:34:41 -0700209 }
210
211 /**
Doris Liu8de13112013-08-23 13:35:24 -0700212 * If the given rect is smaller than viewport on x or y axis, center rect within
213 * viewport on the corresponding axis. Otherwise, make sure viewport is within
214 * the bounds of the rect.
215 */
Doris Liu121022c2013-09-03 16:03:16 -0700216 public static RectF adjustToFitInBounds(RectF rect, int viewportWidth, int viewportHeight) {
217 float dx = 0, dy = 0;
218 RectF newRect = new RectF(rect);
Doris Liu8de13112013-08-23 13:35:24 -0700219 if (newRect.width() < viewportWidth) {
220 dx = viewportWidth / 2 - (newRect.left + newRect.right) / 2;
Doris Liuc6857032013-08-15 17:34:41 -0700221 } else {
Doris Liu8de13112013-08-23 13:35:24 -0700222 if (newRect.left > 0) {
223 dx = -newRect.left;
224 } else if (newRect.right < viewportWidth) {
225 dx = viewportWidth - newRect.right;
226 }
Doris Liuc6857032013-08-15 17:34:41 -0700227 }
Doris Liu8de13112013-08-23 13:35:24 -0700228
229 if (newRect.height() < viewportHeight) {
230 dy = viewportHeight / 2 - (newRect.top + newRect.bottom) / 2;
231 } else {
232 if (newRect.top > 0) {
233 dy = -newRect.top;
234 } else if (newRect.bottom < viewportHeight) {
235 dy = viewportHeight - newRect.bottom;
236 }
237 }
238
239 if (dx != 0 || dy != 0) {
240 newRect.offset(dx, dy);
241 }
242 return newRect;
Doris Liuc6857032013-08-15 17:34:41 -0700243 }
244
Doris Liuc6857032013-08-15 17:34:41 -0700245 private void startPartialDecodingTask(RectF endRect) {
246 // Cancel on-going partial decoding tasks
247 cancelPartialDecodingTask();
248 mPartialDecodingTask = new DecodePartialBitmap();
249 mPartialDecodingTask.execute(endRect);
250 }
251
Doris Liuc6857032013-08-15 17:34:41 -0700252 // TODO: Cache the inputstream
253 private InputStream getInputStream() {
254 InputStream is = null;
255 try {
256 is = getContext().getContentResolver().openInputStream(mUri);
257 } catch (FileNotFoundException e) {
258 Log.e(TAG, "File not found at: " + mUri);
259 }
260 return is;
261 }
262
263 /**
264 * Find closest sample factor that is power of 2, based on the given width and height
265 *
266 * @param width width of the partial region to decode
267 * @param height height of the partial region to decode
268 * @return sample factor
269 */
270 private int getSampleFactor(int width, int height) {
271
272 float fitWidthScale = ((float) mViewportWidth) / ((float) width);
273 float fitHeightScale = ((float) mViewportHeight) / ((float) height);
274
275 float scale = Math.min(fitHeightScale, fitWidthScale);
276
277 // Find the closest sample factor that is power of 2
278 int sampleFactor = (int) (1f / scale);
279 if (sampleFactor <=1) {
280 return 1;
281 }
282 for (int i = 0; i < 32; i++) {
283 if ((1 << (i + 1)) > sampleFactor) {
284 sampleFactor = (1 << i);
285 break;
286 }
287 }
288 return sampleFactor;
289 }
290}