1 /*
2  * Copyright (C) 2014 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.Context;
20 import android.graphics.Matrix;
21 import android.graphics.RectF;
22 
23 import com.android.camera.app.CameraApp;
24 import com.android.camera.app.CameraAppUI;
25 import com.android.camera.ui.PreviewStatusListener;
26 import com.android.camera2.R;
27 
28 /**
29  * This class centralizes the logic of how bottom bar should be laid out and how
30  * preview should be transformed. The two things that could affect bottom bar layout
31  * and preview transform are: window size and preview aspect ratio. Once these two
32  * things are set, the layout of bottom bar and preview rect will be calculated
33  * and can then be queried anywhere inside the app.
34  *
35  * Note that this helper assumes that preview TextureView will be laid out full
36  * screen, meaning all its ascendants are laid out with MATCH_PARENT flags. If
37  * or when this assumption is no longer the case, we need to revisit this logic.
38  */
39 public class CaptureLayoutHelper implements CameraAppUI.NonDecorWindowSizeChangedListener,
40         PreviewStatusListener.PreviewAspectRatioChangedListener {
41 
42     private final int mBottomBarMinHeight;
43     private final int mBottomBarMaxHeight;
44     private final int mBottomBarOptimalHeight;
45 
46     private int mWindowWidth = 0;
47     private int mWindowHeight = 0;
48     /** Aspect ratio of preview. It could be 0, meaning match the screen aspect ratio,
49      * or a float value no less than 1f.
50      */
51     private float mAspectRatio = TextureViewHelper.MATCH_SCREEN;
52     private PositionConfiguration mPositionConfiguration = null;
53     private int mRotation = 0;
54     private boolean mShowBottomBar = true;
55 
56     /**
57      * PositionConfiguration contains the layout info for bottom bar and preview
58      * rect, as well as whether bottom bar should be overlaid on top of preview.
59      */
60     public static final class PositionConfiguration {
61         /**
62          * This specifies the rect of preview on screen.
63          */
64         public final RectF mPreviewRect = new RectF();
65         /**
66          * This specifies the rect where bottom bar should be laid out in.
67          */
68         public final RectF mBottomBarRect = new RectF();
69         /**
70          * This indicates whether bottom bar should overlay itself on top of preview.
71          */
72         public boolean mBottomBarOverlay = false;
73     }
74 
CaptureLayoutHelper(int bottomBarMinHeight, int bottomBarMaxHeight, int bottomBarOptimalHeight)75     public CaptureLayoutHelper(int bottomBarMinHeight, int bottomBarMaxHeight,
76             int bottomBarOptimalHeight) {
77         mBottomBarMinHeight = bottomBarMinHeight;
78         mBottomBarMaxHeight = bottomBarMaxHeight;
79         mBottomBarOptimalHeight = bottomBarOptimalHeight;
80     }
81 
82     @Override
onPreviewAspectRatioChanged(float aspectRatio)83     public void onPreviewAspectRatioChanged(float aspectRatio) {
84         if (mAspectRatio == aspectRatio) {
85             return;
86         }
87         mAspectRatio = aspectRatio;
88         updatePositionConfiguration();
89     }
90 
91     /**
92      * Sets whether bottom bar will show or not. This will affect the calculation
93      * of uncovered preview area, which is used to lay out mode list, mode options,
94      * etc.
95      */
setShowBottomBar(boolean showBottomBar)96     public void setShowBottomBar(boolean showBottomBar) {
97         mShowBottomBar = showBottomBar;
98     }
99 
100     /**
101      * Updates bottom bar rect and preview rect. This gets called whenever
102      * preview aspect ratio changes or main activity layout size changes.
103      */
updatePositionConfiguration()104     private void updatePositionConfiguration() {
105         if (mWindowWidth == 0 || mWindowHeight == 0) {
106             return;
107         }
108         mPositionConfiguration = getPositionConfiguration(mWindowWidth, mWindowHeight, mAspectRatio,
109                 mRotation);
110     }
111 
112     /**
113      * Returns the rect that bottom bar should be laid out in. If not enough info
114      * has been provided to calculate this, return an empty rect. Note that the rect
115      * returned is relative to the content layout of the activity. It may need to be
116      * translated based on the parent view's location.
117      */
getBottomBarRect()118     public RectF getBottomBarRect() {
119         if (mPositionConfiguration == null) {
120             updatePositionConfiguration();
121         }
122         // Not enough info to create a position configuration.
123         if (mPositionConfiguration == null) {
124             return new RectF();
125         }
126         return new RectF(mPositionConfiguration.mBottomBarRect);
127     }
128 
129     /**
130      * Returns the rect that preview should occupy based on aspect ratio. If not
131      * enough info has been provided to calculate this, return an empty rect. Note
132      * that the rect returned is relative to the content layout of the activity.
133      * It may need to be translated based on the parent view's location.
134      */
getPreviewRect()135     public RectF getPreviewRect() {
136         if (mPositionConfiguration == null) {
137             updatePositionConfiguration();
138         }
139         // Not enough info to create a position configuration.
140         if (mPositionConfiguration == null) {
141             return new RectF();
142         }
143         return new RectF(mPositionConfiguration.mPreviewRect);
144     }
145 
146     /**
147      * This returns the rect that is available to display the preview, and
148      * capture buttons
149      *
150      * @return the rect.
151      */
getFullscreenRect()152     public RectF getFullscreenRect() {
153         return new RectF(0, 0, mWindowWidth, mWindowHeight);
154     }
155 
156     /**
157      * Returns the sub-rect of the preview that is not being blocked by the
158      * bottom bar. This can be used to lay out mode options, settings button,
159      * etc. If not enough info has been provided to calculate this, return an
160      * empty rect. Note that the rect returned is relative to the content layout
161      * of the activity. It may need to be translated based on the parent view's
162      * location.
163      */
getUncoveredPreviewRect()164     public RectF getUncoveredPreviewRect() {
165         if (mPositionConfiguration == null) {
166             updatePositionConfiguration();
167         }
168         // Not enough info to create a position configuration.
169         if (mPositionConfiguration == null) {
170             return new RectF();
171         }
172 
173         if (!RectF.intersects(mPositionConfiguration.mBottomBarRect,
174                 mPositionConfiguration.mPreviewRect) || !mShowBottomBar) {
175             return mPositionConfiguration.mPreviewRect;
176         }
177 
178         if (mWindowHeight > mWindowWidth) {
179             // Portrait.
180             if (mRotation >= 180) {
181                 // Reverse portrait, bottom bar align top.
182                 return new RectF(mPositionConfiguration.mPreviewRect.left,
183                         mPositionConfiguration.mBottomBarRect.bottom,
184                         mPositionConfiguration.mPreviewRect.right,
185                         mPositionConfiguration.mPreviewRect.bottom);
186             } else {
187                 return new RectF(mPositionConfiguration.mPreviewRect.left,
188                         mPositionConfiguration.mPreviewRect.top,
189                         mPositionConfiguration.mPreviewRect.right,
190                         mPositionConfiguration.mBottomBarRect.top);
191             }
192         } else {
193             if (mRotation >= 180) {
194                 // Reverse landscape, bottom bar align left.
195                 return new RectF(mPositionConfiguration.mBottomBarRect.right,
196                         mPositionConfiguration.mPreviewRect.top,
197                         mPositionConfiguration.mPreviewRect.right,
198                         mPositionConfiguration.mPreviewRect.bottom);
199             } else {
200                 return new RectF(mPositionConfiguration.mPreviewRect.left,
201                         mPositionConfiguration.mPreviewRect.top,
202                         mPositionConfiguration.mBottomBarRect.left,
203                         mPositionConfiguration.mPreviewRect.bottom);
204             }
205         }
206     }
207 
208     /**
209      * Returns whether the bottom bar should be transparent and overlaid on top
210      * of the preview.
211      */
shouldOverlayBottomBar()212     public boolean shouldOverlayBottomBar() {
213         if (mPositionConfiguration == null) {
214             updatePositionConfiguration();
215         }
216         // Not enough info to create a position configuration.
217         if (mPositionConfiguration == null) {
218             return false;
219         }
220         return mPositionConfiguration.mBottomBarOverlay;
221     }
222 
223     @Override
onNonDecorWindowSizeChanged(int width, int height, int rotation)224     public void onNonDecorWindowSizeChanged(int width, int height, int rotation) {
225         mWindowWidth = width;
226         mWindowHeight = height;
227         mRotation = rotation;
228         updatePositionConfiguration();
229     }
230 
231     /**
232      * Calculates the layout rect of bottom bar and the size of preview based on
233      * activity layout width, height and aspect ratio.
234      *
235      * @param width width of the main activity layout, excluding system decor such
236      *              as status bar, nav bar, etc.
237      * @param height height of the main activity layout, excluding system decor
238      *               such as status bar, nav bar, etc.
239      * @param previewAspectRatio aspect ratio of the preview
240      * @param rotation rotation from the natural orientation
241      * @return a custom position configuration that contains bottom bar rect,
242      *         preview rect and whether bottom bar should be overlaid.
243      */
getPositionConfiguration(int width, int height, float previewAspectRatio, int rotation)244     private PositionConfiguration getPositionConfiguration(int width, int height,
245             float previewAspectRatio, int rotation) {
246         boolean landscape = width > height;
247 
248         // If the aspect ratio is defined as fill the screen, then preview should
249         // take the screen rect.
250         PositionConfiguration config = new PositionConfiguration();
251         if (previewAspectRatio == TextureViewHelper.MATCH_SCREEN) {
252             config.mPreviewRect.set(0, 0, width, height);
253             config.mBottomBarOverlay = true;
254             if (landscape) {
255                 config.mBottomBarRect.set(width - mBottomBarOptimalHeight, 0, width, height);
256             } else {
257                 config.mBottomBarRect.set(0, height - mBottomBarOptimalHeight, width, height);
258             }
259         } else {
260             if (previewAspectRatio < 1) {
261                 previewAspectRatio = 1 / previewAspectRatio;
262             }
263             // Get the bottom bar width and height.
264             float barSize;
265             int longerEdge = Math.max(width, height);
266             int shorterEdge = Math.min(width, height);
267 
268             // Check the remaining space if fit short edge.
269             float spaceNeededAlongLongerEdge = shorterEdge * previewAspectRatio;
270             float remainingSpaceAlongLongerEdge = longerEdge - spaceNeededAlongLongerEdge;
271 
272             float previewShorterEdge;
273             float previewLongerEdge;
274             if (remainingSpaceAlongLongerEdge <= 0) {
275                 // Preview aspect ratio > screen aspect ratio: fit longer edge.
276                 previewLongerEdge = longerEdge;
277                 previewShorterEdge = longerEdge / previewAspectRatio;
278                 barSize = mBottomBarOptimalHeight;
279                 config.mBottomBarOverlay = true;
280 
281                 if (landscape) {
282                     config.mPreviewRect.set(0, height / 2 - previewShorterEdge / 2, previewLongerEdge,
283                             height / 2 + previewShorterEdge / 2);
284                     config.mBottomBarRect.set(width - barSize, height / 2 - previewShorterEdge / 2,
285                             width, height / 2 + previewShorterEdge / 2);
286                 } else {
287                     config.mPreviewRect.set(width / 2 - previewShorterEdge / 2, 0,
288                             width / 2 + previewShorterEdge / 2, previewLongerEdge);
289                     config.mBottomBarRect.set(width / 2 - previewShorterEdge / 2, height - barSize,
290                             width / 2 + previewShorterEdge / 2, height);
291                 }
292             } else if (previewAspectRatio > 14f / 9f) {
293                 // If the preview aspect ratio is large enough, simply offset the
294                 // preview to the bottom/right.
295                 // TODO: This logic needs some refinement.
296                 barSize = mBottomBarOptimalHeight;
297                 previewShorterEdge = shorterEdge;
298                 previewLongerEdge = shorterEdge * previewAspectRatio;
299                 config.mBottomBarOverlay = true;
300                 if (landscape) {
301                     float right = width;
302                     float left = right - previewLongerEdge;
303                     config.mPreviewRect.set(left, 0, right, previewShorterEdge);
304                     config.mBottomBarRect.set(width - barSize, 0, width, height);
305                 } else {
306                     float bottom = height;
307                     float top = bottom - previewLongerEdge;
308                     config.mPreviewRect.set(0, top, previewShorterEdge, bottom);
309                     config.mBottomBarRect.set(0, height - barSize, width, height);
310                 }
311             } else if (remainingSpaceAlongLongerEdge <= mBottomBarMinHeight) {
312                 // Need to scale down the preview to fit in the space excluding the bottom bar.
313                 previewLongerEdge = longerEdge - mBottomBarMinHeight;
314                 previewShorterEdge = previewLongerEdge / previewAspectRatio;
315                 barSize = mBottomBarMinHeight;
316                 config.mBottomBarOverlay = false;
317                 if (landscape) {
318                     config.mPreviewRect.set(0, height / 2 - previewShorterEdge / 2, previewLongerEdge,
319                             height / 2 + previewShorterEdge / 2);
320                     config.mBottomBarRect.set(width - barSize, height / 2 - previewShorterEdge / 2,
321                             width, height / 2 + previewShorterEdge / 2);
322                 } else {
323                     config.mPreviewRect.set(width / 2 - previewShorterEdge / 2, 0,
324                             width / 2 + previewShorterEdge / 2, previewLongerEdge);
325                     config.mBottomBarRect.set(width / 2 - previewShorterEdge / 2, height - barSize,
326                             width / 2 + previewShorterEdge / 2, height);
327                 }
328             } else {
329                 // Fit shorter edge.
330                 barSize = remainingSpaceAlongLongerEdge <= mBottomBarMaxHeight ?
331                         remainingSpaceAlongLongerEdge : mBottomBarMaxHeight;
332                 previewShorterEdge = shorterEdge;
333                 previewLongerEdge = shorterEdge * previewAspectRatio;
334                 config.mBottomBarOverlay = false;
335                 if (landscape) {
336                     float right = width - barSize;
337                     float left = right - previewLongerEdge;
338                     config.mPreviewRect.set(left, 0, right, previewShorterEdge);
339                     config.mBottomBarRect.set(width - barSize, 0, width, height);
340                 } else {
341                     float bottom = height - barSize;
342                     float top = bottom - previewLongerEdge;
343                     config.mPreviewRect.set(0, top, previewShorterEdge, bottom);
344                     config.mBottomBarRect.set(0, height - barSize, width, height);
345                 }
346             }
347         }
348 
349         if (rotation >= 180) {
350             // Rotate 180 degrees.
351             Matrix rotate = new Matrix();
352             rotate.setRotate(180, width / 2, height / 2);
353 
354             rotate.mapRect(config.mPreviewRect);
355             rotate.mapRect(config.mBottomBarRect);
356         }
357 
358         // Round the rect first to avoid rounding errors later on.
359         round(config.mBottomBarRect);
360         round(config.mPreviewRect);
361 
362         return config;
363     }
364 
365     /**
366      * Round the float coordinates in the given rect, and store the rounded value
367      * back in the rect.
368      */
round(RectF rect)369     public static void round(RectF rect) {
370         if (rect == null) {
371             return;
372         }
373         float left = Math.round(rect.left);
374         float top = Math.round(rect.top);
375         float right = Math.round(rect.right);
376         float bottom = Math.round(rect.bottom);
377         rect.set(left, top, right, bottom);
378     }
379 }
380