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
17package com.android.camera;
18
19import android.content.Context;
20import android.graphics.Matrix;
21import android.graphics.RectF;
22
23import com.android.camera.app.CameraApp;
24import com.android.camera.app.CameraAppUI;
25import com.android.camera.ui.PreviewStatusListener;
26import 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 */
39public 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
75    public CaptureLayoutHelper(int bottomBarMinHeight, int bottomBarMaxHeight,
76            int bottomBarOptimalHeight) {
77        mBottomBarMinHeight = bottomBarMinHeight;
78        mBottomBarMaxHeight = bottomBarMaxHeight;
79        mBottomBarOptimalHeight = bottomBarOptimalHeight;
80    }
81
82    @Override
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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