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