1 /* 2 * Copyright (C) 2012 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 package com.android.camera.crop; 17 18 import android.graphics.Matrix; 19 import android.graphics.Rect; 20 import android.graphics.RectF; 21 22 import java.util.Arrays; 23 24 /** 25 * Maintains invariant that inner rectangle is constrained to be within the 26 * outer, rotated rectangle. 27 */ 28 public class BoundedRect { 29 private float rot; 30 private RectF outer; 31 private RectF inner; 32 private float[] innerRotated; 33 34 public BoundedRect(float rotation, Rect outerRect, Rect innerRect) { 35 rot = rotation; 36 outer = new RectF(outerRect); 37 inner = new RectF(innerRect); 38 innerRotated = CropMath.getCornersFromRect(inner); 39 rotateInner(); 40 if (!isConstrained()) 41 reconstrain(); 42 } 43 44 public BoundedRect(float rotation, RectF outerRect, RectF innerRect) { 45 rot = rotation; 46 outer = new RectF(outerRect); 47 inner = new RectF(innerRect); 48 innerRotated = CropMath.getCornersFromRect(inner); 49 rotateInner(); 50 if (!isConstrained()) 51 reconstrain(); 52 } 53 54 public void resetTo(float rotation, RectF outerRect, RectF innerRect) { 55 rot = rotation; 56 outer.set(outerRect); 57 inner.set(innerRect); 58 innerRotated = CropMath.getCornersFromRect(inner); 59 rotateInner(); 60 if (!isConstrained()) 61 reconstrain(); 62 } 63 64 /** 65 * Sets inner, and re-constrains it to fit within the rotated bounding rect. 66 */ 67 public void setInner(RectF newInner) { 68 if (inner.equals(newInner)) 69 return; 70 inner = newInner; 71 innerRotated = CropMath.getCornersFromRect(inner); 72 rotateInner(); 73 if (!isConstrained()) 74 reconstrain(); 75 } 76 77 /** 78 * Sets rotation, and re-constrains inner to fit within the rotated bounding rect. 79 */ 80 public void setRotation(float rotation) { 81 if (rotation == rot) 82 return; 83 rot = rotation; 84 innerRotated = CropMath.getCornersFromRect(inner); 85 rotateInner(); 86 if (!isConstrained()) 87 reconstrain(); 88 } 89 90 public void setToInner(RectF r) { 91 r.set(inner); 92 } 93 94 public void setToOuter(RectF r) { 95 r.set(outer); 96 } 97 98 public RectF getInner() { 99 return new RectF(inner); 100 } 101 102 public RectF getOuter() { 103 return new RectF(outer); 104 } 105 106 /** 107 * Tries to move the inner rectangle by (dx, dy). If this would cause it to leave 108 * the bounding rectangle, snaps the inner rectangle to the edge of the bounding 109 * rectangle. 110 */ 111 public void moveInner(float dx, float dy) { 112 Matrix m0 = getInverseRotMatrix(); 113 114 RectF translatedInner = new RectF(inner); 115 translatedInner.offset(dx, dy); 116 117 float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner); 118 float[] outerCorners = CropMath.getCornersFromRect(outer); 119 120 m0.mapPoints(translatedInnerCorners); 121 float[] correction = { 122 0, 0 123 }; 124 125 // find correction vectors for corners that have moved out of bounds 126 for (int i = 0; i < translatedInnerCorners.length; i += 2) { 127 float correctedInnerX = translatedInnerCorners[i] + correction[0]; 128 float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; 129 if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) { 130 float[] badCorner = { 131 correctedInnerX, correctedInnerY 132 }; 133 float[] nearestSide = CropMath.closestSide(badCorner, outerCorners); 134 float[] correctionVec = 135 GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide); 136 correction[0] += correctionVec[0]; 137 correction[1] += correctionVec[1]; 138 } 139 } 140 141 for (int i = 0; i < translatedInnerCorners.length; i += 2) { 142 float correctedInnerX = translatedInnerCorners[i] + correction[0]; 143 float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; 144 if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) { 145 float[] correctionVec = { 146 correctedInnerX, correctedInnerY 147 }; 148 CropMath.getEdgePoints(outer, correctionVec); 149 correctionVec[0] -= correctedInnerX; 150 correctionVec[1] -= correctedInnerY; 151 correction[0] += correctionVec[0]; 152 correction[1] += correctionVec[1]; 153 } 154 } 155 156 // Set correction 157 for (int i = 0; i < translatedInnerCorners.length; i += 2) { 158 float correctedInnerX = translatedInnerCorners[i] + correction[0]; 159 float correctedInnerY = translatedInnerCorners[i + 1] + correction[1]; 160 // update translated corners with correction vectors 161 translatedInnerCorners[i] = correctedInnerX; 162 translatedInnerCorners[i + 1] = correctedInnerY; 163 } 164 165 innerRotated = translatedInnerCorners; 166 // reconstrain to update inner 167 reconstrain(); 168 } 169 170 /** 171 * Attempts to resize the inner rectangle. If this would cause it to leave 172 * the bounding rect, clips the inner rectangle to fit. 173 */ 174 public void resizeInner(RectF newInner) { 175 Matrix m = getRotMatrix(); 176 Matrix m0 = getInverseRotMatrix(); 177 178 float[] outerCorners = CropMath.getCornersFromRect(outer); 179 m.mapPoints(outerCorners); 180 float[] oldInnerCorners = CropMath.getCornersFromRect(inner); 181 float[] newInnerCorners = CropMath.getCornersFromRect(newInner); 182 RectF ret = new RectF(newInner); 183 184 for (int i = 0; i < newInnerCorners.length; i += 2) { 185 float[] c = { 186 newInnerCorners[i], newInnerCorners[i + 1] 187 }; 188 float[] c0 = Arrays.copyOf(c, 2); 189 m0.mapPoints(c0); 190 if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) { 191 float[] outerSide = CropMath.closestSide(c, outerCorners); 192 float[] pathOfCorner = { 193 newInnerCorners[i], newInnerCorners[i + 1], 194 oldInnerCorners[i], oldInnerCorners[i + 1] 195 }; 196 float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide); 197 if (p == null) { 198 // lines are parallel or not well defined, so don't resize 199 p = new float[2]; 200 p[0] = oldInnerCorners[i]; 201 p[1] = oldInnerCorners[i + 1]; 202 } 203 // relies on corners being in same order as method 204 // getCornersFromRect 205 switch (i) { 206 case 0: 207 case 1: 208 ret.left = (p[0] > ret.left) ? p[0] : ret.left; 209 ret.top = (p[1] > ret.top) ? p[1] : ret.top; 210 break; 211 case 2: 212 case 3: 213 ret.right = (p[0] < ret.right) ? p[0] : ret.right; 214 ret.top = (p[1] > ret.top) ? p[1] : ret.top; 215 break; 216 case 4: 217 case 5: 218 ret.right = (p[0] < ret.right) ? p[0] : ret.right; 219 ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom; 220 break; 221 case 6: 222 case 7: 223 ret.left = (p[0] > ret.left) ? p[0] : ret.left; 224 ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom; 225 break; 226 default: 227 break; 228 } 229 } 230 } 231 float[] retCorners = CropMath.getCornersFromRect(ret); 232 m0.mapPoints(retCorners); 233 innerRotated = retCorners; 234 // reconstrain to update inner 235 reconstrain(); 236 } 237 238 /** 239 * Attempts to resize the inner rectangle. If this would cause it to leave 240 * the bounding rect, clips the inner rectangle to fit while maintaining 241 * aspect ratio. 242 */ 243 public void fixedAspectResizeInner(RectF newInner) { 244 Matrix m = getRotMatrix(); 245 Matrix m0 = getInverseRotMatrix(); 246 247 float aspectW = inner.width(); 248 float aspectH = inner.height(); 249 float aspRatio = aspectW / aspectH; 250 float[] corners = CropMath.getCornersFromRect(outer); 251 252 m.mapPoints(corners); 253 float[] oldInnerCorners = CropMath.getCornersFromRect(inner); 254 float[] newInnerCorners = CropMath.getCornersFromRect(newInner); 255 256 // find fixed corner 257 int fixed = -1; 258 if (inner.top == newInner.top) { 259 if (inner.left == newInner.left) 260 fixed = 0; // top left 261 else if (inner.right == newInner.right) 262 fixed = 2; // top right 263 } else if (inner.bottom == newInner.bottom) { 264 if (inner.right == newInner.right) 265 fixed = 4; // bottom right 266 else if (inner.left == newInner.left) 267 fixed = 6; // bottom left 268 } 269 // no fixed corner, return without update 270 if (fixed == -1) 271 return; 272 float widthSoFar = newInner.width(); 273 int moved = -1; 274 for (int i = 0; i < newInnerCorners.length; i += 2) { 275 float[] c = { 276 newInnerCorners[i], newInnerCorners[i + 1] 277 }; 278 float[] c0 = Arrays.copyOf(c, 2); 279 m0.mapPoints(c0); 280 if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) { 281 moved = i; 282 if (moved == fixed) 283 continue; 284 float[] l2 = CropMath.closestSide(c, corners); 285 float[] l1 = { 286 newInnerCorners[i], newInnerCorners[i + 1], 287 oldInnerCorners[i], oldInnerCorners[i + 1] 288 }; 289 float[] p = GeometryMathUtils.lineIntersect(l1, l2); 290 if (p == null) { 291 // lines are parallel or not well defined, so set to old 292 // corner 293 p = new float[2]; 294 p[0] = oldInnerCorners[i]; 295 p[1] = oldInnerCorners[i + 1]; 296 } 297 // relies on corners being in same order as method 298 // getCornersFromRect 299 float fixed_x = oldInnerCorners[fixed]; 300 float fixed_y = oldInnerCorners[fixed + 1]; 301 float newWidth = Math.abs(fixed_x - p[0]); 302 float newHeight = Math.abs(fixed_y - p[1]); 303 newWidth = Math.max(newWidth, aspRatio * newHeight); 304 if (newWidth < widthSoFar) 305 widthSoFar = newWidth; 306 } 307 } 308 309 float heightSoFar = widthSoFar / aspRatio; 310 RectF ret = new RectF(inner); 311 if (fixed == 0) { 312 ret.right = ret.left + widthSoFar; 313 ret.bottom = ret.top + heightSoFar; 314 } else if (fixed == 2) { 315 ret.left = ret.right - widthSoFar; 316 ret.bottom = ret.top + heightSoFar; 317 } else if (fixed == 4) { 318 ret.left = ret.right - widthSoFar; 319 ret.top = ret.bottom - heightSoFar; 320 } else if (fixed == 6) { 321 ret.right = ret.left + widthSoFar; 322 ret.top = ret.bottom - heightSoFar; 323 } 324 float[] retCorners = CropMath.getCornersFromRect(ret); 325 m0.mapPoints(retCorners); 326 innerRotated = retCorners; 327 // reconstrain to update inner 328 reconstrain(); 329 } 330 331 // internal methods 332 333 private boolean isConstrained() { 334 for (int i = 0; i < 8; i += 2) { 335 if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1])) 336 return false; 337 } 338 return true; 339 } 340 341 private void reconstrain() { 342 // innerRotated has been changed to have incorrect values 343 CropMath.getEdgePoints(outer, innerRotated); 344 Matrix m = getRotMatrix(); 345 float[] unrotated = Arrays.copyOf(innerRotated, 8); 346 m.mapPoints(unrotated); 347 inner = CropMath.trapToRect(unrotated); 348 } 349 350 private void rotateInner() { 351 Matrix m = getInverseRotMatrix(); 352 m.mapPoints(innerRotated); 353 } 354 355 private Matrix getRotMatrix() { 356 Matrix m = new Matrix(); 357 m.setRotate(rot, outer.centerX(), outer.centerY()); 358 return m; 359 } 360 361 private Matrix getInverseRotMatrix() { 362 Matrix m = new Matrix(); 363 m.setRotate(-rot, outer.centerX(), outer.centerY()); 364 return m; 365 } 366 } 367