1 /* 2 * Copyright 2022 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 androidx.camera.integration.extensions.utils 18 19 import android.graphics.Matrix 20 import android.graphics.Rect 21 import android.graphics.RectF 22 import android.util.Size 23 import android.view.Surface 24 import android.view.TextureView 25 import androidx.core.graphics.toRect 26 27 // Normalized space (-1, -1) - (1, 1). 28 private val NORMALIZED_RECT = RectF(-1f, -1f, 1f, 1f) 29 30 object TransformUtil { 31 32 /** Converts [Surface] rotation to rotation degrees: 90, 180, 270 or 0. */ 33 @JvmStatic surfaceRotationToRotationDegreesnull34 fun surfaceRotationToRotationDegrees(rotationValue: Int): Int = 35 when (rotationValue) { 36 Surface.ROTATION_0 -> 0 37 Surface.ROTATION_90 -> 90 38 Surface.ROTATION_180 -> 180 39 Surface.ROTATION_270 -> 270 40 else -> 41 throw UnsupportedOperationException("Unsupported display rotation: $rotationValue") 42 } 43 44 /** 45 * Calculates the delta between a source rotation and destination rotation. 46 * 47 * <p>A typical use of this method would be calculating the angular difference between the 48 * display orientation (destRotationDegrees) and camera sensor orientation 49 * (sourceRotationDegrees). 50 * 51 * @param destRotationDegrees The destination rotation relative to the device's natural 52 * rotation. 53 * @param sourceRotationDegrees The source rotation relative to the device's natural rotation. 54 * @param isOppositeFacing Whether the source and destination planes are facing opposite 55 * directions. 56 */ 57 @JvmStatic calculateRelativeImageRotationDegreesnull58 fun calculateRelativeImageRotationDegrees( 59 destRotationDegrees: Int, 60 sourceRotationDegrees: Int, 61 isOppositeFacing: Boolean 62 ): Int = 63 if (isOppositeFacing) { 64 (sourceRotationDegrees - destRotationDegrees + 360) % 360 65 } else { 66 (sourceRotationDegrees + destRotationDegrees) % 360 67 } 68 69 /** 70 * Calculates the transformation and applies it to the inner view of [TextureView] preview. 71 * 72 * [TextureView] needs a preliminary correction since it doesn't handle the display rotation. 73 */ 74 @JvmStatic transformTextureViewnull75 fun transformTextureView( 76 preview: TextureView, 77 containerViewSize: Size, 78 resolution: Size, 79 targetRotation: Int, 80 sensorRotationDegrees: Int, 81 isOppositeFacing: Boolean 82 ) { 83 // For TextureView, correct the orientation to match the target rotation. 84 preview.setTransform(getTextureViewCorrectionMatrix(resolution, targetRotation)) 85 86 val surfaceRectInPreview = 87 getTransformedSurfaceRect( 88 containerViewSize, 89 resolution, 90 calculateRelativeImageRotationDegrees( 91 surfaceRotationToRotationDegrees(targetRotation), 92 sensorRotationDegrees, 93 isOppositeFacing 94 ) 95 ) 96 97 preview.pivotX = 0f 98 preview.pivotY = 0f 99 preview.scaleX = surfaceRectInPreview.width() / resolution.width 100 preview.scaleY = surfaceRectInPreview.height() / resolution.height 101 preview.translationX = surfaceRectInPreview.left - preview.left 102 preview.translationY = surfaceRectInPreview.top - preview.top 103 } 104 105 /** 106 * Creates a matrix that makes [TextureView]'s rotation matches the target rotation. 107 * 108 * The value should be applied by calling [TextureView.setTransform]. Usually the target 109 * rotation is the display rotation. In that case, this matrix will just make a [TextureView] 110 * works like a SurfaceView. If not, then it will further correct it to the desired rotation. 111 */ 112 @JvmStatic getTextureViewCorrectionMatrixnull113 private fun getTextureViewCorrectionMatrix(resolution: Size, targetRotation: Int): Matrix { 114 val surfaceRect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat()) 115 val rotationDegrees = -surfaceRotationToRotationDegrees(targetRotation) 116 return getRectToRect(surfaceRect, surfaceRect, rotationDegrees) 117 } 118 119 /** 120 * Gets the transform from one {@link Rect} to another with rotation degrees. 121 * 122 * <p> Following is how the source is mapped to the target with a 90° rotation. The rect <a, b, 123 * c, d> is mapped to <a', b', c', d'>. 124 * <pre> 125 * a----------b d'-----------a' 126 * | source | -90°-> | | 127 * d----------c | target | 128 * | | 129 * c'-----------b' 130 * </pre> 131 */ 132 @JvmStatic getRectToRectnull133 private fun getRectToRect(source: RectF, target: RectF, rotationDegrees: Int): Matrix = 134 Matrix().apply { 135 // Map source to normalized space. 136 setRectToRect(source, NORMALIZED_RECT, Matrix.ScaleToFit.FILL) 137 // Add rotation. 138 postRotate(rotationDegrees.toFloat()) 139 // Restore the normalized space to target's coordinates. 140 postConcat(getNormalizedToBuffer(target)) 141 } 142 143 /** Gets the transform from a normalized space (-1, -1) - (1, 1) to the given rect. */ 144 @JvmStatic getNormalizedToBuffernull145 private fun getNormalizedToBuffer(viewPortRect: RectF): Matrix = 146 Matrix().apply { setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL) } 147 148 /** 149 * Gets the transformed [Surface] rect in the preview coordinates. 150 * 151 * Returns desired rect of the inner view that once applied, the only part visible to end users 152 * is the crop rect. 153 */ 154 @JvmStatic getTransformedSurfaceRectnull155 private fun getTransformedSurfaceRect( 156 containerViewSize: Size, 157 resolution: Size, 158 rotationDegrees: Int 159 ): RectF { 160 val surfaceToPreviewMatrix = 161 getSurfaceToPreviewMatrix(containerViewSize, resolution, rotationDegrees) 162 val rect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat()) 163 surfaceToPreviewMatrix.mapRect(rect) 164 return rect 165 } 166 167 /** 168 * Calculates the transformation from [Surface] coordinates to the preview coordinates. 169 * 170 * The calculation is based on making the crop rect to center fill the preview. 171 */ 172 @JvmStatic getSurfaceToPreviewMatrixnull173 private fun getSurfaceToPreviewMatrix( 174 containerViewSize: Size, 175 resolution: Size, 176 rotationDegrees: Int 177 ): Matrix { 178 val surfaceRect = RectF(0f, 0f, resolution.width.toFloat(), resolution.height.toFloat()) 179 180 // Get the target of the mapping, the coordinates of the crop rect in the preview. 181 val previewCropRect = 182 getPreviewCropRect(containerViewSize, surfaceRect.toRect(), rotationDegrees) 183 184 return getRectToRect(surfaceRect, previewCropRect, rotationDegrees) 185 } 186 187 /** Gets the crop rect in the preview coordinates. */ 188 @JvmStatic getPreviewCropRectnull189 private fun getPreviewCropRect( 190 containerViewSize: Size, 191 surfaceCropRect: Rect, 192 rotationDegrees: Int 193 ): RectF { 194 val containerViewRect = 195 RectF(0f, 0f, containerViewSize.width.toFloat(), containerViewSize.height.toFloat()) 196 val rotatedCropRectSize = getRotatedCropRectSize(surfaceCropRect, rotationDegrees) 197 val rotatedCropRect = 198 RectF(0f, 0f, rotatedCropRectSize.width.toFloat(), rotatedCropRectSize.height.toFloat()) 199 200 Matrix().apply { 201 // android.graphics.Matrix doesn't support fill scale types. The workaround is 202 // mapping inversely from destination to source, then invert the matrix. 203 setRectToRect(containerViewRect, rotatedCropRect, Matrix.ScaleToFit.CENTER) 204 invert(this) 205 mapRect(rotatedCropRect) 206 } 207 208 return rotatedCropRect 209 } 210 211 /** Returns crop rect size with target rotation applied. */ 212 @JvmStatic getRotatedCropRectSizenull213 private fun getRotatedCropRectSize(surfaceCropRect: Rect, rotationDegrees: Int): Size = 214 if (is90or270(rotationDegrees)) { 215 Size(surfaceCropRect.height(), surfaceCropRect.width()) 216 } else Size(surfaceCropRect.width(), surfaceCropRect.height()) 217 218 /** Returns true if the rotation degrees is 90 or 270. */ 219 @JvmStatic is90or270null220 private fun is90or270(rotationDegrees: Int): Boolean { 221 if (rotationDegrees == 90 || rotationDegrees == 270) { 222 return true 223 } 224 if (rotationDegrees == 0 || rotationDegrees == 180) { 225 return false 226 } 227 throw IllegalArgumentException("Invalid rotation degrees: $rotationDegrees") 228 } 229 } 230