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