Add startAutoFocus(Rect) (#724)
	
		
	
				
					
				
			* Create MeteringRegion(s), MeteringTransform, add startAutoFocus(RectF) API * Improve docs * Changelog * Testspull/725/head
							parent
							
								
									9b916f12fd
								
							
						
					
					
						commit
						df139994d7
					
				| @ -0,0 +1,137 @@ | ||||
| package com.otaliastudios.cameraview.metering; | ||||
| 
 | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.res.TypedArray; | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.RectF; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||
| import androidx.test.filters.SmallTest; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.BaseTest; | ||||
| import com.otaliastudios.cameraview.R; | ||||
| import com.otaliastudios.cameraview.markers.AutoFocusMarker; | ||||
| import com.otaliastudios.cameraview.markers.AutoFocusTrigger; | ||||
| import com.otaliastudios.cameraview.markers.MarkerParser; | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.mockito.invocation.InvocationOnMock; | ||||
| import org.mockito.stubbing.Answer; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import static junit.framework.TestCase.assertNotNull; | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNull; | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.Mockito.atLeastOnce; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.when; | ||||
| 
 | ||||
| 
 | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| @SmallTest | ||||
| public class MeteringRegionsTest extends BaseTest { | ||||
| 
 | ||||
|     private final Size bounds = new Size(1000, 1000); | ||||
| 
 | ||||
|     private void checkRegion(@NonNull MeteringRegion region, @NonNull PointF center, int weight) { | ||||
|         assertEquals(center.x, region.mRegion.centerX(), 0.01F); | ||||
|         assertEquals(center.y, region.mRegion.centerY(), 0.01F); | ||||
|         assertEquals(weight, region.mWeight); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testFromPoint() { | ||||
|         PointF center = new PointF(500, 500); | ||||
|         MeteringRegions regions = MeteringRegions.fromPoint(bounds, center); | ||||
|         assertEquals(2, regions.mRegions.size()); | ||||
|         MeteringRegion first = regions.mRegions.get(0); | ||||
|         MeteringRegion second = regions.mRegions.get(1); | ||||
|         checkRegion(first, center, MeteringRegion.MAX_WEIGHT); | ||||
|         checkRegion(second, center, | ||||
|                 Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testFromArea() { | ||||
|         RectF area = new RectF(400, 400, 600, 600); | ||||
|         MeteringRegions regions = MeteringRegions.fromArea(bounds, area); | ||||
|         assertEquals(1, regions.mRegions.size()); | ||||
|         MeteringRegion region = regions.mRegions.get(0); | ||||
|         checkRegion(region, new PointF(area.centerX(), area.centerY()), MeteringRegion.MAX_WEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testFromArea_withBlur() { | ||||
|         RectF area = new RectF(400, 400, 600, 600); | ||||
|         MeteringRegions regions = MeteringRegions.fromArea(bounds, area, | ||||
|                 MeteringRegion.MAX_WEIGHT, true); | ||||
|         assertEquals(2, regions.mRegions.size()); | ||||
|         MeteringRegion first = regions.mRegions.get(0); | ||||
|         MeteringRegion second = regions.mRegions.get(1); | ||||
|         PointF center = new PointF(area.centerX(), area.centerY()); | ||||
|         checkRegion(first, center, MeteringRegion.MAX_WEIGHT); | ||||
|         checkRegion(second, center, | ||||
|                 Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT)); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testTransform() { | ||||
|         MeteringTransform transform = mock(MeteringTransform.class); | ||||
|         when(transform.transformMeteringPoint(any(PointF.class))).then(new Answer<PointF>() { | ||||
|             @Override | ||||
|             public PointF answer(InvocationOnMock invocation) { | ||||
|                 PointF in = invocation.getArgument(0); | ||||
|                 // This will swap x and y coordinates
 | ||||
|                 //noinspection SuspiciousNameCombination
 | ||||
|                 return new PointF(in.y, in.x); | ||||
|             } | ||||
|         }); | ||||
|         RectF area = new RectF(0, 0, 100, 500); // tall area
 | ||||
|         RectF expected = new RectF(0, 0, 500, 100); // wide area
 | ||||
|         MeteringRegions regions = MeteringRegions.fromArea(bounds, area); | ||||
|         MeteringRegions transformed = regions.transform(transform); | ||||
|         verify(transform, times(4)).transformMeteringPoint(any(PointF.class)); | ||||
|         assertEquals(1, transformed.mRegions.size()); | ||||
|         assertEquals(expected, transformed.mRegions.get(0).mRegion); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Test | ||||
|     public void testGet() { | ||||
|         MeteringTransform<Integer> transform = new MeteringTransform<Integer>() { | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public PointF transformMeteringPoint(@NonNull PointF point) { | ||||
|                 return point; | ||||
|             } | ||||
| 
 | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public Integer transformMeteringRegion(@NonNull RectF region, int weight) { | ||||
|                 return weight; | ||||
|             } | ||||
|         }; | ||||
|         MeteringRegions regions = MeteringRegions.fromArea(bounds, | ||||
|                 new RectF(400, 400, 600, 600), | ||||
|                 900, | ||||
|                 true); | ||||
|         assertEquals(2, regions.mRegions.size()); | ||||
|         List<Integer> result = regions.get(1, transform); | ||||
|         assertEquals(1, result.size()); | ||||
|         assertEquals(900, (int) result.get(0)); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,55 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.RectF; | ||||
| import android.hardware.Camera; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.offset.Angles; | ||||
| import com.otaliastudios.cameraview.engine.offset.Axis; | ||||
| import com.otaliastudios.cameraview.engine.offset.Reference; | ||||
| import com.otaliastudios.cameraview.metering.MeteringTransform; | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| public class Camera1MeteringTransform implements MeteringTransform<Camera.Area> { | ||||
| 
 | ||||
|     protected static final String TAG = Camera1MeteringTransform.class.getSimpleName(); | ||||
|     protected static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private final int displayToSensor; | ||||
|     private final Size previewSize; | ||||
| 
 | ||||
|     public Camera1MeteringTransform(@NonNull Angles angles, @NonNull Size previewSize) { | ||||
|         this.displayToSensor = -angles.offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); | ||||
|         this.previewSize = previewSize; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public PointF transformMeteringPoint(@NonNull PointF point) { | ||||
|         // First, rescale to the -1000 ... 1000 range.
 | ||||
|         PointF scaled = new PointF(); | ||||
|         scaled.x = -1000F + (point.x / previewSize.getWidth()) * 2000F; | ||||
|         scaled.y = -1000F + (point.y / previewSize.getHeight()) * 2000F; | ||||
| 
 | ||||
|         // Apply rotation to this point.
 | ||||
|         // https://academo.org/demos/rotation-about-point/
 | ||||
|         PointF rotated = new PointF(); | ||||
|         double theta = ((double) displayToSensor) * Math.PI / 180; | ||||
|         rotated.x = (float) (scaled.x * Math.cos(theta) - scaled.y * Math.sin(theta)); | ||||
|         rotated.y = (float) (scaled.x * Math.sin(theta) + scaled.y * Math.cos(theta)); | ||||
|         LOG.i("scaled:", scaled, "rotated:", rotated); | ||||
|         return rotated; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Camera.Area transformMeteringRegion(@NonNull RectF region, int weight) { | ||||
|         Rect rect = new Rect(); | ||||
|         region.round(rect); | ||||
|         return new Camera.Area(rect, weight); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,195 @@ | ||||
| package com.otaliastudios.cameraview.engine.metering; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.RectF; | ||||
| import android.hardware.camera2.CameraCharacteristics; | ||||
| import android.hardware.camera2.CaptureRequest; | ||||
| import android.hardware.camera2.params.MeteringRectangle; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.RequiresApi; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.CameraLogger; | ||||
| import com.otaliastudios.cameraview.engine.offset.Angles; | ||||
| import com.otaliastudios.cameraview.engine.offset.Axis; | ||||
| import com.otaliastudios.cameraview.engine.offset.Reference; | ||||
| import com.otaliastudios.cameraview.metering.MeteringTransform; | ||||
| import com.otaliastudios.cameraview.size.AspectRatio; | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.LOLLIPOP) | ||||
| public class Camera2MeteringTransform implements MeteringTransform<MeteringRectangle> { | ||||
| 
 | ||||
|     protected static final String TAG = Camera2MeteringTransform.class.getSimpleName(); | ||||
|     protected static final CameraLogger LOG = CameraLogger.create(TAG); | ||||
| 
 | ||||
|     private final Angles angles; | ||||
|     private final Size previewSize; | ||||
|     private final Size previewStreamSize; | ||||
|     private final boolean previewIsCropping; | ||||
|     private final CameraCharacteristics characteristics; | ||||
|     private final CaptureRequest.Builder builder; | ||||
| 
 | ||||
|     public Camera2MeteringTransform(@NonNull Angles angles, | ||||
|                                     @NonNull Size previewSize, | ||||
|                                     @NonNull Size previewStreamSize, | ||||
|                                     boolean previewIsCropping, | ||||
|                                     @NonNull CameraCharacteristics characteristics, | ||||
|                                     @NonNull CaptureRequest.Builder builder) { | ||||
|         this.angles = angles; | ||||
|         this.previewSize = previewSize; | ||||
|         this.previewStreamSize = previewStreamSize; | ||||
|         this.previewIsCropping = previewIsCropping; | ||||
|         this.characteristics = characteristics; | ||||
|         this.builder = builder; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public MeteringRectangle transformMeteringRegion(@NonNull RectF region, int weight) { | ||||
|         Rect round = new Rect(); | ||||
|         region.round(round); | ||||
|         return new MeteringRectangle(round, weight); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public PointF transformMeteringPoint(@NonNull PointF point) { | ||||
|         // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782
 | ||||
|         // At first, the point is relative to the View system and does not account
 | ||||
|         // our own cropping. Will keep updating these two below.
 | ||||
|         final PointF referencePoint = new PointF(point.x, point.y); | ||||
|         Size referenceSize = previewSize; | ||||
| 
 | ||||
|         // 1. Account for cropping.
 | ||||
|         // This will enlarge the preview size so that aspect ratio matches.
 | ||||
|         referenceSize = applyPreviewCropping(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 2. Scale to the preview stream coordinates.
 | ||||
|         // This will move to the preview stream coordinates by scaling.
 | ||||
|         referenceSize = applyPreviewScale(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 3. Rotate to the stream coordinate system.
 | ||||
|         // This leaves us with sensor stream coordinates.
 | ||||
|         referenceSize = applyPreviewToSensorRotation(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 4. Move to the crop region coordinate system.
 | ||||
|         // The crop region is the union of all currently active streams.
 | ||||
|         referenceSize = applyCropRegionCoordinates(referenceSize, referencePoint); | ||||
| 
 | ||||
|         // 5. Move to the active array coordinate system.
 | ||||
|         referenceSize = applyActiveArrayCoordinates(referenceSize, referencePoint); | ||||
|         LOG.i("input:", point, "output (before clipping):", referencePoint); | ||||
| 
 | ||||
|         // 6. Probably not needed, but make sure we clip.
 | ||||
|         if (referencePoint.x < 0) referencePoint.x = 0; | ||||
|         if (referencePoint.y < 0) referencePoint.y = 0; | ||||
|         if (referencePoint.x > referenceSize.getWidth()) referencePoint.x = referenceSize.getWidth(); | ||||
|         if (referencePoint.y > referenceSize.getHeight()) referencePoint.y = referenceSize.getHeight(); | ||||
|         LOG.i("input:", point, "output (after clipping):", referencePoint); | ||||
|         return referencePoint; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @SuppressWarnings("UnnecessaryLocalVariable") | ||||
|     @NonNull | ||||
|     private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         Size previewStreamSize = this.previewStreamSize; | ||||
|         Size previewSurfaceSize = referenceSize; | ||||
|         int referenceWidth = previewSurfaceSize.getWidth(); | ||||
|         int referenceHeight = previewSurfaceSize.getHeight(); | ||||
|         AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); | ||||
|         AspectRatio previewSurfaceAspectRatio = AspectRatio.of(previewSurfaceSize); | ||||
|         if (previewIsCropping) { | ||||
|             if (previewStreamAspectRatio.toFloat() > previewSurfaceAspectRatio.toFloat()) { | ||||
|                 // Stream is larger. The x coordinate must be increased: a touch on the left side
 | ||||
|                 // of the surface is not on the left size of stream (it's more to the right).
 | ||||
|                 float scale = previewStreamAspectRatio.toFloat() | ||||
|                         / previewSurfaceAspectRatio.toFloat(); | ||||
|                 referencePoint.x += previewSurfaceSize.getWidth() * (scale - 1F) / 2F; | ||||
|                 referenceWidth = Math.round(previewSurfaceSize.getWidth() * scale); | ||||
|             } else { | ||||
|                 // Stream is taller. The y coordinate must be increased: a touch on the top side
 | ||||
|                 // of the surface is not on the top size of stream (it's a bit lower).
 | ||||
|                 float scale = previewSurfaceAspectRatio.toFloat() | ||||
|                         / previewStreamAspectRatio.toFloat(); | ||||
|                 referencePoint.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; | ||||
|                 referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale); | ||||
|             } | ||||
|         } | ||||
|         return new Size(referenceWidth, referenceHeight); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyPreviewScale(@NonNull Size referenceSize, @NonNull PointF referencePoint) { | ||||
|         // The referenceSize how has the same aspect ratio of the previewStreamSize, but they
 | ||||
|         // can still have different size (that is, a scale operation is needed).
 | ||||
|         Size previewStreamSize = this.previewStreamSize; | ||||
|         referencePoint.x *= (float) previewStreamSize.getWidth() / referenceSize.getWidth(); | ||||
|         referencePoint.y *= (float) previewStreamSize.getHeight() / referenceSize.getHeight(); | ||||
|         return previewStreamSize; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("SuspiciousNameCombination") | ||||
|     @NonNull | ||||
|     private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, | ||||
|                                               @NonNull PointF referencePoint) { | ||||
|         // Not elegant, but the sin/cos way was failing for some reason.
 | ||||
|         int angle = angles.offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); | ||||
|         boolean flip = angle % 180 != 0; | ||||
|         float tempX = referencePoint.x; | ||||
|         float tempY = referencePoint.y; | ||||
|         if (angle == 0) { | ||||
|             referencePoint.x = tempX; | ||||
|             referencePoint.y = tempY; | ||||
|         } else if (angle == 90) { | ||||
|             referencePoint.x = tempY; | ||||
|             referencePoint.y = referenceSize.getWidth() - tempX; | ||||
|         } else if (angle == 180) { | ||||
|             referencePoint.x = referenceSize.getWidth() - tempX; | ||||
|             referencePoint.y = referenceSize.getHeight() - tempY; | ||||
|         } else if (angle == 270) { | ||||
|             referencePoint.x = referenceSize.getHeight() - tempY; | ||||
|             referencePoint.y = tempX; | ||||
|         } else { | ||||
|             throw new IllegalStateException("Unexpected angle " + angle); | ||||
|         } | ||||
|         return flip ? referenceSize.flip() : referenceSize; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyCropRegionCoordinates(@NonNull Size referenceSize, | ||||
|                                             @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the stream rect.
 | ||||
|         // The stream rect is part of the 'crop region', as described below.
 | ||||
|         // https://source.android.com/devices/camera/camera3_crop_reprocess.html
 | ||||
|         Rect cropRect = builder.get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         // For now we don't care about x and y position. Rect should not be null, but let's be safe.
 | ||||
|         int cropRectWidth = cropRect == null ? referenceSize.getWidth() : cropRect.width(); | ||||
|         int cropRectHeight = cropRect == null ? referenceSize.getHeight() : cropRect.height(); | ||||
|         // The stream is always centered inside the crop region, and one of the dimensions
 | ||||
|         // should always match. We just increase the other one.
 | ||||
|         referencePoint.x += (cropRectWidth - referenceSize.getWidth()) / 2F; | ||||
|         referencePoint.y += (cropRectHeight - referenceSize.getHeight()) / 2F; | ||||
|         return new Size(cropRectWidth, cropRectHeight); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, | ||||
|                                              @NonNull PointF referencePoint) { | ||||
|         // The input point and size refer to the scaler crop region.
 | ||||
|         // We can query for the crop region position inside the active array, so this is easy.
 | ||||
|         Rect cropRect = builder.get(CaptureRequest.SCALER_CROP_REGION); | ||||
|         referencePoint.x += cropRect == null ? 0 : cropRect.left; | ||||
|         referencePoint.y += cropRect == null ? 0 : cropRect.top; | ||||
|         // Finally, get the active rect width and height from characteristics.
 | ||||
|         Rect activeRect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); | ||||
|         if (activeRect == null) { // Should never happen
 | ||||
|             activeRect = new Rect(0, 0, referenceSize.getWidth(), | ||||
|                     referenceSize.getHeight()); | ||||
|         } | ||||
|         return new Size(activeRect.width(), activeRect.height()); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,76 @@ | ||||
| package com.otaliastudios.cameraview.metering; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.RectF; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| class MeteringRegion implements Comparable<MeteringRegion> { | ||||
| 
 | ||||
|     final static int MAX_WEIGHT = 1000; | ||||
| 
 | ||||
|     final RectF mRegion; | ||||
|     final int mWeight; | ||||
| 
 | ||||
|     MeteringRegion(@NonNull RectF region, int weight) { | ||||
|         mRegion = region; | ||||
|         mWeight = weight; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     MeteringRegion transform(@NonNull MeteringTransform transform) { | ||||
|         RectF result = new RectF(Float.MAX_VALUE, Float.MAX_VALUE, | ||||
|                 -Float.MAX_VALUE, -Float.MAX_VALUE); | ||||
|         PointF point = new PointF(); | ||||
|         // top-left
 | ||||
|         point.set(mRegion.left, mRegion.top); | ||||
|         point = transform.transformMeteringPoint(point); | ||||
|         updateRect(result, point); | ||||
|         // top-right
 | ||||
|         point.set(mRegion.right, mRegion.top); | ||||
|         point = transform.transformMeteringPoint(point); | ||||
|         updateRect(result, point); | ||||
|         // bottom-right
 | ||||
|         point.set(mRegion.right, mRegion.bottom); | ||||
|         point = transform.transformMeteringPoint(point); | ||||
|         updateRect(result, point); | ||||
|         // bottom-left
 | ||||
|         point.set(mRegion.left, mRegion.bottom); | ||||
|         point = transform.transformMeteringPoint(point); | ||||
|         updateRect(result, point); | ||||
|         return new MeteringRegion(result, mWeight); | ||||
|     } | ||||
| 
 | ||||
|     private void updateRect(@NonNull RectF rect, @NonNull PointF point) { | ||||
|         rect.left = Math.min(rect.left, point.x); | ||||
|         rect.top = Math.min(rect.top, point.y); | ||||
|         rect.right = Math.max(rect.right, point.x); | ||||
|         rect.bottom = Math.max(rect.bottom, point.y); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     MeteringRegion clip(@NonNull Size bounds) { | ||||
|         return clip(new RectF(0, 0, bounds.getWidth(), bounds.getHeight())); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     @NonNull | ||||
|     MeteringRegion clip(@NonNull RectF bounds) { | ||||
|         RectF region = new RectF(); | ||||
|         region.set( | ||||
|                 Math.max(bounds.left, mRegion.left), | ||||
|                 Math.max(bounds.top, mRegion.top), | ||||
|                 Math.min(bounds.right, mRegion.right), | ||||
|                 Math.min(bounds.bottom, mRegion.bottom) | ||||
|         ); | ||||
|         return new MeteringRegion(region, mWeight); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int compareTo(@NonNull MeteringRegion o) { | ||||
|         //noinspection UseCompareMethod
 | ||||
|         return -Integer.valueOf(mWeight).compareTo(o.mWeight); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,112 @@ | ||||
| package com.otaliastudios.cameraview.metering; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.RectF; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.VisibleForTesting; | ||||
| 
 | ||||
| import com.otaliastudios.cameraview.size.Size; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class MeteringRegions { | ||||
|     private final static float POINT_AREA = 0.05F; | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     final static float BLUR_FACTOR_WEIGHT = 0.1F; | ||||
|     private final static float BLUR_FACTOR_SIZE = 1.5F; | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     final List<MeteringRegion> mRegions; | ||||
| 
 | ||||
|     private MeteringRegions(@NonNull List<MeteringRegion> regions) { | ||||
|         mRegions = regions; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public MeteringRegions transform(@NonNull MeteringTransform transform) { | ||||
|         List<MeteringRegion> regions = new ArrayList<>(); | ||||
|         for (MeteringRegion region : mRegions) { | ||||
|             regions.add(region.transform(transform)); | ||||
|         } | ||||
|         return new MeteringRegions(regions); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public <T> List<T> get(int atMost, @NonNull MeteringTransform<T> transform) { | ||||
|         List<T> result = new ArrayList<>(); | ||||
|         Collections.sort(mRegions); | ||||
|         for (MeteringRegion region : mRegions) { | ||||
|             result.add(transform.transformMeteringRegion(region.mRegion, region.mWeight)); | ||||
|         } | ||||
|         atMost = Math.min(atMost, result.size()); | ||||
|         return result.subList(0, atMost); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static MeteringRegions fromPoint(@NonNull Size bounds, | ||||
|                                             @NonNull PointF point) { | ||||
|         return fromPoint(bounds, point, MeteringRegion.MAX_WEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static MeteringRegions fromPoint(@NonNull Size bounds, | ||||
|                                             @NonNull PointF point, | ||||
|                                             int weight) { | ||||
|         float width = POINT_AREA * bounds.getWidth(); | ||||
|         float height = POINT_AREA * bounds.getHeight(); | ||||
|         RectF rectF = expand(point, width, height); | ||||
|         return fromArea(bounds, rectF, weight, true); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static MeteringRegions fromArea(@NonNull Size bounds, | ||||
|                                            @NonNull RectF area) { | ||||
|         return fromArea(bounds, area, MeteringRegion.MAX_WEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static MeteringRegions fromArea(@NonNull Size bounds, | ||||
|                                            @NonNull RectF area, | ||||
|                                            int weight) { | ||||
|         return fromArea(bounds, area, weight, false); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static MeteringRegions fromArea(@NonNull Size bounds, | ||||
|                                            @NonNull RectF area, | ||||
|                                            int weight, | ||||
|                                            boolean blur) { | ||||
|         List<MeteringRegion> regions = new ArrayList<>(); | ||||
|         final PointF center = new PointF(area.centerX(), area.centerY()); | ||||
|         final float width = area.width(); | ||||
|         final float height = area.height(); | ||||
|         regions.add(new MeteringRegion(area, weight)); | ||||
|         if (blur) { | ||||
|             RectF background = expand(center, | ||||
|                     BLUR_FACTOR_SIZE * width, | ||||
|                     BLUR_FACTOR_SIZE * height); | ||||
|             regions.add(new MeteringRegion(background, | ||||
|                     Math.round(BLUR_FACTOR_WEIGHT * weight))); | ||||
|         } | ||||
|         List<MeteringRegion> clipped = new ArrayList<>(); | ||||
|         for (MeteringRegion region : regions) { | ||||
|             clipped.add(region.clip(bounds)); | ||||
|         } | ||||
|         return new MeteringRegions(clipped); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private static RectF expand(@NonNull PointF center, float width, float height) { | ||||
|         return new RectF( | ||||
|                 center.x - width / 2F, | ||||
|                 center.y - height / 2F, | ||||
|                 center.x + width / 2F, | ||||
|                 center.y + height / 2F | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,14 @@ | ||||
| package com.otaliastudios.cameraview.metering; | ||||
| 
 | ||||
| import android.graphics.PointF; | ||||
| import android.graphics.RectF; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| public interface MeteringTransform<T> { | ||||
|     @NonNull | ||||
|     PointF transformMeteringPoint(@NonNull PointF point); | ||||
| 
 | ||||
|     @NonNull | ||||
|     T transformMeteringRegion(@NonNull RectF region, int weight); | ||||
| } | ||||
					Loading…
					
					
				
		Reference in new issue