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