Skip to content

Commit 340c269

Browse files
authored
Expand BarcodeScanner scanning workflows (#860)
1 parent 95604e3 commit 340c269

39 files changed

Lines changed: 2733 additions & 125 deletions

.github/copilot-instructions.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ Format: `[Component/Feature] Description` (see existing entries for style)
254254
6. **Don't** use `FontFamily="monospace"` - use `FontFamily="Body"`, `"UI"`, or `"Header"`
255255
7. **Don't** forget to test on both platforms - behavior often differs
256256
8. **When reusing an existing component in a new context** (e.g. embedding a component inside a new handler, effect, or renderer), always read the component's existing canonical platform consumer first and replicate **all** of its event subscriptions, lifecycle hooks, and teardown logic. Never assume that rendering the component is sufficient. Components often have additional contracts (update events, binding context propagation, etc.) that only the canonical consumer reveals. Missing these causes silent regressions where the component renders correctly initially but fails to update afterwards.
257-
9. **StepFlow tap targets**: A collapsed/activatable StepFlow card should be tappable across the whole card surface, not only on the header text. Preserve interactive content behavior when the active body contains buttons, inputs, or other controls.
258257

259258
## Key Files to Reference
260259
- `API/Builder/AppHostBuilderExtensions.cs` - Library initialization and handler registration

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## [59.0.0]
2+
- [BarcodeScanner] **BREAKING**: Replaced positional `Start` parameters and `BarcodeScanningSettings` with `BarcodeScanner.Start(BarcodeScannerStartOptions)` so preview, camera failure handling, validation, async callbacks, scan rectangle, and completion behavior are configured in one scanner session contract.
3+
- [BarcodeScanner] Added visible focused scan rectangle overlay controlled by `BarcodeScannerStartOptions.ScanRectangle` and `BarcodeScanRectangleOptions`
4+
- [BarcodeScanner] Barcode results are now filtered to only include barcodes within the visible scan rectangle region
5+
- [BarcodeScanner] Added validation-aware success and failure animations, optional required scan count progress with animated bottom counter and barcode collection animation, initial count support, duplicate scan cooldown, completion callbacks, and pause/resume support that keeps overlays attached
6+
- [BarcodeScanner] Added validation results with optional typed state for accepted barcodes and optional reason codes for rejected barcodes
7+
- [BarcodeScanner] Fixed duplicate scan suppression so an already-confirmed barcode is not retried while validation and success animations are running
8+
19
## [58.1.0]
210
- [StepFlow] Added new accordion-style multi-step flow component.
311

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml
2+
version="1.0"
3+
encoding="utf-8"?>
4+
5+
<dui:ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
7+
xmlns:dui="http://dips.com/mobile.ui"
8+
x:Class="Components.ComponentsSamples.BarcodeScanning.BarcodeCounterSample">
9+
<dui:ContentPage.ToolbarItems>
10+
<ToolbarItem IconImageSource="{dui:Icons close_line}"
11+
Clicked="Close" />
12+
</dui:ContentPage.ToolbarItems>
13+
<dui:CameraPreview x:Name="CameraPreview" IsInFullscreen="False"/>
14+
</dui:ContentPage>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using DIPS.Mobile.UI.API.Camera;
2+
using DIPS.Mobile.UI.API.Camera.BarcodeScanning;
3+
4+
namespace Components.ComponentsSamples.BarcodeScanning;
5+
6+
public partial class BarcodeCounterSample
7+
{
8+
private const int RequiredScanCount = 5;
9+
private readonly BarcodeScanner m_barcodeScanner;
10+
private readonly BarcodeScannerStartOptions m_barcodeScannerStartOptions;
11+
private readonly HashSet<string> m_acceptedBarcodes = [];
12+
13+
public BarcodeCounterSample()
14+
{
15+
InitializeComponent();
16+
m_barcodeScanner = new BarcodeScanner();
17+
m_barcodeScannerStartOptions = new BarcodeScannerStartOptions
18+
{
19+
Preview = CameraPreview,
20+
OnCameraFailed = CameraFailed,
21+
ScanRectangle = new BarcodeScanRectangleOptions
22+
{
23+
WidthFraction = 0.8f,
24+
HeightFraction = 0.3f
25+
},
26+
Completion = new BarcodeScanCompletionOptions
27+
{
28+
RequiredCount = RequiredScanCount,
29+
OnCompletedAsync = OnScanCountCompletedAsync
30+
},
31+
OnBarcodeAcceptedAsync = HandleBarcodeAcceptedAsync,
32+
ValidateBarcodeAsync = ValidateBarcodeAsync,
33+
OnBarcodeRejectedAsync = OnBarcodeRejectedAsync
34+
};
35+
}
36+
37+
private async Task Start()
38+
{
39+
try
40+
{
41+
await m_barcodeScanner.Start(m_barcodeScannerStartOptions);
42+
}
43+
catch (Exception exception)
44+
{
45+
await Application.Current?.MainPage?.DisplayAlert("Failed, check console!", exception.Message, "Ok")!;
46+
Console.WriteLine(exception);
47+
}
48+
}
49+
50+
private void CameraFailed(CameraException e)
51+
{
52+
App.Current.MainPage.DisplayAlert("Something failed!", e.Message, "Ok");
53+
}
54+
55+
private Task HandleBarcodeAcceptedAsync(BarcodeScanResult barcodeScanResult)
56+
{
57+
m_acceptedBarcodes.Add(barcodeScanResult.Barcode.RawValue);
58+
Console.WriteLine($"Accepted barcode: {barcodeScanResult.Barcode.RawValue}");
59+
return Task.CompletedTask;
60+
}
61+
62+
private async Task<BarcodeScanValidationResult> ValidateBarcodeAsync(string barcode)
63+
{
64+
await Task.Delay(150);
65+
66+
if (string.IsNullOrWhiteSpace(barcode))
67+
{
68+
return BarcodeScanValidationResult.Invalid();
69+
}
70+
71+
return m_acceptedBarcodes.Contains(barcode)
72+
? BarcodeScanValidationResult.Invalid()
73+
: BarcodeScanValidationResult.Valid();
74+
}
75+
76+
private Task OnBarcodeRejectedAsync(BarcodeScanResult barcodeScanResult, BarcodeScanValidationResult validationResult)
77+
{
78+
Console.WriteLine($"Rejected barcode: {barcodeScanResult.Barcode.RawValue}. {validationResult.ErrorMessage}");
79+
return Task.CompletedTask;
80+
}
81+
82+
private Task OnScanCountCompletedAsync()
83+
{
84+
Console.WriteLine("Required barcode scan count completed.");
85+
return Task.CompletedTask;
86+
}
87+
88+
protected override void OnAppearing()
89+
{
90+
_ = Start();
91+
base.OnAppearing();
92+
}
93+
94+
private void Close(object? sender, EventArgs e)
95+
{
96+
m_barcodeScanner.StopAndDispose();
97+
Shell.Current.Navigation.PopModalAsync();
98+
}
99+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml
2+
version="1.0"
3+
encoding="utf-8"?>
4+
5+
<dui:ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
7+
xmlns:dui="http://dips.com/mobile.ui"
8+
x:Class="Components.ComponentsSamples.BarcodeScanning.BarcodeOverlaySample">
9+
<dui:ContentPage.ToolbarItems>
10+
<ToolbarItem IconImageSource="{dui:Icons close_line}"
11+
Clicked="Close" />
12+
</dui:ContentPage.ToolbarItems>
13+
<dui:CameraPreview x:Name="CameraPreview" IsInFullscreen="False"/>
14+
</dui:ContentPage>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using DIPS.Mobile.UI.API.Camera;
2+
using DIPS.Mobile.UI.API.Camera.BarcodeScanning;
3+
using DIPS.Mobile.UI.Resources.Styles;
4+
using DIPS.Mobile.UI.Resources.Styles.Label;
5+
using Colors = Microsoft.Maui.Graphics.Colors;
6+
7+
namespace Components.ComponentsSamples.BarcodeScanning;
8+
9+
public partial class BarcodeOverlaySample
10+
{
11+
private readonly BarcodeScanner m_barcodeScanner;
12+
private BarcodeScanningResultBottomSheet? m_barCodeResultBottomSheet;
13+
private View? m_topToolbarView;
14+
15+
public BarcodeOverlaySample()
16+
{
17+
InitializeComponent();
18+
m_barcodeScanner = new BarcodeScanner();
19+
}
20+
21+
private async Task Start()
22+
{
23+
try
24+
{
25+
await m_barcodeScanner.Start(new BarcodeScannerStartOptions
26+
{
27+
Preview = CameraPreview,
28+
OnCameraFailed = CameraFailed,
29+
OnBarcodeAcceptedAsync = HandleBarcodeAcceptedAsync,
30+
ScanRectangle = new BarcodeScanRectangleOptions
31+
{
32+
WidthFraction = 0.8f,
33+
HeightFraction = 0.3f
34+
}
35+
});
36+
37+
CameraPreview.AddTopToolbarView(GetTopToolbarView());
38+
}
39+
catch (Exception exception)
40+
{
41+
await Application.Current?.MainPage?.DisplayAlert("Failed, check console!", exception.Message, "Ok")!;
42+
Console.WriteLine(exception);
43+
}
44+
}
45+
46+
private void CameraFailed(CameraException e)
47+
{
48+
App.Current.MainPage.DisplayAlert("Something failed!", e.Message, "Ok");
49+
}
50+
51+
private View GetTopToolbarView()
52+
{
53+
return m_topToolbarView ??= new VerticalStackLayout
54+
{
55+
Spacing = 4,
56+
Children =
57+
{
58+
new Label
59+
{
60+
Text = "Scan barcode",
61+
Style = Styles.GetLabelStyle(LabelStyle.UI200),
62+
TextColor = Colors.White,
63+
HorizontalTextAlignment = TextAlignment.Center
64+
},
65+
new Label
66+
{
67+
Text = "Point the camera at a barcode",
68+
Style = Styles.GetLabelStyle(LabelStyle.UI100),
69+
TextColor = Colors.White,
70+
HorizontalTextAlignment = TextAlignment.Center,
71+
Opacity = 0.7
72+
}
73+
}
74+
};
75+
}
76+
77+
private Task HandleBarcodeAcceptedAsync(BarcodeScanResult barcodeScanResult)
78+
{
79+
if (m_barCodeResultBottomSheet is { HasBarCode: true })
80+
{
81+
return Task.CompletedTask;
82+
}
83+
84+
m_barCodeResultBottomSheet = new BarcodeScanningResultBottomSheet();
85+
m_barCodeResultBottomSheet.Closed += BottomSheetClosed;
86+
m_barCodeResultBottomSheet.OpenWithBarCode(barcodeScanResult);
87+
m_barcodeScanner.PauseScanning(resetOverlay: false);
88+
return Task.CompletedTask;
89+
}
90+
91+
private void BottomSheetClosed(object? sender, EventArgs e)
92+
{
93+
if (m_barCodeResultBottomSheet != null)
94+
{
95+
m_barCodeResultBottomSheet.Closed -= BottomSheetClosed;
96+
}
97+
98+
m_barCodeResultBottomSheet = null;
99+
m_barcodeScanner.ResumeScanning();
100+
}
101+
102+
protected override void OnAppearing()
103+
{
104+
_ = Start();
105+
base.OnAppearing();
106+
}
107+
108+
private void Close(object? sender, EventArgs e)
109+
{
110+
m_barcodeScanner.StopAndDispose();
111+
Shell.Current.Navigation.PopModalAsync();
112+
}
113+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml
2+
version="1.0"
3+
encoding="utf-8"?>
4+
5+
<dui:ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
7+
xmlns:dui="http://dips.com/mobile.ui"
8+
x:Class="Components.ComponentsSamples.BarcodeScanning.BarcodeScanningHubSamples">
9+
<dui:VerticalStackLayout Spacing="0"
10+
Margin="{dui:Thickness Left=content_margin_medium, Right=content_margin_medium, Top=content_margin_large}"
11+
dui:Layout.AutoCornerRadius="True"
12+
VerticalOptions="Start">
13+
<dui:NavigationListItem x:Name="BasicScannerItem"
14+
Title="Basic Barcode Scanner"
15+
Subtitle="Original scanner without overlays"
16+
HasBottomDivider="True" />
17+
18+
<dui:NavigationListItem x:Name="OverlayScannerItem"
19+
Title="Barcode Scanner with Overlay"
20+
Subtitle="Scan rectangle and custom content"
21+
HasBottomDivider="True" />
22+
23+
<dui:NavigationListItem x:Name="TooltipScannerItem"
24+
Title="Barcode Scanner with Tooltip"
25+
Subtitle="Scan rectangle with tooltip view above"
26+
HasBottomDivider="True" />
27+
28+
<dui:NavigationListItem x:Name="CounterScannerItem"
29+
Title="Barcode Counter"
30+
Subtitle="Scan continuously and count barcodes" />
31+
</dui:VerticalStackLayout>
32+
</dui:ContentPage>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Components.ComponentsSamples.BarcodeScanning;
2+
3+
public partial class BarcodeScanningHubSamples
4+
{
5+
public BarcodeScanningHubSamples()
6+
{
7+
InitializeComponent();
8+
BasicScannerItem.Command = new Command(() => OpenModal(new BarcodeScanningSample()));
9+
OverlayScannerItem.Command = new Command(() => OpenModal(new BarcodeOverlaySample()));
10+
TooltipScannerItem.Command = new Command(() => OpenModal(new BarcodeTooltipSample()));
11+
CounterScannerItem.Command = new Command(() => OpenModal(new BarcodeCounterSample()));
12+
}
13+
14+
private static void OpenModal(Page page)
15+
{
16+
Shell.Current.Navigation.PushModalAsync(new NavigationPage(page));
17+
}
18+
}

src/app/Components/ComponentsSamples/BarcodeScanning/BarcodeScanningSample.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
xmlns:dui="http://dips.com/mobile.ui"
88
x:Class="Components.ComponentsSamples.BarcodeScanning.BarcodeScanningSample">
99
<dui:ContentPage.ToolbarItems>
10+
<ToolbarItem IconImageSource="{dui:Icons close_line}"
11+
Clicked="Close" />
1012
<ToolbarItem IconImageSource="{dui:Icons help_outline}"
1113
Clicked="ShowTip">
1214
</ToolbarItem>

src/app/Components/ComponentsSamples/BarcodeScanning/BarcodeScanningSample.xaml.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ private async Task Start()
2121
{
2222
try
2323
{
24-
await m_barcodeScanner.Start(CameraPreview, DidFindBarcode, CameraFailed);
24+
await m_barcodeScanner.Start(new BarcodeScannerStartOptions
25+
{
26+
Preview = CameraPreview,
27+
OnCameraFailed = CameraFailed,
28+
OnBarcodeAcceptedAsync = HandleBarcodeAcceptedAsync
29+
});
2530
}
2631
catch (Exception exception)
2732
{
@@ -35,34 +40,35 @@ private void CameraFailed(CameraException e)
3540
App.Current.MainPage.DisplayAlert("Something failed!", e.Message, "Ok");
3641
}
3742

38-
private void DidFindBarcode(BarcodeScanResult barcodeScanResult)
43+
private Task HandleBarcodeAcceptedAsync(BarcodeScanResult barcodeScanResult)
3944
{
4045
if (m_barCodeResultBottomSheet is
4146
{
4247
HasBarCode: true
4348
})
4449
{
45-
return;
50+
return Task.CompletedTask;
4651
}
4752

4853
m_barCodeResultBottomSheet = new BarcodeScanningResultBottomSheet();
4954
m_barCodeResultBottomSheet.Closed += BottomSheetClosed;
5055
m_barCodeResultBottomSheet.OpenWithBarCode(barcodeScanResult);
51-
m_barcodeScanner.StopAndDispose();
56+
m_barcodeScanner.PauseScanning();
57+
return Task.CompletedTask;
5258
}
5359

54-
private async void BottomSheetClosed(object? sender, EventArgs e)
60+
private void BottomSheetClosed(object? sender, EventArgs e)
5561
{
5662
if (m_barCodeResultBottomSheet != null)
5763
{
5864
m_barCodeResultBottomSheet.Closed -= BottomSheetClosed;
5965
}
6066

6167
m_barCodeResultBottomSheet = null;
62-
_ = Start();
68+
m_barcodeScanner.ResumeScanning();
6369
}
6470

65-
protected override async void OnAppearing()
71+
protected override void OnAppearing()
6672
{
6773
_ = Start();
6874
base.OnAppearing();
@@ -72,4 +78,10 @@ private void ShowTip(object? sender, EventArgs e)
7278
{
7379
CameraPreview.ShowZoomSliderTip("Om strekkoden er liten, er det bedre å bruke zoom funksjonen istedet for å ha mobilen for nært strekkoden. Du kan også dra i slideren for å justere zoomen.");
7480
}
81+
82+
private void Close(object? sender, EventArgs e)
83+
{
84+
m_barcodeScanner.StopAndDispose();
85+
Shell.Current.Navigation.PopModalAsync();
86+
}
7587
}

0 commit comments

Comments
 (0)