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