1 /* 2 * Copyright 2021 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.view; 18 19 import static java.lang.Math.abs; 20 import static java.lang.Math.round; 21 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Matrix; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.graphics.RectF; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Environment; 35 import android.provider.MediaStore; 36 import android.util.Log; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.Toast; 41 import android.widget.ToggleButton; 42 43 import androidx.annotation.MainThread; 44 import androidx.annotation.OptIn; 45 import androidx.camera.core.CameraSelector; 46 import androidx.camera.core.ImageAnalysis; 47 import androidx.camera.core.ImageCapture; 48 import androidx.camera.core.ImageCaptureException; 49 import androidx.camera.core.ImageProxy; 50 import androidx.camera.core.Logger; 51 import androidx.camera.view.LifecycleCameraController; 52 import androidx.camera.view.PreviewView; 53 import androidx.camera.view.TransformExperimental; 54 import androidx.camera.view.transform.CoordinateTransform; 55 import androidx.camera.view.transform.FileTransformFactory; 56 import androidx.camera.view.transform.ImageProxyTransformFactory; 57 import androidx.camera.view.transform.OutputTransform; 58 import androidx.exifinterface.media.ExifInterface; 59 import androidx.fragment.app.Fragment; 60 61 import org.jspecify.annotations.NonNull; 62 import org.jspecify.annotations.Nullable; 63 64 import java.io.File; 65 import java.io.FileInputStream; 66 import java.io.FileOutputStream; 67 import java.io.IOException; 68 import java.io.InputStream; 69 import java.io.OutputStream; 70 import java.util.concurrent.ExecutorService; 71 import java.util.concurrent.Executors; 72 73 /** 74 * A fragment that demos transform utilities. 75 */ 76 @OptIn(markerClass = TransformExperimental.class) 77 public final class TransformFragment extends Fragment { 78 79 private static final String TAG = "TransformFragment"; 80 81 private static final int TILE_COUNT = 4; 82 public static final RectF NORMALIZED_RECT = new RectF(-1, -1, 1, 1); 83 84 private LifecycleCameraController mCameraController; 85 private ExecutorService mExecutorService; 86 private ToggleButton mMirror; 87 private ToggleButton mCameraToggle; 88 89 // Synthetic access 90 @SuppressWarnings("WeakerAccess") 91 PreviewView mPreviewView; 92 // Synthetic access 93 @SuppressWarnings("WeakerAccess") 94 OverlayView mOverlayView; 95 96 // The following two variables should only be accessed from mExecutorService. 97 // Synthetic access 98 @SuppressWarnings("WeakerAccess") 99 @Nullable OutputTransform mImageProxyTransform; 100 // Synthetic access 101 @SuppressWarnings("WeakerAccess") 102 @Nullable RectF mBrightestTile; 103 104 private FileTransformFactory mFileTransformFactoryWithoutExif; 105 private FileTransformFactory mFileTransformFactoryWithExif; 106 107 private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() { 108 109 private final ImageProxyTransformFactory mImageProxyTransformFactory = 110 new ImageProxyTransformFactory(); 111 112 @Override 113 @OptIn(markerClass = TransformExperimental.class) 114 public void analyze(@NonNull ImageProxy imageProxy) { 115 // Find the brightest tile to highlight. 116 mBrightestTile = findBrightestTile(imageProxy); 117 mImageProxyTransform = 118 mImageProxyTransformFactory.getOutputTransform(imageProxy); 119 imageProxy.close(); 120 121 // Take a snapshot of the analyze result for thread safety. 122 final RectF brightestTile = new RectF(mBrightestTile); 123 final OutputTransform imageProxyTransform = mImageProxyTransform; 124 125 // Calculate PreviewView transform on UI thread. 126 mOverlayView.post(() -> { 127 // Calculate the transform. 128 RectF brightestTileInPreviewView = getBrightestTileInPreviewView( 129 imageProxyTransform, brightestTile); 130 if (brightestTileInPreviewView == null) { 131 // PreviewView transform info is not ready. No-op. 132 return; 133 } 134 135 // Draw the tile on top of PreviewView. 136 mOverlayView.setTileRect(brightestTileInPreviewView); 137 mOverlayView.postInvalidate(); 138 }); 139 } 140 }; 141 142 /** 143 * Gets the transform matrix based on exif orientation. 144 * 145 * <p> A forked version of {@code TransformUtil#getExifTransform} to make the test app 146 * self-contained. 147 */ getExifTransform(int exifOrientation, int width, int height)148 public static @NonNull Matrix getExifTransform(int exifOrientation, int width, int height) { 149 Matrix matrix = new Matrix(); 150 151 // Map the bitmap to a normalized space (-1, -1) - (1, 1) and perform transform in the 152 // normalized space. 153 RectF rect = new RectF(0, 0, width, height); 154 matrix.setRectToRect(rect, NORMALIZED_RECT, Matrix.ScaleToFit.FILL); 155 156 // A flag that check if the image has been rotated 90/270. 157 boolean isWidthHeightSwapped = false; 158 159 // Transform the normalized space based on exif orientation. 160 switch (exifOrientation) { 161 case android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 162 matrix.postScale(-1f, 1f); 163 break; 164 case android.media.ExifInterface.ORIENTATION_ROTATE_180: 165 matrix.postRotate(180); 166 break; 167 case android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL: 168 matrix.postScale(1f, -1f); 169 break; 170 case android.media.ExifInterface.ORIENTATION_TRANSPOSE: 171 // Flipped about top-left <--> bottom-right axis, it can also be represented by 172 // flip horizontally and then rotate 270 degree clockwise. 173 matrix.postScale(-1f, 1f); 174 matrix.postRotate(270); 175 isWidthHeightSwapped = true; 176 break; 177 case android.media.ExifInterface.ORIENTATION_ROTATE_90: 178 matrix.postRotate(90); 179 isWidthHeightSwapped = true; 180 break; 181 case android.media.ExifInterface.ORIENTATION_TRANSVERSE: 182 // Flipped about top-right <--> bottom-left axis, it can also be 183 // represented by flip horizontally and then rotate 90 degree clockwise. 184 matrix.postScale(-1f, 1f); 185 matrix.postRotate(90); 186 isWidthHeightSwapped = true; 187 break; 188 case android.media.ExifInterface.ORIENTATION_ROTATE_270: 189 matrix.postRotate(270); 190 isWidthHeightSwapped = true; 191 break; 192 case android.media.ExifInterface.ORIENTATION_NORMAL: 193 // Fall-through 194 case android.media.ExifInterface.ORIENTATION_UNDEFINED: 195 // Fall-through 196 default: 197 break; 198 } 199 200 // Map the normalized space back to the bitmap coordinates. 201 RectF restoredRect = isWidthHeightSwapped ? new RectF(0, 0, height, width) : rect; 202 Matrix restore = new Matrix(); 203 restore.setRectToRect(NORMALIZED_RECT, restoredRect, Matrix.ScaleToFit.FILL); 204 matrix.postConcat(restore); 205 return matrix; 206 } 207 208 /** 209 * Finds the brightest tile in the given {@link ImageProxy}. 210 * 211 * <p> Divides the crop rect of the image into a 4x4 grid, and find the brightest tile 212 * among the 16 tiles. 213 * 214 * @return the box of the brightest tile. 215 */ 216 // Synthetic access 217 @SuppressWarnings("WeakerAccess") findBrightestTile(ImageProxy image)218 RectF findBrightestTile(ImageProxy image) { 219 // Divide the crop rect in to 4x4 tiles. 220 Rect cropRect = image.getCropRect(); 221 int[][] tiles = new int[TILE_COUNT][TILE_COUNT]; 222 int tileWidth = cropRect.width() / TILE_COUNT; 223 int tileHeight = cropRect.height() / TILE_COUNT; 224 225 // Loop through the y plane and get the sum of the luminance for each tile. 226 byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()]; 227 image.getPlanes()[0].getBuffer().get(bytes); 228 int tileX; 229 int tileY; 230 for (int x = 0; x < cropRect.width(); x++) { 231 for (int y = 0; y < cropRect.height(); y++) { 232 tileX = Math.min(x / tileWidth, TILE_COUNT - 1); 233 tileY = Math.min(y / tileHeight, TILE_COUNT - 1); 234 tiles[tileX][tileY] += 235 bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF; 236 } 237 } 238 239 // Find the brightest tile among the 16 tiles. 240 float maxLuminance = 0; 241 int brightestTileX = 0; 242 int brightestTileY = 0; 243 for (int i = 0; i < TILE_COUNT; i++) { 244 for (int j = 0; j < TILE_COUNT; j++) { 245 if (tiles[i][j] > maxLuminance) { 246 maxLuminance = tiles[i][j]; 247 brightestTileX = i; 248 brightestTileY = j; 249 } 250 } 251 } 252 253 // Return the rectangle of the tile. 254 return new RectF(brightestTileX * tileWidth + cropRect.left, 255 brightestTileY * tileHeight + cropRect.top, 256 (brightestTileX + 1) * tileWidth + cropRect.left, 257 (brightestTileY + 1) * tileHeight + cropRect.top); 258 } 259 260 @Override 261 @OptIn(markerClass = TransformExperimental.class) onCreateView( @onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)262 public @NonNull View onCreateView( 263 @NonNull LayoutInflater inflater, 264 @Nullable ViewGroup container, 265 @Nullable Bundle savedInstanceState) { 266 mFileTransformFactoryWithoutExif = new FileTransformFactory(); 267 mFileTransformFactoryWithExif = new FileTransformFactory(); 268 mFileTransformFactoryWithExif.setUsingExifOrientation(true); 269 mExecutorService = Executors.newSingleThreadExecutor(); 270 mCameraController = new LifecycleCameraController(requireContext()); 271 mCameraController.bindToLifecycle(getViewLifecycleOwner()); 272 273 View view = inflater.inflate(R.layout.transform_view, container, false); 274 275 mPreviewView = view.findViewById(R.id.preview_view); 276 // Set to compatible so the custom transform (e.g. mirroring) would work. 277 mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE); 278 mPreviewView.setController(mCameraController); 279 280 mCameraController.setImageAnalysisAnalyzer(mExecutorService, mAnalyzer); 281 282 mOverlayView = view.findViewById(R.id.overlay_view); 283 mMirror = view.findViewById(R.id.mirror_preview); 284 mMirror.setOnCheckedChangeListener((buttonView, isChecked) -> updateMirrorState()); 285 286 mCameraToggle = view.findViewById(R.id.toggle_camera); 287 mCameraToggle.setOnCheckedChangeListener( 288 (buttonView, isChecked) -> updateCameraOrientation()); 289 290 view.findViewById(R.id.capture).setOnClickListener( 291 v -> saveHighlightedFilePreservingExif()); 292 293 view.findViewById(R.id.capture_and_transform).setOnClickListener( 294 v -> saveHighlightedUriWithoutExif()); 295 296 updateMirrorState(); 297 updateCameraOrientation(); 298 return view; 299 } 300 301 // Synthetic access 302 @SuppressWarnings("WeakerAccess") showToast(String message)303 void showToast(String message) { 304 requireActivity().runOnUiThread( 305 () -> Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()); 306 } 307 createDefaultPictureFolderIfNotExist()308 private void createDefaultPictureFolderIfNotExist() { 309 File pictureFolder = Environment.getExternalStoragePublicDirectory( 310 Environment.DIRECTORY_PICTURES); 311 if (!pictureFolder.exists()) { 312 if (!pictureFolder.mkdir()) { 313 Log.e(TAG, "Failed to create directory: " + pictureFolder); 314 } 315 } 316 } 317 318 /** 319 * Takes a picture, applies the exif info, highlights the brightest tile and saves it to 320 * MediaStore without exif info. 321 */ saveHighlightedUriWithoutExif()322 private void saveHighlightedUriWithoutExif() { 323 // Take a picture and save to MediaStore 324 createDefaultPictureFolderIfNotExist(); 325 ContentValues contentValues = new ContentValues(); 326 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); 327 ImageCapture.OutputFileOptions outputFileOptions = 328 new ImageCapture.OutputFileOptions.Builder( 329 requireContext().getContentResolver(), 330 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 331 contentValues).build(); 332 333 mCameraController.takePicture(outputFileOptions, mExecutorService, 334 new ImageCapture.OnImageSavedCallback() { 335 @Override 336 public void onImageSaved( 337 ImageCapture.@NonNull OutputFileResults outputFileResults) { 338 if (mImageProxyTransform == null || mBrightestTile == null) { 339 Logger.d(TAG, "ImageAnalysis result not ready."); 340 return; 341 } 342 Uri uri = outputFileResults.getSavedUri(); 343 if (uri == null) { 344 showToast("Saved URI should not be null."); 345 return; 346 } 347 try { 348 RectF tileInUri = getBrightestTileInUriWithExif( 349 uri, 350 mImageProxyTransform, 351 mBrightestTile); 352 Bitmap bitmap = loadBitmapWithExifApplied(uri); 353 drawRectOnBitmap(bitmap, tileInUri); 354 saveBitmapToUri(bitmap, uri); 355 showToast("Image saved."); 356 } catch (IOException e) { 357 showToast("Failed to draw on file. " + e); 358 } 359 } 360 361 @Override 362 public void onError(@NonNull ImageCaptureException exception) { 363 showToast("Failed to capture image. " + exception); 364 } 365 }); 366 } 367 368 /** 369 * Loads {@link Bitmap} from the given {@link Uri} and applies exif info to the {@link Bitmap}. 370 */ 371 // Synthetic access 372 @SuppressWarnings("WeakerAccess") loadBitmapWithExifApplied(Uri uri)373 @NonNull Bitmap loadBitmapWithExifApplied(Uri uri) throws IOException { 374 // Loads bitmap. 375 BitmapFactory.Options options = new BitmapFactory.Options(); 376 options.inMutable = true; 377 Bitmap original; 378 try (InputStream inputStream = requireContext().getContentResolver().openInputStream(uri)) { 379 original = BitmapFactory.decodeStream(inputStream, /*outPadding*/ null, options); 380 } 381 382 // Reads exif orientation. 383 int exifOrientation; 384 try (InputStream inputStream = requireContext().getContentResolver().openInputStream(uri)) { 385 ExifInterface exifInterface = new ExifInterface(inputStream); 386 exifOrientation = exifInterface.getAttributeInt( 387 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 388 } 389 Matrix matrix = getExifTransform(exifOrientation, original.getWidth(), 390 original.getHeight()); 391 392 // Calculate the bitmap size with exif applied. 393 float[] sizeVector = new float[]{original.getWidth(), original.getHeight()}; 394 matrix.mapVectors(sizeVector); 395 396 // Create a new bitmap with exif applied. 397 Bitmap bitmapWithExif = Bitmap.createBitmap( 398 round(abs(sizeVector[0])), 399 round(abs(sizeVector[1])), 400 Bitmap.Config.ARGB_8888); 401 Canvas canvas = new Canvas(bitmapWithExif); 402 Paint paint = new Paint(); 403 paint.setAntiAlias(true); 404 canvas.drawBitmap(original, matrix, paint); 405 return bitmapWithExif; 406 } 407 408 // Synthetic access 409 @SuppressWarnings("WeakerAccess") saveBitmapToUri(@onNull Bitmap bitmap, @NonNull Uri uri)410 void saveBitmapToUri(@NonNull Bitmap bitmap, @NonNull Uri uri) throws IOException { 411 try (OutputStream outputStream = 412 requireContext().getContentResolver().openOutputStream(uri)) { 413 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); 414 } 415 } 416 417 /** 418 * Takes a picture, highlights the brightest tile and saves it to MediaStore preserving Exif 419 * info. 420 */ saveHighlightedFilePreservingExif()421 private void saveHighlightedFilePreservingExif() { 422 // Create an internal temp file for drawing an overlay. 423 File tempFile; 424 try { 425 tempFile = File.createTempFile("camerax-view-test_transform-test", ".jpg"); 426 tempFile.deleteOnExit(); 427 } catch (IOException e) { 428 showToast("Failed to create temp file. " + e); 429 return; 430 } 431 432 // Take a picture. 433 ImageCapture.OutputFileOptions outputFileOptions = 434 new ImageCapture.OutputFileOptions.Builder(tempFile).build(); 435 mCameraController.takePicture(outputFileOptions, mExecutorService, 436 new ImageCapture.OnImageSavedCallback() { 437 @Override 438 public void onImageSaved( 439 ImageCapture.@NonNull OutputFileResults outputFileResults) { 440 if (mImageProxyTransform == null || mBrightestTile == null) { 441 Logger.d(TAG, "ImageAnalysis result not ready."); 442 return; 443 } 444 try { 445 RectF tileInFile = getBrightestTileInFileWithoutExif( 446 tempFile, 447 mImageProxyTransform, 448 mBrightestTile); 449 // Load a mutable Bitmap. 450 BitmapFactory.Options options = new BitmapFactory.Options(); 451 options.inMutable = true; 452 Bitmap bitmap = BitmapFactory.decodeFile(tempFile.getAbsolutePath(), 453 options); 454 drawRectOnBitmap(bitmap, tileInFile); 455 saveBitmapToFilePreservingExif(tempFile, bitmap); 456 insertFileToMediaStore(tempFile); 457 showToast("Image saved."); 458 } catch (IOException e) { 459 showToast("Failed to draw on file. " + e); 460 } 461 } 462 463 @Override 464 public void onError(@NonNull ImageCaptureException exception) { 465 showToast("Failed to capture image. " + exception); 466 } 467 }); 468 } 469 updateMirrorState()470 private void updateMirrorState() { 471 if (mMirror.isChecked()) { 472 mPreviewView.setScaleX(-1); 473 } else { 474 mPreviewView.setScaleX(1); 475 } 476 } 477 updateCameraOrientation()478 private void updateCameraOrientation() { 479 if (mCameraToggle.isChecked()) { 480 mCameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA); 481 } else { 482 mCameraController.setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA); 483 } 484 } 485 486 /** 487 * Saves the Bitmap to the given File while preserving the File's exif orientation. 488 */ saveBitmapToFilePreservingExif(@onNull File originalFile, @NonNull Bitmap bitmap)489 void saveBitmapToFilePreservingExif(@NonNull File originalFile, @NonNull Bitmap bitmap) 490 throws IOException { 491 ExifInterface exifInterface = new ExifInterface(originalFile); 492 int orientation = exifInterface.getAttributeInt( 493 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); 494 try (OutputStream outputStream = new FileOutputStream(originalFile)) { 495 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); 496 } 497 exifInterface = new ExifInterface(originalFile); 498 exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); 499 exifInterface.saveAttributes(); 500 } 501 insertFileToMediaStore(@onNull File file)502 void insertFileToMediaStore(@NonNull File file) throws IOException { 503 ContentValues contentValues = new ContentValues(); 504 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); 505 ContentResolver contentResolver = requireContext().getContentResolver(); 506 Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 507 contentValues); 508 try (OutputStream outputStream = contentResolver.openOutputStream(uri)) { 509 try (InputStream inputStream = new FileInputStream(file)) { 510 byte[] buf = new byte[1024]; 511 int len; 512 while ((len = inputStream.read(buf)) > 0) { 513 outputStream.write(buf, 0, len); 514 } 515 } 516 } 517 } 518 519 // Synthetic access 520 @SuppressWarnings("WeakerAccess") drawRectOnBitmap(@onNull Bitmap bitmap, @NonNull RectF rectF)521 void drawRectOnBitmap(@NonNull Bitmap bitmap, @NonNull RectF rectF) { 522 Paint paint = new Paint(); 523 paint.setStyle(Paint.Style.STROKE); 524 525 Canvas canvas = new Canvas(bitmap); 526 527 // Draw a rect with black stroke and white glow so it's always visible regardless of 528 // background. 529 paint.setStrokeWidth(20); 530 paint.setColor(Color.WHITE); 531 canvas.drawRect(rectF, paint); 532 533 paint.setStrokeWidth(10); 534 paint.setColor(Color.BLACK); 535 canvas.drawRect(rectF, paint); 536 } 537 538 // Synthetic access 539 @SuppressWarnings("WeakerAccess") 540 @OptIn(markerClass = TransformExperimental.class) getBrightestTileInFileWithoutExif(@onNull File file, @NonNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)541 @NonNull RectF getBrightestTileInFileWithoutExif(@NonNull File file, 542 @NonNull OutputTransform imageProxyTransform, 543 @NonNull RectF imageProxyTile) throws IOException { 544 return getBrightestTile( 545 imageProxyTransform, 546 mFileTransformFactoryWithoutExif.getOutputTransform(file), 547 imageProxyTile); 548 } 549 550 // Synthetic access 551 @SuppressWarnings("WeakerAccess") 552 @OptIn(markerClass = TransformExperimental.class) getBrightestTileInUriWithExif(@onNull Uri uri, @NonNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)553 @NonNull RectF getBrightestTileInUriWithExif(@NonNull Uri uri, 554 @NonNull OutputTransform imageProxyTransform, 555 @NonNull RectF imageProxyTile) throws IOException { 556 OutputTransform uriTransform = mFileTransformFactoryWithExif.getOutputTransform( 557 requireContext().getContentResolver(), uri); 558 return getBrightestTile(imageProxyTransform, uriTransform, imageProxyTile); 559 } 560 561 // Synthetic access 562 @SuppressWarnings("WeakerAccess") 563 @OptIn(markerClass = TransformExperimental.class) 564 @MainThread getBrightestTileInPreviewView(@onNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)565 @Nullable RectF getBrightestTileInPreviewView(@NonNull OutputTransform imageProxyTransform, 566 @NonNull RectF imageProxyTile) { 567 OutputTransform previewViewTransform = mPreviewView.getOutputTransform(); 568 if (previewViewTransform == null) { 569 // PreviewView transform info is not ready. No-op. 570 return null; 571 } 572 return getBrightestTile(imageProxyTransform, previewViewTransform, imageProxyTile); 573 } 574 575 @OptIn(markerClass = TransformExperimental.class) getBrightestTile(@onNull OutputTransform source, @NonNull OutputTransform target, @NonNull RectF imageProxyTile)576 private @NonNull RectF getBrightestTile(@NonNull OutputTransform source, 577 @NonNull OutputTransform target, 578 @NonNull RectF imageProxyTile) { 579 CoordinateTransform transform = new CoordinateTransform(source, target); 580 Matrix matrix = new Matrix(); 581 transform.transform(matrix); 582 matrix.mapRect(imageProxyTile); 583 return imageProxyTile; 584 } 585 586 @Override onDestroyView()587 public void onDestroyView() { 588 super.onDestroyView(); 589 if (mExecutorService != null) { 590 mExecutorService.shutdown(); 591 } 592 } 593 } 594