| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ex.camera2.portability; |
| |
| import static android.hardware.camera2.CaptureRequest.*; |
| |
| import android.graphics.Matrix; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.hardware.camera2.CameraAccessException; |
| import android.hardware.camera2.CameraDevice; |
| import android.hardware.camera2.params.MeteringRectangle; |
| import android.location.Location; |
| import android.util.Range; |
| |
| import com.android.ex.camera2.portability.CameraCapabilities.FlashMode; |
| import com.android.ex.camera2.portability.CameraCapabilities.FocusMode; |
| import com.android.ex.camera2.portability.CameraCapabilities.SceneMode; |
| import com.android.ex.camera2.portability.CameraCapabilities.WhiteBalance; |
| import com.android.ex.camera2.portability.debug.Log; |
| import com.android.ex.camera2.utils.Camera2RequestSettingsSet; |
| |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * The subclass of {@link CameraSettings} for Android Camera 2 API. |
| */ |
| public class AndroidCamera2Settings extends CameraSettings { |
| private static final Log.Tag TAG = new Log.Tag("AndCam2Set"); |
| |
| private final Builder mTemplateSettings; |
| private final Camera2RequestSettingsSet mRequestSettings; |
| /** Sensor's active array bounds. */ |
| private final Rect mActiveArray; |
| /** Crop rectangle for digital zoom (measured WRT the active array). */ |
| private final Rect mCropRectangle; |
| /** Bounds of visible preview portion (measured WRT the active array). */ |
| private Rect mVisiblePreviewRectangle; |
| |
| /** |
| * Create a settings representation that answers queries of unspecified |
| * options in the same way as the provided template would. |
| * |
| * <p>The default settings provided by the given template are only ever used |
| * for reporting back to the client app (i.e. when it queries an option |
| * it didn't explicitly set first). {@link Camera2RequestSettingsSet}s |
| * generated by an instance of this class will have any settings not |
| * modified using one of that instance's mutators forced to default, so that |
| * their effective values when submitting a capture request will be those of |
| * the template that is provided to the camera framework at that time.</p> |
| * |
| * @param camera Device from which to draw default settings |
| * (non-{@code null}). |
| * @param template Specific template to use for the defaults. |
| * @param activeArray Boundary coordinates of the sensor's active array |
| * (non-{@code null}). |
| * @param preview Dimensions of preview streams. |
| * @param photo Dimensions of captured images. |
| * |
| * @throws IllegalArgumentException If {@code camera} or {@code activeArray} |
| * is {@code null}. |
| * @throws CameraAccessException Upon internal framework/driver failure. |
| */ |
| public AndroidCamera2Settings(CameraDevice camera, int template, Rect activeArray, |
| Size preview, Size photo) throws CameraAccessException { |
| if (camera == null) { |
| throw new NullPointerException("camera must not be null"); |
| } |
| if (activeArray == null) { |
| throw new NullPointerException("activeArray must not be null"); |
| } |
| |
| mTemplateSettings = camera.createCaptureRequest(template); |
| mRequestSettings = new Camera2RequestSettingsSet(); |
| mActiveArray = activeArray; |
| mCropRectangle = new Rect(0, 0, activeArray.width(), activeArray.height()); |
| |
| mSizesLocked = false; |
| |
| Range<Integer> previewFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE); |
| if (previewFpsRange != null) { |
| setPreviewFpsRange(previewFpsRange.getLower(), previewFpsRange.getUpper()); |
| } |
| setPreviewSize(preview); |
| // TODO: mCurrentPreviewFormat |
| setPhotoSize(photo); |
| mJpegCompressQuality = queryTemplateDefaultOrMakeOneUp(JPEG_QUALITY, (byte) 0); |
| // TODO: mCurrentPhotoFormat |
| // NB: We're assuming that templates won't be zoomed in by default. |
| mCurrentZoomRatio = CameraCapabilities.ZOOM_RATIO_UNZOOMED; |
| // TODO: mCurrentZoomIndex |
| mExposureCompensationIndex = |
| queryTemplateDefaultOrMakeOneUp(CONTROL_AE_EXPOSURE_COMPENSATION, 0); |
| |
| mCurrentFlashMode = flashModeFromRequest(); |
| Integer currentFocusMode = mTemplateSettings.get(CONTROL_AF_MODE); |
| if (currentFocusMode != null) { |
| mCurrentFocusMode = AndroidCamera2Capabilities.focusModeFromInt(currentFocusMode); |
| } |
| Integer currentSceneMode = mTemplateSettings.get(CONTROL_SCENE_MODE); |
| if (currentSceneMode != null) { |
| mCurrentSceneMode = AndroidCamera2Capabilities.sceneModeFromInt(currentSceneMode); |
| } |
| Integer whiteBalance = mTemplateSettings.get(CONTROL_AWB_MODE); |
| if (whiteBalance != null) { |
| mWhiteBalance = AndroidCamera2Capabilities.whiteBalanceFromInt(whiteBalance); |
| } |
| |
| mVideoStabilizationEnabled = queryTemplateDefaultOrMakeOneUp( |
| CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF) == |
| CONTROL_VIDEO_STABILIZATION_MODE_ON; |
| mAutoExposureLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AE_LOCK, false); |
| mAutoWhiteBalanceLocked = queryTemplateDefaultOrMakeOneUp(CONTROL_AWB_LOCK, false); |
| // TODO: mRecordingHintEnabled |
| // TODO: mGpsData |
| android.util.Size exifThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE); |
| if (exifThumbnailSize != null) { |
| mExifThumbnailSize = |
| new Size(exifThumbnailSize.getWidth(), exifThumbnailSize.getHeight()); |
| } |
| } |
| |
| public AndroidCamera2Settings(AndroidCamera2Settings other) { |
| super(other); |
| mTemplateSettings = other.mTemplateSettings; |
| mRequestSettings = new Camera2RequestSettingsSet(other.mRequestSettings); |
| mActiveArray = other.mActiveArray; |
| mCropRectangle = new Rect(other.mCropRectangle); |
| } |
| |
| @Override |
| public CameraSettings copy() { |
| return new AndroidCamera2Settings(this); |
| } |
| |
| private <T> T queryTemplateDefaultOrMakeOneUp(Key<T> key, T defaultDefault) { |
| T val = mTemplateSettings.get(key); |
| if (val != null) { |
| return val; |
| } else { |
| // Spoof the default so matchesTemplateDefault excludes this key from generated sets. |
| // This approach beats a simple sentinel because it provides basic boolean support. |
| mTemplateSettings.set(key, defaultDefault); |
| return defaultDefault; |
| } |
| } |
| |
| private FlashMode flashModeFromRequest() { |
| Integer autoExposure = mTemplateSettings.get(CONTROL_AE_MODE); |
| if (autoExposure != null) { |
| switch (autoExposure) { |
| case CONTROL_AE_MODE_ON: |
| return FlashMode.OFF; |
| case CONTROL_AE_MODE_ON_AUTO_FLASH: |
| return FlashMode.AUTO; |
| case CONTROL_AE_MODE_ON_ALWAYS_FLASH: { |
| if (mTemplateSettings.get(FLASH_MODE) == FLASH_MODE_TORCH) { |
| return FlashMode.TORCH; |
| } else { |
| return FlashMode.ON; |
| } |
| } |
| case CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE: |
| return FlashMode.RED_EYE; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public void setZoomRatio(float ratio) { |
| super.setZoomRatio(ratio); |
| |
| // Compute the crop rectangle to be passed to the framework |
| mCropRectangle.set(0, 0, |
| toIntConstrained( |
| mActiveArray.width() / mCurrentZoomRatio, 0, mActiveArray.width()), |
| toIntConstrained( |
| mActiveArray.height() / mCurrentZoomRatio, 0, mActiveArray.height())); |
| mCropRectangle.offsetTo((mActiveArray.width() - mCropRectangle.width()) / 2, |
| (mActiveArray.height() - mCropRectangle.height()) / 2); |
| |
| // Compute the effective crop rectangle to be used for computing focus/metering coordinates |
| mVisiblePreviewRectangle = |
| effectiveCropRectFromRequested(mCropRectangle, mCurrentPreviewSize); |
| } |
| |
| private boolean matchesTemplateDefault(Key<?> setting) { |
| if (setting == CONTROL_AE_REGIONS) { |
| return mMeteringAreas.size() == 0; |
| } else if (setting == CONTROL_AF_REGIONS) { |
| return mFocusAreas.size() == 0; |
| } else if (setting == CONTROL_AE_TARGET_FPS_RANGE) { |
| Range<Integer> defaultFpsRange = mTemplateSettings.get(CONTROL_AE_TARGET_FPS_RANGE); |
| return (mPreviewFpsRangeMin == 0 && mPreviewFpsRangeMax == 0) || |
| (defaultFpsRange != null && mPreviewFpsRangeMin == defaultFpsRange.getLower() && |
| mPreviewFpsRangeMax == defaultFpsRange.getUpper()); |
| } else if (setting == JPEG_QUALITY) { |
| return Objects.equals(mJpegCompressQuality, |
| mTemplateSettings.get(JPEG_QUALITY)); |
| } else if (setting == CONTROL_AE_EXPOSURE_COMPENSATION) { |
| return Objects.equals(mExposureCompensationIndex, |
| mTemplateSettings.get(CONTROL_AE_EXPOSURE_COMPENSATION)); |
| } else if (setting == CONTROL_VIDEO_STABILIZATION_MODE) { |
| Integer videoStabilization = mTemplateSettings.get(CONTROL_VIDEO_STABILIZATION_MODE); |
| return (videoStabilization != null && |
| (mVideoStabilizationEnabled && videoStabilization == |
| CONTROL_VIDEO_STABILIZATION_MODE_ON) || |
| (!mVideoStabilizationEnabled && videoStabilization == |
| CONTROL_VIDEO_STABILIZATION_MODE_OFF)); |
| } else if (setting == CONTROL_AE_LOCK) { |
| return Objects.equals(mAutoExposureLocked, mTemplateSettings.get(CONTROL_AE_LOCK)); |
| } else if (setting == CONTROL_AWB_LOCK) { |
| return Objects.equals(mAutoWhiteBalanceLocked, mTemplateSettings.get(CONTROL_AWB_LOCK)); |
| } else if (setting == JPEG_THUMBNAIL_SIZE) { |
| if (mExifThumbnailSize == null) { |
| // It doesn't matter if this is true or false since setting this |
| // to null in the request settings will use the default anyway. |
| return false; |
| } |
| android.util.Size defaultThumbnailSize = mTemplateSettings.get(JPEG_THUMBNAIL_SIZE); |
| return (mExifThumbnailSize.width() == 0 && mExifThumbnailSize.height() == 0) || |
| (defaultThumbnailSize != null && |
| mExifThumbnailSize.width() == defaultThumbnailSize.getWidth() && |
| mExifThumbnailSize.height() == defaultThumbnailSize.getHeight()); |
| } |
| Log.w(TAG, "Settings implementation checked default of unhandled option key"); |
| // Since this class isn't equipped to handle it, claim it matches the default to prevent |
| // updateRequestSettingOrForceToDefault from going with the user-provided preference |
| return true; |
| } |
| |
| private <T> void updateRequestSettingOrForceToDefault(Key<T> setting, T possibleChoice) { |
| mRequestSettings.set(setting, matchesTemplateDefault(setting) ? null : possibleChoice); |
| } |
| |
| public Camera2RequestSettingsSet getRequestSettings() { |
| updateRequestSettingOrForceToDefault(CONTROL_AE_REGIONS, |
| legacyAreasToMeteringRectangles(mMeteringAreas)); |
| updateRequestSettingOrForceToDefault(CONTROL_AF_REGIONS, |
| legacyAreasToMeteringRectangles(mFocusAreas)); |
| updateRequestSettingOrForceToDefault(CONTROL_AE_TARGET_FPS_RANGE, |
| new Range(mPreviewFpsRangeMin, mPreviewFpsRangeMax)); |
| // TODO: mCurrentPreviewFormat |
| updateRequestSettingOrForceToDefault(JPEG_QUALITY, mJpegCompressQuality); |
| // TODO: mCurrentPhotoFormat |
| mRequestSettings.set(SCALER_CROP_REGION, mCropRectangle); |
| // TODO: mCurrentZoomIndex |
| updateRequestSettingOrForceToDefault(CONTROL_AE_EXPOSURE_COMPENSATION, |
| mExposureCompensationIndex); |
| updateRequestFlashMode(); |
| updateRequestFocusMode(); |
| updateRequestSceneMode(); |
| updateRequestWhiteBalance(); |
| updateRequestSettingOrForceToDefault(CONTROL_VIDEO_STABILIZATION_MODE, |
| mVideoStabilizationEnabled ? |
| CONTROL_VIDEO_STABILIZATION_MODE_ON : CONTROL_VIDEO_STABILIZATION_MODE_OFF); |
| // OIS shouldn't be on if software video stabilization is. |
| mRequestSettings.set(LENS_OPTICAL_STABILIZATION_MODE, |
| mVideoStabilizationEnabled ? LENS_OPTICAL_STABILIZATION_MODE_OFF : |
| null); |
| updateRequestSettingOrForceToDefault(CONTROL_AE_LOCK, mAutoExposureLocked); |
| updateRequestSettingOrForceToDefault(CONTROL_AWB_LOCK, mAutoWhiteBalanceLocked); |
| // TODO: mRecordingHintEnabled |
| updateRequestGpsData(); |
| if (mExifThumbnailSize != null) { |
| updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, |
| new android.util.Size( |
| mExifThumbnailSize.width(), mExifThumbnailSize.height())); |
| } else { |
| updateRequestSettingOrForceToDefault(JPEG_THUMBNAIL_SIZE, null); |
| } |
| |
| return mRequestSettings; |
| } |
| |
| private MeteringRectangle[] legacyAreasToMeteringRectangles( |
| List<android.hardware.Camera.Area> reference) { |
| MeteringRectangle[] transformed = null; |
| if (reference.size() > 0) { |
| transformed = new MeteringRectangle[reference.size()]; |
| for (int index = 0; index < reference.size(); ++index) { |
| android.hardware.Camera.Area source = reference.get(index); |
| Rect rectangle = source.rect; |
| |
| // Old API coordinates were [-1000,1000]; new ones are [0,ACTIVE_ARRAY_SIZE). |
| // We're also going from preview image--relative to sensor active array--relative. |
| double oldLeft = (rectangle.left + 1000) / 2000.0; |
| double oldTop = (rectangle.top + 1000) / 2000.0; |
| double oldRight = (rectangle.right + 1000) / 2000.0; |
| double oldBottom = (rectangle.bottom + 1000) / 2000.0; |
| int left = mCropRectangle.left + toIntConstrained( |
| mCropRectangle.width() * oldLeft, 0, mCropRectangle.width() - 1); |
| int top = mCropRectangle.top + toIntConstrained( |
| mCropRectangle.height() * oldTop, 0, mCropRectangle.height() - 1); |
| int right = mCropRectangle.left + toIntConstrained( |
| mCropRectangle.width() * oldRight, 0, mCropRectangle.width() - 1); |
| int bottom = mCropRectangle.top + toIntConstrained( |
| mCropRectangle.height() * oldBottom, 0, mCropRectangle.height() - 1); |
| transformed[index] = new MeteringRectangle(left, top, right - left, bottom - top, |
| source.weight); |
| } |
| } |
| return transformed; |
| } |
| |
| private int toIntConstrained(double original, int min, int max) { |
| original = Math.max(original, min); |
| original = Math.min(original, max); |
| return (int) original; |
| } |
| |
| private void updateRequestFlashMode() { |
| Integer aeMode = null; |
| Integer flashMode = null; |
| if (mCurrentFlashMode != null) { |
| switch (mCurrentFlashMode) { |
| case AUTO: { |
| aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH; |
| break; |
| } |
| case OFF: { |
| aeMode = CONTROL_AE_MODE_ON; |
| flashMode = FLASH_MODE_OFF; |
| break; |
| } |
| case ON: { |
| aeMode = CONTROL_AE_MODE_ON_ALWAYS_FLASH; |
| flashMode = FLASH_MODE_SINGLE; |
| break; |
| } |
| case TORCH: { |
| flashMode = FLASH_MODE_TORCH; |
| break; |
| } |
| case RED_EYE: { |
| aeMode = CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE; |
| break; |
| } |
| default: { |
| Log.w(TAG, "Unable to convert to API 2 flash mode: " + mCurrentFlashMode); |
| break; |
| } |
| } |
| } |
| mRequestSettings.set(CONTROL_AE_MODE, aeMode); |
| mRequestSettings.set(FLASH_MODE, flashMode); |
| } |
| |
| private void updateRequestFocusMode() { |
| Integer mode = null; |
| if (mCurrentFocusMode != null) { |
| switch (mCurrentFocusMode) { |
| case AUTO: { |
| mode = CONTROL_AF_MODE_AUTO; |
| break; |
| } |
| case CONTINUOUS_PICTURE: { |
| mode = CONTROL_AF_MODE_CONTINUOUS_PICTURE; |
| break; |
| } |
| case CONTINUOUS_VIDEO: { |
| mode = CONTROL_AF_MODE_CONTINUOUS_VIDEO; |
| break; |
| } |
| case EXTENDED_DOF: { |
| mode = CONTROL_AF_MODE_EDOF; |
| break; |
| } |
| case FIXED: { |
| mode = CONTROL_AF_MODE_OFF; |
| break; |
| } |
| // TODO: We cannot support INFINITY |
| case MACRO: { |
| mode = CONTROL_AF_MODE_MACRO; |
| break; |
| } |
| default: { |
| Log.w(TAG, "Unable to convert to API 2 focus mode: " + mCurrentFocusMode); |
| break; |
| } |
| } |
| } |
| mRequestSettings.set(CONTROL_AF_MODE, mode); |
| } |
| |
| private void updateRequestSceneMode() { |
| Integer mode = null; |
| if (mCurrentSceneMode != null) { |
| switch (mCurrentSceneMode) { |
| case AUTO: { |
| mode = CONTROL_SCENE_MODE_DISABLED; |
| break; |
| } |
| case ACTION: { |
| mode = CONTROL_SCENE_MODE_ACTION; |
| break; |
| } |
| case BARCODE: { |
| mode = CONTROL_SCENE_MODE_BARCODE; |
| break; |
| } |
| case BEACH: { |
| mode = CONTROL_SCENE_MODE_BEACH; |
| break; |
| } |
| case CANDLELIGHT: { |
| mode = CONTROL_SCENE_MODE_CANDLELIGHT; |
| break; |
| } |
| case FIREWORKS: { |
| mode = CONTROL_SCENE_MODE_FIREWORKS; |
| break; |
| } |
| case HDR: { |
| mode = CONTROL_SCENE_MODE_HDR; |
| break; |
| } |
| case LANDSCAPE: { |
| mode = CONTROL_SCENE_MODE_LANDSCAPE; |
| break; |
| } |
| case NIGHT: { |
| mode = CONTROL_SCENE_MODE_NIGHT; |
| break; |
| } |
| // TODO: We cannot support NIGHT_PORTRAIT |
| case PARTY: { |
| mode = CONTROL_SCENE_MODE_PARTY; |
| break; |
| } |
| case PORTRAIT: { |
| mode = CONTROL_SCENE_MODE_PORTRAIT; |
| break; |
| } |
| case SNOW: { |
| mode = CONTROL_SCENE_MODE_SNOW; |
| break; |
| } |
| case SPORTS: { |
| mode = CONTROL_SCENE_MODE_SPORTS; |
| break; |
| } |
| case STEADYPHOTO: { |
| mode = CONTROL_SCENE_MODE_STEADYPHOTO; |
| break; |
| } |
| case SUNSET: { |
| mode = CONTROL_SCENE_MODE_SUNSET; |
| break; |
| } |
| case THEATRE: { |
| mode = CONTROL_SCENE_MODE_THEATRE; |
| break; |
| } |
| default: { |
| Log.w(TAG, "Unable to convert to API 2 scene mode: " + mCurrentSceneMode); |
| break; |
| } |
| } |
| } |
| mRequestSettings.set(CONTROL_SCENE_MODE, mode); |
| } |
| |
| private void updateRequestWhiteBalance() { |
| Integer mode = null; |
| if (mWhiteBalance != null) { |
| switch (mWhiteBalance) { |
| case AUTO: { |
| mode = CONTROL_AWB_MODE_AUTO; |
| break; |
| } |
| case CLOUDY_DAYLIGHT: { |
| mode = CONTROL_AWB_MODE_CLOUDY_DAYLIGHT; |
| break; |
| } |
| case DAYLIGHT: { |
| mode = CONTROL_AWB_MODE_DAYLIGHT; |
| break; |
| } |
| case FLUORESCENT: { |
| mode = CONTROL_AWB_MODE_FLUORESCENT; |
| break; |
| } |
| case INCANDESCENT: { |
| mode = CONTROL_AWB_MODE_INCANDESCENT; |
| break; |
| } |
| case SHADE: { |
| mode = CONTROL_AWB_MODE_SHADE; |
| break; |
| } |
| case TWILIGHT: { |
| mode = CONTROL_AWB_MODE_TWILIGHT; |
| break; |
| } |
| case WARM_FLUORESCENT: { |
| mode = CONTROL_AWB_MODE_WARM_FLUORESCENT; |
| break; |
| } |
| default: { |
| Log.w(TAG, "Unable to convert to API 2 white balance: " + mWhiteBalance); |
| break; |
| } |
| } |
| } |
| mRequestSettings.set(CONTROL_AWB_MODE, mode); |
| } |
| |
| private void updateRequestGpsData() { |
| if (mGpsData == null || mGpsData.processingMethod == null) { |
| // It's a hack since we always use GPS time stamp but does |
| // not use other fields sometimes. Setting processing |
| // method to null means the other fields should not be used. |
| mRequestSettings.set(JPEG_GPS_LOCATION, null); |
| } else { |
| Location location = new Location(mGpsData.processingMethod); |
| location.setTime(mGpsData.timeStamp); |
| location.setAltitude(mGpsData.altitude); |
| location.setLatitude(mGpsData.latitude); |
| location.setLongitude(mGpsData.longitude); |
| mRequestSettings.set(JPEG_GPS_LOCATION, location); |
| } |
| } |
| |
| /** |
| * Calculate the effective crop rectangle for this preview viewport; |
| * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions |
| * without skewing. |
| * |
| * <p>Assumes the zoom level of the provided desired crop rectangle.</p> |
| * |
| * @param requestedCrop Desired crop rectangle, in active array space. |
| * @param previewSize Size of the preview buffer render target, in pixels (not in sensor space). |
| * @return A rectangle that serves as the preview stream's effective crop region (unzoomed), in |
| * sensor space. |
| * |
| * @throws NullPointerException |
| * If any of the args were {@code null}. |
| */ |
| private static Rect effectiveCropRectFromRequested(Rect requestedCrop, Size previewSize) { |
| float aspectRatioArray = requestedCrop.width() * 1.0f / requestedCrop.height(); |
| float aspectRatioPreview = previewSize.width() * 1.0f / previewSize.height(); |
| |
| float cropHeight, cropWidth; |
| if (aspectRatioPreview < aspectRatioArray) { |
| // The new width must be smaller than the height, so scale the width by AR |
| cropHeight = requestedCrop.height(); |
| cropWidth = cropHeight * aspectRatioPreview; |
| } else { |
| // The new height must be smaller (or equal) than the width, so scale the height by AR |
| cropWidth = requestedCrop.width(); |
| cropHeight = cropWidth / aspectRatioPreview; |
| } |
| |
| Matrix translateMatrix = new Matrix(); |
| RectF cropRect = new RectF(/*left*/0, /*top*/0, cropWidth, cropHeight); |
| |
| // Now center the crop rectangle so its center is in the center of the active array |
| translateMatrix.setTranslate(requestedCrop.exactCenterX(), requestedCrop.exactCenterY()); |
| translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY()); |
| |
| translateMatrix.mapRect(/*inout*/cropRect); |
| |
| // Round the rect corners towards the nearest integer values |
| Rect result = new Rect(); |
| cropRect.roundOut(result); |
| return result; |
| } |
| } |