ZoomView.java revision c502bc9d50ded1f2eec28f4809c7193ec4ec5298
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.Rect;
25import android.graphics.RectF;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.util.Log;
29import android.view.View;
30import android.widget.ImageView;
31
32import java.io.FileNotFoundException;
33import java.io.IOException;
34import java.io.InputStream;
35
36public 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.CENTER_INSIDE);
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