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;
18 
19 import android.content.res.Configuration;
20 import android.graphics.Bitmap;
21 import android.graphics.Matrix;
22 import android.graphics.RectF;
23 import android.graphics.SurfaceTexture;
24 import android.view.TextureView;
25 import android.view.View;
26 import android.view.View.OnLayoutChangeListener;
27 
28 import com.android.camera.app.CameraProvider;
29 import com.android.camera.debug.Log;
30 import com.android.camera.settings.Keys;
31 import com.android.camera.settings.ResolutionUtil;
32 import com.android.camera.settings.SettingsManager;
33 import com.android.camera.settings.SettingsUtil;
34 import com.android.camera.ui.PreviewStatusListener;
35 import com.android.camera.util.CameraUtil;
36 import com.android.ex.camera2.portability.CameraDeviceInfo;
37 import com.android.ex.camera2.portability.Size;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * This class aims to automate TextureView transform change and notify listeners
44  * (e.g. bottom bar) of the preview size change.
45  */
46 public class TextureViewHelper implements TextureView.SurfaceTextureListener,
47         OnLayoutChangeListener {
48 
49     private static final Log.Tag TAG = new Log.Tag("TexViewHelper");
50     public static final float MATCH_SCREEN = 0f;
51     private static final int UNSET = -1;
52     private final TextureView mPreview;
53     private final CameraProvider mCameraProvider;
54     private int mWidth = 0;
55     private int mHeight = 0;
56     private RectF mPreviewArea = new RectF();
57     private float mAspectRatio = MATCH_SCREEN;
58     private boolean mAutoAdjustTransform = true;
59     private TextureView.SurfaceTextureListener mSurfaceTextureListener;
60 
61     private final ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>
62             mAspectRatioChangedListeners =
63             new ArrayList<PreviewStatusListener.PreviewAspectRatioChangedListener>();
64 
65     private final ArrayList<PreviewStatusListener.PreviewAreaChangedListener>
66             mPreviewSizeChangedListeners =
67             new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>();
68     private OnLayoutChangeListener mOnLayoutChangeListener = null;
69     private CaptureLayoutHelper mCaptureLayoutHelper = null;
70     private int mOrientation = UNSET;
71 
TextureViewHelper(TextureView preview, CaptureLayoutHelper helper, CameraProvider cameraProvider)72     public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper,
73                              CameraProvider cameraProvider) {
74         mPreview = preview;
75         mCameraProvider = cameraProvider;
76         mPreview.addOnLayoutChangeListener(this);
77         mPreview.setSurfaceTextureListener(this);
78         mCaptureLayoutHelper = helper;
79     }
80 
81     /**
82      * If auto adjust transform is enabled, when there is a layout change, the
83      * transform matrix will be automatically adjusted based on the preview stream
84      * aspect ratio in the new layout.
85      *
86      * @param enable whether or not auto adjustment should be enabled
87      */
setAutoAdjustTransform(boolean enable)88     public void setAutoAdjustTransform(boolean enable) {
89         mAutoAdjustTransform = enable;
90     }
91 
92     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)93     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
94                                int oldTop, int oldRight, int oldBottom) {
95         Log.v(TAG, "onLayoutChange");
96         int width = right - left;
97         int height = bottom - top;
98         int rotation = CameraUtil.getDisplayRotation(mPreview.getContext());
99         if (mWidth != width || mHeight != height || mOrientation != rotation) {
100             mWidth = width;
101             mHeight = height;
102             mOrientation = rotation;
103             if (!updateTransform()) {
104                 clearTransform();
105             }
106         }
107         if (mOnLayoutChangeListener != null) {
108             mOnLayoutChangeListener.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop,
109                     oldRight, oldBottom);
110         }
111     }
112 
113     /**
114      * Transforms the preview with the identity matrix, ensuring there
115      * is no scaling on the preview.  It also calls onPreviewSizeChanged, to
116      * trigger any necessary preview size changing callbacks.
117      */
clearTransform()118     public void clearTransform() {
119         mPreview.setTransform(new Matrix());
120         mPreviewArea.set(0, 0, mWidth, mHeight);
121         onPreviewAreaChanged(mPreviewArea);
122         setAspectRatio(MATCH_SCREEN);
123     }
124 
updateAspectRatio(float aspectRatio)125     public void updateAspectRatio(float aspectRatio) {
126         Log.v(TAG, "updateAspectRatio");
127         if (aspectRatio <= 0) {
128             Log.e(TAG, "Invalid aspect ratio: " + aspectRatio);
129             return;
130         }
131         if (aspectRatio < 1f) {
132             aspectRatio = 1f / aspectRatio;
133         }
134         setAspectRatio(aspectRatio);
135         updateTransform();
136     }
137 
setAspectRatio(float aspectRatio)138     private void setAspectRatio(float aspectRatio) {
139         Log.v(TAG, "setAspectRatio: " + aspectRatio);
140         if (mAspectRatio != aspectRatio) {
141             Log.v(TAG, "aspect ratio changed from: " + mAspectRatio);
142             mAspectRatio = aspectRatio;
143             onAspectRatioChanged();
144         }
145     }
146 
onAspectRatioChanged()147     private void onAspectRatioChanged() {
148         mCaptureLayoutHelper.onPreviewAspectRatioChanged(mAspectRatio);
149         for (PreviewStatusListener.PreviewAspectRatioChangedListener listener
150                 : mAspectRatioChangedListeners) {
151             listener.onPreviewAspectRatioChanged(mAspectRatio);
152         }
153     }
154 
addAspectRatioChangedListener( PreviewStatusListener.PreviewAspectRatioChangedListener listener)155     public void addAspectRatioChangedListener(
156             PreviewStatusListener.PreviewAspectRatioChangedListener listener) {
157         if (listener != null && !mAspectRatioChangedListeners.contains(listener)) {
158             mAspectRatioChangedListeners.add(listener);
159         }
160     }
161 
162 
163     /**
164      * This returns the rect that is available to display the preview, and
165      * capture buttons
166      *
167      * @return the rect.
168      */
getFullscreenRect()169     public RectF getFullscreenRect() {
170         return mCaptureLayoutHelper.getFullscreenRect();
171     }
172 
173     /**
174      * This takes a matrix to apply to the texture view and uses the screen
175      * aspect ratio as the target aspect ratio
176      *
177      * @param matrix the matrix to apply
178      * @param aspectRatio the aspectRatio that the preview should be
179      */
updateTransformFullScreen(Matrix matrix, float aspectRatio)180     public void updateTransformFullScreen(Matrix matrix, float aspectRatio) {
181         aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
182         if (aspectRatio != mAspectRatio) {
183             setAspectRatio(aspectRatio);
184         }
185 
186         mPreview.setTransform(matrix);
187         mPreviewArea = mCaptureLayoutHelper.getPreviewRect();
188         onPreviewAreaChanged(mPreviewArea);
189 
190     }
191 
192     public void updateTransform(Matrix matrix) {
193         RectF previewRect = new RectF(0, 0, mWidth, mHeight);
194         matrix.mapRect(previewRect);
195 
196         float previewWidth = previewRect.width();
197         float previewHeight = previewRect.height();
198         if (previewHeight == 0 || previewWidth == 0) {
199             Log.e(TAG, "Invalid preview size: " + previewWidth + " x " + previewHeight);
200             return;
201         }
202         float aspectRatio = previewWidth / previewHeight;
203         aspectRatio = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
204         if (aspectRatio != mAspectRatio) {
205             setAspectRatio(aspectRatio);
206         }
207 
208         RectF previewAreaBasedOnAspectRatio = mCaptureLayoutHelper.getPreviewRect();
209         Matrix addtionalTransform = new Matrix();
210         addtionalTransform.setRectToRect(previewRect, previewAreaBasedOnAspectRatio,
211                 Matrix.ScaleToFit.CENTER);
212         matrix.postConcat(addtionalTransform);
213         mPreview.setTransform(matrix);
214         updatePreviewArea(matrix);
215     }
216 
217     /**
218      * Calculates and updates the preview area rect using the latest transform matrix.
219      */
220     private void updatePreviewArea(Matrix matrix) {
221         mPreviewArea.set(0, 0, mWidth, mHeight);
222         matrix.mapRect(mPreviewArea);
223         onPreviewAreaChanged(mPreviewArea);
224     }
225 
226     public void setOnLayoutChangeListener(OnLayoutChangeListener listener) {
227         mOnLayoutChangeListener = listener;
228     }
229 
230     public void setSurfaceTextureListener(TextureView.SurfaceTextureListener listener) {
231         mSurfaceTextureListener = listener;
232     }
233 
234     /**
235      * Updates the transform matrix based current width and height of TextureView
236      * and preview stream aspect ratio.
237      *
238      * <p>If not {@code mAutoAdjustTransform}, this does nothing except return
239      * {@code false}. In all other cases, it returns {@code true}, regardless of
240      * whether the transform was changed.</p>
241      *
242      * @return Whether {@code mAutoAdjustTransform}.
243      */
244     private boolean updateTransform() {
245         Log.v(TAG, "updateTransform");
246         if (!mAutoAdjustTransform) {
247             return false;
248         }
249         if (mAspectRatio == MATCH_SCREEN || mAspectRatio < 0 || mWidth == 0 || mHeight == 0) {
250             return true;
251         }
252 
253         Matrix matrix;
254         int cameraId = mCameraProvider.getCurrentCameraId();
255         if (cameraId >= 0) {
256             CameraDeviceInfo.Characteristics info = mCameraProvider.getCharacteristics(cameraId);
257             matrix = info.getPreviewTransform(mOrientation, new RectF(0, 0, mWidth, mHeight),
258                     mCaptureLayoutHelper.getPreviewRect());
259         } else {
260             Log.w(TAG, "Unable to find current camera... defaulting to identity matrix");
261             matrix = new Matrix();
262         }
263 
264         mPreview.setTransform(matrix);
265         updatePreviewArea(matrix);
266         return true;
267     }
268 
269     private void onPreviewAreaChanged(final RectF previewArea) {
270         // Notify listeners of preview area change
271         final List<PreviewStatusListener.PreviewAreaChangedListener> listeners =
272                 new ArrayList<PreviewStatusListener.PreviewAreaChangedListener>(
273                         mPreviewSizeChangedListeners);
274         // This method can be called during layout pass. We post a Runnable so
275         // that the callbacks won't happen during the layout pass.
276         mPreview.post(new Runnable() {
277             @Override
278             public void run() {
279                 for (PreviewStatusListener.PreviewAreaChangedListener listener : listeners) {
280                     listener.onPreviewAreaChanged(previewArea);
281                 }
282             }
283         });
284     }
285 
286     /**
287      * Returns a new copy of the preview area, to avoid internal data being modified
288      * from outside of the class.
289      */
290     public RectF getPreviewArea() {
291         return new RectF(mPreviewArea);
292     }
293 
294     /**
295      * Returns a copy of the area of the whole preview, including bits clipped
296      * by the view
297      */
298     public RectF getTextureArea() {
299 
300         if (mPreview == null) {
301             return new RectF();
302         }
303         Matrix matrix = new Matrix();
304         RectF area = new RectF(0, 0, mWidth, mHeight);
305         mPreview.getTransform(matrix).mapRect(area);
306         return area;
307     }
308 
309     public Bitmap getPreviewBitmap(int downsample) {
310         RectF textureArea = getTextureArea();
311         int width = (int) textureArea.width() / downsample;
312         int height = (int) textureArea.height() / downsample;
313         Bitmap preview = mPreview.getBitmap(width, height);
314         return Bitmap.createBitmap(preview, 0, 0, width, height, mPreview.getTransform(null), true);
315     }
316 
317     /**
318      * Adds a listener that will get notified when the preview area changed. This
319      * can be useful for UI elements or focus overlay to adjust themselves according
320      * to the preview area change.
321      * <p/>
322      * Note that a listener will only be added once. A newly added listener will receive
323      * a notification of current preview area immediately after being added.
324      * <p/>
325      * This function should be called on the UI thread and listeners will be notified
326      * on the UI thread.
327      *
328      * @param listener the listener that will get notified of preview area change
329      */
330     public void addPreviewAreaSizeChangedListener(
331             PreviewStatusListener.PreviewAreaChangedListener listener) {
332         if (listener != null && !mPreviewSizeChangedListeners.contains(listener)) {
333             mPreviewSizeChangedListeners.add(listener);
334             if (mPreviewArea.width() == 0 || mPreviewArea.height() == 0) {
335                 listener.onPreviewAreaChanged(new RectF(0, 0, mWidth, mHeight));
336             } else {
337                 listener.onPreviewAreaChanged(new RectF(mPreviewArea));
338             }
339         }
340     }
341 
342     /**
343      * Removes a listener that gets notified when the preview area changed.
344      *
345      * @param listener the listener that gets notified of preview area change
346      */
347     public void removePreviewAreaSizeChangedListener(
348             PreviewStatusListener.PreviewAreaChangedListener listener) {
349         if (listener != null && mPreviewSizeChangedListeners.contains(listener)) {
350             mPreviewSizeChangedListeners.remove(listener);
351         }
352     }
353 
354     @Override
355     public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
356         // Workaround for b/11168275, see b/10981460 for more details
357         if (mWidth != 0 && mHeight != 0) {
358             // Re-apply transform matrix for new surface texture
359             updateTransform();
360         }
361         if (mSurfaceTextureListener != null) {
362             mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height);
363         }
364     }
365 
366     @Override
367     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
368         if (mSurfaceTextureListener != null) {
369             mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height);
370         }
371     }
372 
373     @Override
374     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
375         if (mSurfaceTextureListener != null) {
376             mSurfaceTextureListener.onSurfaceTextureDestroyed(surface);
377         }
378         return false;
379     }
380 
381     @Override
382     public void onSurfaceTextureUpdated(SurfaceTexture surface) {
383         if (mSurfaceTextureListener != null) {
384             mSurfaceTextureListener.onSurfaceTextureUpdated(surface);
385         }
386 
387     }
388 }
389