Skip to content

Commit a80f403

Browse files
Vetle444Vetle Finstad
andauthored
Improve iOS barcode close focus (#894)
Co-authored-by: Vetle Finstad <finstad@Vetles-MacBook-Pro-2.local>
1 parent 74d6da0 commit a80f403

10 files changed

Lines changed: 230 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## [60.2.12]
2+
- [Camera][iOS] Added ultra-wide capable back camera selection and zoom mapping for barcode scanning and image capture on supported devices.
3+
14
## [60.2.11]
25
- [CollectionView][Android] Fixed crash when deferred item decoration updates ran after the RecyclerView was detached during list reloads.
36

src/library/DIPS.Mobile.UI/API/Camera/BarcodeScanning/iOS/BarcodeScanner.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,8 @@ public override void ConfigureSession()
8383
SetRecommendedZoomFactor();
8484
}
8585

86-
//Choosing build in wide angle camera, same as the sample app from Apple: AVCamBarCode: https://developer.apple.com/documentation/avfoundation/capture_setup/avcambarcode_detecting_barcodes_and_faces
87-
public override AVCaptureDevice? SelectCaptureDevice() =>
88-
AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInWideAngleCamera,
89-
AVMediaTypes.Video, AVCaptureDevicePosition.Back);
86+
// Prefer virtual back cameras so iOS starts at normal 1x wide framing but can switch to macro-capable lenses when close.
87+
public override AVCaptureDevice? SelectCaptureDevice() => SelectPreferredBackCaptureDevice();
9088

9189
//Taken from: https://developer.apple.com/wwdc21/10047?time=117
9290
//and sample code from Apple: https://developer.apple.com/documentation/avfoundation/capture_setup/avcambarcode_detecting_barcodes_and_faces
@@ -125,7 +123,7 @@ of minimumCodeSize (mm) fill the previewFillPercentage.
125123

126124
if (m_cameraPreview is { CameraZoomView: not null })
127125
{
128-
m_cameraPreview.CameraZoomView.SetZoomRatio((float)captureDevice.VideoZoomFactor);
126+
m_cameraPreview.CameraZoomView.SetZoomRatio(ToDisplayZoomRatio((float)captureDevice.VideoZoomFactor));
129127
}
130128
}
131129

src/library/DIPS.Mobile.UI/API/Camera/ImageCapturing/Views/CameraZoom/CameraZoomView.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ public CameraZoomView(float minRatio, float maxRatio, Action<float> onChangedZoo
2020
InputTransparent = true;
2121
CascadeInputTransparent = false;
2222

23-
// We do not support ultra-wide cameras yet, so we can safely assume that the minimum ratio is 1
24-
minRatio = 1;
25-
2623
VerticalOptions = LayoutOptions.End;
2724

2825
Margin = new Thickness(0, 0, 0, Sizes.GetSize(SizeName.content_margin_large));

src/library/DIPS.Mobile.UI/API/Camera/ImageCapturing/Views/CameraZoom/ZoomButtons.cs

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ public ZoomButtons(float minRatio, float maxRatio, Action<float> onChangedZoomRa
1818
{
1919
m_onChangedZoomRatio = onChangedZoomRatio;
2020
m_onPanned = onPanned;
21-
22-
// We do not support ultra-wide cameras yet, so we can safely assume that the minimum ratio is 1
23-
minRatio = 1;
2421

2522
var panGestureRecognizer = new PanGestureRecognizer();
2623
panGestureRecognizer.PanUpdated += OnPanned;
@@ -33,37 +30,51 @@ public ZoomButtons(float minRatio, float maxRatio, Action<float> onChangedZoomRa
3330
Padding = 2;
3431

3532
UI.Effects.Layout.Layout.SetCornerRadius(this, Sizes.GetSize(SizeName.size_5));
36-
37-
var firstButton = CreateCameraZoomButton(minRatio, true);
38-
var secondButton = CreateCameraZoomButton(minRatio + 1);
39-
var thirdButton = CreateCameraZoomButton(MathF.Min(secondButton.DefaultZoomRatio + 1, maxRatio));
4033

41-
// If we have not yet hit the max ratio, lets add another button
42-
ZoomButton? fourthButton = null;
43-
if (Math.Abs(thirdButton.DefaultZoomRatio - maxRatio) > 0.01f)
34+
m_zoomButtons = CreateDefaultZoomRatios(minRatio, maxRatio)
35+
.Select((zoomRatio, index) => CreateCameraZoomButton(zoomRatio, index == 0))
36+
.ToList();
37+
38+
m_currentlyActiveButton = m_zoomButtons.First();
39+
40+
foreach (var zoomButton in m_zoomButtons)
4441
{
45-
fourthButton = CreateCameraZoomButton(MathF.Min(secondButton.DefaultZoomRatio + 2, maxRatio));
42+
Add(zoomButton);
4643
}
47-
48-
m_currentlyActiveButton = firstButton.IsDefaultActive ? firstButton : secondButton;
49-
50-
Add(firstButton);
51-
Add(secondButton);
52-
Add(thirdButton);
53-
54-
if(fourthButton is not null)
55-
Add(fourthButton);
5644

5745
this.SizeChanged += OnSizeChanged;
5846

59-
m_zoomButtons = [firstButton, secondButton, thirdButton];
60-
61-
if(fourthButton is not null)
62-
m_zoomButtons.Add(fourthButton);
63-
6447
DUI.OrientationChanged += OrientationChanged;
6548
}
6649

50+
private static List<float> CreateDefaultZoomRatios(float minRatio, float maxRatio)
51+
{
52+
var zoomRatios = new List<float>();
53+
54+
AddZoomRatio(minRatio);
55+
AddZoomRatio(minRatio < 1 ? 1 : minRatio + 1);
56+
AddZoomRatio(MathF.Min(zoomRatios.Last() + 1, maxRatio));
57+
58+
var fourthZoomRatio = maxRatio;
59+
if (Math.Abs(zoomRatios.Last() - fourthZoomRatio) > 0.01f)
60+
{
61+
AddZoomRatio(fourthZoomRatio);
62+
}
63+
64+
return zoomRatios;
65+
66+
void AddZoomRatio(float zoomRatio)
67+
{
68+
zoomRatio = Math.Clamp(zoomRatio, minRatio, maxRatio);
69+
if (zoomRatios.Any(existingZoomRatio => Math.Abs(existingZoomRatio - zoomRatio) <= 0.01f))
70+
{
71+
return;
72+
}
73+
74+
zoomRatios.Add(zoomRatio);
75+
}
76+
}
77+
6778
private void OrientationChanged(OrientationDegree orientationDegree)
6879
{
6980
foreach (var child in Children)
@@ -170,7 +181,7 @@ public ZoomButton(float defaultZoomRatio, Action<(float, ZoomButton)> onTapped,
170181

171182
m_label = new Label
172183
{
173-
Text = IsDefaultActive ? $"{defaultZoomRatio} ×" : $"{defaultZoomRatio}",
184+
Text = IsDefaultActive ? $"{FormatZoomRatio(defaultZoomRatio)} ×" : FormatZoomRatio(defaultZoomRatio),
174185
TextColor = IsDefaultActive ? Colors.Gold : Colors.White,
175186
VerticalTextAlignment = TextAlignment.Center,
176187
HorizontalTextAlignment = TextAlignment.Center,
@@ -227,9 +238,11 @@ public void SetCustomZoomRatio(float zoomRatio)
227238

228239
public void ResetToDefault()
229240
{
230-
m_label.Text = $"{DefaultZoomRatio}";
241+
m_label.Text = FormatZoomRatio(DefaultZoomRatio);
231242
m_label.TextColor = Colors.White;
232243
}
244+
245+
private static string FormatZoomRatio(float zoomRatio) => zoomRatio.ToString("0.#");
233246

234247
public float DefaultZoomRatio { get; }
235248
public bool IsDefaultActive { get; }

src/library/DIPS.Mobile.UI/API/Camera/ImageCapturing/Views/CameraZoom/ZoomSlider.cs

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ internal class ZoomSlider : Grid
1616
private Border m_zoomRatioLevelBorder;
1717
private Label m_zoomRatioLevelLabel;
1818

19+
private readonly float m_minRatio;
1920
private readonly float m_maxRatio;
2021

2122
private readonly Action<float> m_onChangedZoomRatio;
2223
private readonly Action<PanUpdatedEventArgs> m_onPanned;
2324

2425
private double m_startingX;
26+
private float m_startingZoomRatio;
2527
private double m_minimumZoomRatiosLayoutTranslationX;
2628
private double m_maxZoomRatiosLayoutTranslationX;
2729
private double m_zoomRatioLevel;
30+
private float? m_pendingZoomRatio;
31+
private bool m_hasMeasuredZoomRatiosLayout;
2832

2933
private int m_currentSnappedZoomRatio;
3034

@@ -34,6 +38,7 @@ public ZoomSlider(float minRatio, float maxRatio, Action<float> onChangedZoomRat
3438
{
3539
InputTransparent = true;
3640
CascadeInputTransparent = false;
41+
m_minRatio = minRatio;
3742
m_maxRatio = maxRatio;
3843
m_onChangedZoomRatio = onChangedZoomRatio;
3944
m_onPanned = onPanned;
@@ -159,12 +164,23 @@ private static List<float> GenerateZoomRatios(float minRatio, float maxRatio)
159164

160165
private void ZoomRatiosLayoutOnSizeChanged(object? sender, EventArgs e)
161166
{
162-
if(m_zoomRatiosLayout.TranslationX != 0)
167+
if (m_zoomRatiosLayout.Width <= 0)
163168
return;
164-
165-
m_zoomRatiosLayout.TranslationX += m_zoomRatiosLayout.Width / 2;
166-
m_maxZoomRatiosLayoutTranslationX = m_zoomRatiosLayout.TranslationX;
169+
170+
if (m_hasMeasuredZoomRatiosLayout && m_pendingZoomRatio is null)
171+
return;
172+
173+
m_maxZoomRatiosLayoutTranslationX = m_zoomRatiosLayout.Width / 2;
167174
m_minimumZoomRatiosLayoutTranslationX = m_maxZoomRatiosLayoutTranslationX - m_zoomRatiosLayout.Width;
175+
m_hasMeasuredZoomRatiosLayout = true;
176+
177+
if (m_pendingZoomRatio is { } pendingZoomRatio)
178+
{
179+
SetZoomRatio(pendingZoomRatio);
180+
return;
181+
}
182+
183+
m_zoomRatiosLayout.TranslationX = m_maxZoomRatiosLayoutTranslationX;
168184
}
169185

170186
private double CalculateZoomRatio(double desiredTranslationX)
@@ -173,48 +189,52 @@ private double CalculateZoomRatio(double desiredTranslationX)
173189
currentTranslation = m_zoomRatiosLayout.Width - currentTranslation;
174190
var translationPercentage = (currentTranslation / m_zoomRatiosLayout.Width);
175191

176-
var zoomRatioIndex = ((m_maxRatio - 1) * 10) * translationPercentage;
192+
var zoomRatioIndex = ((m_maxRatio - m_minRatio) * 10) * translationPercentage;
177193

178-
return zoomRatioIndex / 10 + 1;
194+
return zoomRatioIndex / 10 + m_minRatio;
179195
}
180196

181197
private double CalculateTranslationXFromZoomRatio(float zoomRatio)
182198
{
183-
return m_maxZoomRatiosLayoutTranslationX - (m_zoomRatiosLayout.Width * ((zoomRatio - 1) / (m_maxRatio - 1)));
199+
if (Math.Abs(m_maxRatio - m_minRatio) < 0.01f)
200+
{
201+
return m_maxZoomRatiosLayoutTranslationX;
202+
}
203+
204+
return m_maxZoomRatiosLayoutTranslationX - (m_zoomRatiosLayout.Width * ((zoomRatio - m_minRatio) / (m_maxRatio - m_minRatio)));
184205
}
185206

186207
private void PanGestureRecognizerOnPanUpdated(PanUpdatedEventArgs e)
187208
{
188209
m_onPanned.Invoke(e);
189210
}
211+
212+
private float ClampZoomRatio(float zoomRatio) => Math.Clamp(zoomRatio, m_minRatio, m_maxRatio);
190213

191214
public void TranslateZoomSlider(PanUpdatedEventArgs e)
192215
{
193216
switch (e.StatusType)
194217
{
195218
case GestureStatus.Started:
196219
m_startingX = e.TotalX;
220+
m_startingZoomRatio = ClampZoomRatio((float)m_zoomRatioLevel);
197221

198222
m_cancellationTokenSource.Cancel();
199223
m_cancellationTokenSource = new CancellationTokenSource();
200224
break;
201225
case GestureStatus.Running:
202226
{
203-
var desiredTranslationX = m_zoomRatiosLayout.TranslationX + e.TotalX - m_startingX;
204-
205-
var actualZoomRatio = CalculateZoomRatio(m_zoomRatiosLayout.TranslationX);
206-
207-
// Vibrate when near integers
208-
var roundedZoomRatio = (int)MathF.Round((float)actualZoomRatio);
209-
if (Math.Abs(roundedZoomRatio - actualZoomRatio) < 0.025f)
227+
if (m_minRatio < 1)
210228
{
211-
if (roundedZoomRatio != m_currentSnappedZoomRatio)
212-
{
213-
VibrationService.SelectionChanged();
214-
}
215-
m_currentSnappedZoomRatio = roundedZoomRatio;
229+
var dragRange = Math.Max(Width, 1);
230+
var zoomRatioDelta = (float)((m_startingX - e.TotalX) / dragRange * (m_maxRatio - m_minRatio));
231+
var desiredZoomRatio = ClampZoomRatio(m_startingZoomRatio + zoomRatioDelta);
232+
m_zoomRatiosLayout.TranslationX = CalculateTranslationXFromZoomRatio(desiredZoomRatio);
233+
UpdateZoomRatio(desiredZoomRatio);
234+
break;
216235
}
217-
else m_currentSnappedZoomRatio = -1;
236+
237+
var desiredTranslationX = m_zoomRatiosLayout.TranslationX + e.TotalX - m_startingX;
218238

219239
// Stop the user from panning too far or too little
220240
if(desiredTranslationX > m_maxZoomRatiosLayoutTranslationX)
@@ -229,13 +249,30 @@ public void TranslateZoomSlider(PanUpdatedEventArgs e)
229249
m_zoomRatiosLayout.TranslationX = desiredTranslationX;
230250
m_startingX = e.TotalX;
231251

232-
ZoomRatioLevel = actualZoomRatio;
252+
UpdateZoomRatio(CalculateZoomRatio(desiredTranslationX));
233253

234254
break;
235255
}
236256
}
237257
}
238258

259+
private void UpdateZoomRatio(double zoomRatio)
260+
{
261+
// Vibrate when near integers
262+
var roundedZoomRatio = (int)MathF.Round((float)zoomRatio);
263+
if (Math.Abs(roundedZoomRatio - zoomRatio) < 0.025f)
264+
{
265+
if (roundedZoomRatio != m_currentSnappedZoomRatio)
266+
{
267+
VibrationService.SelectionChanged();
268+
}
269+
m_currentSnappedZoomRatio = roundedZoomRatio;
270+
}
271+
else m_currentSnappedZoomRatio = -1;
272+
273+
ZoomRatioLevel = zoomRatio;
274+
}
275+
239276
public double ZoomRatioLevel
240277
{
241278
get => m_zoomRatioLevel;
@@ -249,13 +286,23 @@ private set
249286

250287
public void SetZoomRatio(float zoomRatio)
251288
{
252-
m_zoomRatiosLayout.TranslationX = CalculateTranslationXFromZoomRatio((int)zoomRatio);
289+
zoomRatio = ClampZoomRatio(zoomRatio);
290+
m_zoomRatioLevel = zoomRatio;
291+
m_zoomRatioLevelLabel.Text = zoomRatio.ToString("F1");
292+
293+
if (!m_hasMeasuredZoomRatiosLayout)
294+
{
295+
m_pendingZoomRatio = zoomRatio;
296+
return;
297+
}
298+
299+
m_zoomRatiosLayout.TranslationX = CalculateTranslationXFromZoomRatio(zoomRatio);
300+
m_pendingZoomRatio = null;
253301
}
254302

255303
public async Task<bool> OnPinchToZoom(float zoomRatio)
256304
{
257-
m_zoomRatiosLayout.TranslationX = CalculateTranslationXFromZoomRatio(zoomRatio);
258-
m_zoomRatioLevelLabel.Text = zoomRatio.ToString("F1");
305+
SetZoomRatio(zoomRatio);
259306

260307
m_cancellationTokenSource.Cancel();
261308
m_cancellationTokenSource = new CancellationTokenSource();

src/library/DIPS.Mobile.UI/API/Camera/ImageCapturing/iOS/ImageCapture.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,7 @@ private Size GetCurrentCameraResolution()
5353
private partial void PlatformOnCameraFailed(CameraException cameraException) =>
5454
OnCameraFailed<ImageCapture>(cameraException);
5555

56-
public override AVCaptureDevice? SelectCaptureDevice() =>
57-
AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInWideAngleCamera,
58-
AVMediaTypes.Video, AVCaptureDevicePosition.Back) ??
59-
AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInDualCamera,
60-
AVMediaTypes.Video, AVCaptureDevicePosition.Back);
56+
public override AVCaptureDevice? SelectCaptureDevice() => SelectPreferredBackCaptureDevice();
6157

6258
private void PhotoCaptured(AVCapturePhoto photo)
6359
{
@@ -223,6 +219,6 @@ protected override void Dispose(bool disposing)
223219

224220
m_onPhotoCaptureFailed = null!;
225221
m_onPhotoCaptured = null!;
226-
m_onBeforeCapture = null;
222+
m_onBeforeCapture = null!;
227223
}
228224
}

src/library/DIPS.Mobile.UI/API/Camera/Preview/iOS/PreviewView.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ private void TapToFocus(NSSet touches, UIEvent @event, AVCaptureDevice captureDe
7777
/*UISlider?.BecomeFirstResponder();*/ //Make sure slider does not loose focus for people to slide it after they tap to focus
7878
}
7979

80-
public void AddPinchToZoom(AVCaptureDevice captureDevice)
80+
public void AddPinchToZoom(AVCaptureDevice captureDevice, float maximumZoomRatio)
8181
{
8282
m_pinchToZoomGestureRecognizer =
83-
new UIPinchGestureRecognizer((recognizer => PinchToZoom(recognizer, captureDevice)));
83+
new UIPinchGestureRecognizer((recognizer => PinchToZoom(recognizer, captureDevice, maximumZoomRatio)));
8484
AddGestureRecognizer(m_pinchToZoomGestureRecognizer);
8585
}
8686

8787
//Taken from: https://stackoverflow.com/a/31214458
88-
private void PinchToZoom(UIPinchGestureRecognizer pinchRecognizer, AVCaptureDevice captureDevice)
88+
private void PinchToZoom(UIPinchGestureRecognizer pinchRecognizer, AVCaptureDevice captureDevice, float maximumZoomRatio)
8989
{
9090
if (pinchRecognizer.State == UIGestureRecognizerState.Changed)
9191
{
@@ -94,13 +94,12 @@ private void PinchToZoom(UIPinchGestureRecognizer pinchRecognizer, AVCaptureDevi
9494
try
9595
{
9696
var pinchVelocityDividerFactor = 10f;
97-
var desiredZoomFactor = captureDevice.VideoZoomFactor +
97+
var desiredZoomFactor = (double)captureDevice.VideoZoomFactor +
9898
Math.Atan2(pinchRecognizer.Velocity, pinchVelocityDividerFactor);
9999

100-
var maxZoomFactor = Math.Min(captureDevice.ActiveFormat.VideoMaxZoomFactor, MaxZoomRatio);
100+
var maxZoomFactor = Math.Min((double)captureDevice.ActiveFormat.VideoMaxZoomFactor, maximumZoomRatio);
101101
// Check if desiredZoomFactor fits required range from 1.0 to activeFormat.videoMaxZoomFactor
102-
var zoomFactor = (nfloat)Math.Max(1.0,
103-
Math.Min(desiredZoomFactor, maxZoomFactor));
102+
var zoomFactor = (nfloat)Math.Max(1.0, Math.Min(desiredZoomFactor, maxZoomFactor));
104103
captureDevice.VideoZoomFactor = zoomFactor;
105104

106105
OnZoomChanged?.Invoke((float)zoomFactor);

0 commit comments

Comments
 (0)