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.camera.ui; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.BitmapFactory; 22import android.graphics.BitmapRegionDecoder; 23import android.graphics.Matrix; 24import android.graphics.Point; 25import android.graphics.Rect; 26import android.graphics.RectF; 27import android.net.Uri; 28import android.os.AsyncTask; 29import android.view.View; 30import android.widget.ImageView; 31 32import com.android.camera.data.FilmstripItemUtils; 33import com.android.camera.debug.Log; 34 35import java.io.FileNotFoundException; 36import java.io.IOException; 37import java.io.InputStream; 38 39public 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