| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.camera.ui; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.BitmapRegionDecoder; |
| import android.graphics.Matrix; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.util.Log; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| public class ZoomView extends ImageView { |
| |
| private static final String TAG = "ZoomView"; |
| |
| private int mViewportWidth = 0; |
| private int mViewportHeight = 0; |
| |
| private int mFullResImageWidth; |
| private int mFullResImageHeight; |
| |
| private BitmapRegionDecoder mRegionDecoder; |
| private DecodePartialBitmap mPartialDecodingTask; |
| |
| private Uri mUri; |
| private int mOrientation; |
| |
| private class DecodePartialBitmap extends AsyncTask<RectF, Void, Bitmap> { |
| |
| @Override |
| protected Bitmap doInBackground(RectF... params) { |
| RectF endRect = params[0]; |
| |
| // Calculate the rotation matrix to apply orientation on the original image |
| // rect. |
| RectF fullResRect = new RectF(0, 0, mFullResImageWidth - 1, mFullResImageHeight - 1); |
| Matrix rotationMatrix = new Matrix(); |
| rotationMatrix.setRotate(mOrientation, 0, 0); |
| rotationMatrix.mapRect(fullResRect); |
| // Set the translation of the matrix so that after rotation, the top left |
| // of the image rect is at (0, 0) |
| rotationMatrix.postTranslate(-fullResRect.left, -fullResRect.top); |
| rotationMatrix.mapRect(fullResRect, new RectF(0, 0, mFullResImageWidth - 1, |
| mFullResImageHeight - 1)); |
| |
| // Find intersection with the screen |
| RectF visibleRect = new RectF(endRect); |
| visibleRect.intersect(0, 0, mViewportWidth - 1, mViewportHeight - 1); |
| // Calculate the mapping (i.e. transform) between current low res rect |
| // and full res image rect, and apply the mapping on current visible rect |
| // to find out the partial region in the full res image that we need |
| // to decode. |
| Matrix mapping = new Matrix(); |
| mapping.setRectToRect(endRect, fullResRect, Matrix.ScaleToFit.CENTER); |
| RectF visibleAfterRotation = new RectF(); |
| mapping.mapRect(visibleAfterRotation, visibleRect); |
| |
| // Now the visible region we have is rotated, we need to reverse the |
| // rotation to find out the region in the original image |
| RectF visibleInImage = new RectF(); |
| Matrix invertRotation = new Matrix(); |
| rotationMatrix.invert(invertRotation); |
| invertRotation.mapRect(visibleInImage, visibleAfterRotation); |
| |
| // Decode region |
| Rect region = new Rect(); |
| visibleInImage.round(region); |
| |
| // Make sure region to decode is inside the image. |
| region.intersect(0, 0, mFullResImageWidth - 1, mFullResImageHeight - 1); |
| |
| if (region.width() == 0 || region.height() == 0) { |
| Log.e(TAG, "Invalid size for partial region. Region: " + region.toString()); |
| return null; |
| } |
| |
| if (isCancelled()) { |
| return null; |
| } |
| |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| |
| if ((mOrientation + 360) % 180 == 0) { |
| options.inSampleSize = getSampleFactor(region.width(), region.height()); |
| } else { |
| // The decoded region will be rotated 90/270 degrees before showing |
| // on screen. In other words, the width and height will be swapped. |
| // Therefore, sample factor should be calculated using swapped width |
| // and height. |
| options.inSampleSize = getSampleFactor(region.height(), region.width()); |
| } |
| |
| if (mRegionDecoder == null) { |
| InputStream is = getInputStream(); |
| try { |
| mRegionDecoder = BitmapRegionDecoder.newInstance(is, false); |
| is.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to instantiate region decoder"); |
| } |
| } |
| if (mRegionDecoder == null) { |
| return null; |
| } |
| Bitmap b = mRegionDecoder.decodeRegion(region, options); |
| if (isCancelled()) { |
| return null; |
| } |
| Matrix rotation = new Matrix(); |
| rotation.setRotate(mOrientation); |
| return Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), rotation, false); |
| } |
| |
| @Override |
| protected void onPostExecute(Bitmap b) { |
| if (b == null) { |
| return; |
| } |
| setImageBitmap(b); |
| showPartiallyDecodedImage(true); |
| mPartialDecodingTask = null; |
| } |
| } |
| |
| public ZoomView(Context context) { |
| super(context); |
| setScaleType(ScaleType.FIT_CENTER); |
| addOnLayoutChangeListener(new OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| int w = right - left; |
| int h = bottom - top; |
| if (mViewportHeight != h || mViewportWidth != w) { |
| mViewportWidth = w; |
| mViewportHeight = h; |
| } |
| } |
| }); |
| } |
| |
| public void loadBitmap(Uri uri, int orientation, RectF imageRect) { |
| if (!uri.equals(mUri)) { |
| mUri = uri; |
| mOrientation = orientation; |
| mFullResImageHeight = 0; |
| mFullResImageWidth = 0; |
| decodeImageSize(); |
| mRegionDecoder = null; |
| } |
| startPartialDecodingTask(imageRect); |
| } |
| |
| private void showPartiallyDecodedImage(boolean show) { |
| if (show) { |
| setVisibility(View.VISIBLE); |
| } else { |
| setVisibility(View.GONE); |
| } |
| mPartialDecodingTask = null; |
| } |
| |
| public void cancelPartialDecodingTask() { |
| if (mPartialDecodingTask != null && !mPartialDecodingTask.isCancelled()) { |
| mPartialDecodingTask.cancel(true); |
| setVisibility(GONE); |
| } |
| mPartialDecodingTask = null; |
| } |
| |
| /** |
| * If the given rect is smaller than viewport on x or y axis, center rect within |
| * viewport on the corresponding axis. Otherwise, make sure viewport is within |
| * the bounds of the rect. |
| */ |
| public static RectF adjustToFitInBounds(RectF rect, int viewportWidth, int viewportHeight) { |
| float dx = 0, dy = 0; |
| RectF newRect = new RectF(rect); |
| if (newRect.width() < viewportWidth) { |
| dx = viewportWidth / 2 - (newRect.left + newRect.right) / 2; |
| } else { |
| if (newRect.left > 0) { |
| dx = -newRect.left; |
| } else if (newRect.right < viewportWidth) { |
| dx = viewportWidth - newRect.right; |
| } |
| } |
| |
| if (newRect.height() < viewportHeight) { |
| dy = viewportHeight / 2 - (newRect.top + newRect.bottom) / 2; |
| } else { |
| if (newRect.top > 0) { |
| dy = -newRect.top; |
| } else if (newRect.bottom < viewportHeight) { |
| dy = viewportHeight - newRect.bottom; |
| } |
| } |
| |
| if (dx != 0 || dy != 0) { |
| newRect.offset(dx, dy); |
| } |
| return newRect; |
| } |
| |
| private void startPartialDecodingTask(RectF endRect) { |
| // Cancel on-going partial decoding tasks |
| cancelPartialDecodingTask(); |
| mPartialDecodingTask = new DecodePartialBitmap(); |
| mPartialDecodingTask.execute(endRect); |
| } |
| |
| private void decodeImageSize() { |
| BitmapFactory.Options option = new BitmapFactory.Options(); |
| option.inJustDecodeBounds = true; |
| InputStream is = getInputStream(); |
| BitmapFactory.decodeStream(is, null, option); |
| try { |
| is.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to close input stream"); |
| } |
| mFullResImageWidth = option.outWidth; |
| mFullResImageHeight = option.outHeight; |
| } |
| |
| // TODO: Cache the inputstream |
| private InputStream getInputStream() { |
| InputStream is = null; |
| try { |
| is = getContext().getContentResolver().openInputStream(mUri); |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, "File not found at: " + mUri); |
| } |
| return is; |
| } |
| |
| /** |
| * Find closest sample factor that is power of 2, based on the given width and height |
| * |
| * @param width width of the partial region to decode |
| * @param height height of the partial region to decode |
| * @return sample factor |
| */ |
| private int getSampleFactor(int width, int height) { |
| |
| float fitWidthScale = ((float) mViewportWidth) / ((float) width); |
| float fitHeightScale = ((float) mViewportHeight) / ((float) height); |
| |
| float scale = Math.min(fitHeightScale, fitWidthScale); |
| |
| // Find the closest sample factor that is power of 2 |
| int sampleFactor = (int) (1f / scale); |
| if (sampleFactor <=1) { |
| return 1; |
| } |
| for (int i = 0; i < 32; i++) { |
| if ((1 << (i + 1)) > sampleFactor) { |
| sampleFactor = (1 << i); |
| break; |
| } |
| } |
| return sampleFactor; |
| } |
| } |