Home | History | Annotate | Download | only in camera
      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 
     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