Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [62.0.0]
- [StepFlow] **BREAKING**: Removed `StepFlowItem.LockWhenCompleted`. Use `StepFlowItem.CanGoBack` to allow selected completed steps to be reopened before the flow is fully completed, while resetting that step and following steps for confirmation again.
- [BarcodeScanner] Fixed invalid scan-rectangle validation results restarting detection before the overlay returned to idle by waiting for the reset animation and adding a short cooldown before rescanning.

## [61.5.3]
- [DateAndTimePicker][DatePicker][HorizontalInlineDatePicker] Fixed issue where the default value of pickers with no set value was reused and never updated until restart.

Expand Down
16 changes: 13 additions & 3 deletions src/app/Components/ComponentsSamples/StepFlow/StepFlowSamples.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
<Switch IsToggled="{Binding AutoScrollIntoView}" />
</dui:HorizontalStackLayout>

<dui:HorizontalStackLayout Spacing="{dui:Sizes size_3}">
<dui:Label Text="CanGoBack"
Style="{dui:Styles Label=UI200}"
VerticalOptions="Center" />
<Switch IsToggled="{Binding CanGoBack}" />
</dui:HorizontalStackLayout>

<!-- Filler so the StepFlow starts off-screen and the auto-scroll is visible. -->
<dui:Label Style="{dui:Styles Label=UI100}"
Text="↓ Scroll down to start the flow. As steps complete, the next active step will auto-scroll into view." />
Expand All @@ -37,7 +44,8 @@
AutoScrollIntoView="{Binding AutoScrollIntoView}">

<!-- Step 1: Confirm patient -->
<dui:StepFlowItem Title="Confirm patient">
<dui:StepFlowItem Title="Confirm patient"
CanGoBack="{Binding CanGoBack}">
<dui:VerticalStackLayout Spacing="{dui:Sizes size_3}">
<dui:Label Style="{dui:Styles Label=UI200}"
Text="{Binding PatientName, StringFormat='Patient: {0}'}" />
Expand All @@ -49,7 +57,8 @@
</dui:StepFlowItem>

<!-- Step 2: Scan sample labels -->
<dui:StepFlowItem Title="Scan sample labels and verify each barcode against the requisition before confirming the patient sampling">
<dui:StepFlowItem Title="Scan sample labels and verify each barcode against the requisition before confirming the patient sampling"
CanGoBack="{Binding CanGoBack}">
<dui:VerticalStackLayout Spacing="{dui:Sizes size_3}">
<dui:Label Style="{dui:Styles Label=UI100}"
Text="Tap to add a fake scanned label. After 3 labels you can finish." />
Expand Down Expand Up @@ -77,7 +86,8 @@
</dui:StepFlowItem>

<!-- Step 3: Confirm sampling -->
<dui:StepFlowItem Title="Confirm sampling">
<dui:StepFlowItem Title="Confirm sampling"
CanGoBack="{Binding CanGoBack}">
<dui:VerticalStackLayout Spacing="{dui:Sizes size_3}">
<dui:Label Style="{dui:Styles Label=UI200}"
Text="{Binding PatientName, StringFormat='Patient: {0}'}" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class StepFlowSamplesViewModel : ViewModel
private bool m_isScanningDone;
private bool m_isFlowFinished;
private bool m_autoScrollIntoView = true;
private bool m_canGoBack = true;

public StepFlowSamplesViewModel()
{
Expand Down Expand Up @@ -59,6 +60,12 @@ public bool AutoScrollIntoView
set => RaiseWhenSet(ref m_autoScrollIntoView, value);
}

public bool CanGoBack
{
get => m_canGoBack;
set => RaiseWhenSet(ref m_canGoBack, value);
}

public AsyncCommand ConfirmPatientCommand { get; }
public AsyncCommand AddScannedLabelCommand { get; }
public AsyncCommand FinishScanningCommand { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ internal class BarcodeScanRectangleOverlay : Grid
private const float TrackingTargetLargeChangeThreshold = 48f;
private const float TrackingTargetSmoothing = .35f;
private const float TrackingTargetFastSmoothing = .65f;
private const uint BracketsReturnLength = 280;
private const int FailureRescanCooldownMilliseconds = 500;

private const string AnimationKeyCornerBreathing = "CornerBreathing";
private const string AnimationKeyBracketsToBarcode = "BracketsToBarcode";
Expand Down Expand Up @@ -631,10 +633,16 @@ internal async Task PlayFailureAndResetAsync(string? errorMessage)
await m_cornersGraphicsView.TranslateToAsync(-shakeDistance / 2, 0, 65, Easing.CubicInOut);
await m_cornersGraphicsView.TranslateToAsync(0, 0, 80, Easing.CubicOut);

ResetBarcodeDetection();
await ResetBarcodeDetectionAsync();
await Task.Delay(FailureRescanCooldownMilliseconds);
}

internal void ResetBarcodeDetection()
{
_ = ResetBarcodeDetectionAsync();
}

private Task ResetBarcodeDetectionAsync()
{
m_cornersGraphicsView.AbortAnimation(AnimationKeyBracketsToBarcode);
m_cornersGraphicsView.AbortAnimation(AnimationKeyBracketsReturn);
Expand All @@ -652,11 +660,11 @@ internal void ResetBarcodeDetection()

if (currentRect is not null)
{
// Animate back to scan rectangle
var startX = currentRect.Value.X;
var startY = currentRect.Value.Y;
var startW = currentRect.Value.Width;
var startH = currentRect.Value.Height;
var taskCompletionSource = new TaskCompletionSource<bool>();

var animation = new Animation(v =>
{
Expand All @@ -671,21 +679,24 @@ internal void ResetBarcodeDetection()

animation.Commit(m_cornersGraphicsView, AnimationKeyBracketsReturn,
rate: 16,
length: 280,
length: BracketsReturnLength,
finished: (_, cancelled) =>
{
m_cornersDrawable.OverrideRect = null;
if (!cancelled)
StartBreathingAnimation();

taskCompletionSource.TrySetResult(true);
});
}
else
{
m_cornersDrawable.OverrideRect = null;
StartBreathingAnimation();

return taskCompletionSource.Task;
}

m_cornersDrawable.OverrideRect = null;
StartBreathingAnimation();
return Task.CompletedTask;
}

private RectF GetScanRectangleForDrawable()
{
var w = (float)Width;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public bool AutoScrollIntoView
nameof(AllowDirectStepActivation),
typeof(bool),
typeof(StepFlow),
defaultValue: false);
defaultValue: false,
propertyChanged: (b, _, _) => ((StepFlow)b).RefreshItemTapTargets());

public static readonly BindableProperty AutoScrollIntoViewProperty = BindableProperty.Create(
nameof(AutoScrollIntoView),
Expand Down
94 changes: 84 additions & 10 deletions src/library/DIPS.Mobile.UI/Components/StepFlow/StepFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ private void ReindexAndSyncStack()
{
m_stack.Children.Insert(Math.Min(i, m_stack.Children.Count), item);
}
item.RefreshTapTarget();
}
}

private void RefreshItemTapTargets()
{
foreach (var item in m_items)
{
item.RefreshTapTarget();
}
}

Expand Down Expand Up @@ -161,9 +170,22 @@ private void OnControllerStateChanged(object? sender, StepFlowEventArgs e)

private void OnControllerFlowCompleted(object? sender, EventArgs e)
{
RefreshItemTapTargets();
FlowCompleted?.Invoke(this, EventArgs.Empty);
}

internal bool CanActivateCompletedStep(StepFlowItem item) =>
m_items.Contains(item) &&
item.State == StepFlowItemState.Completed &&
item.CanGoBack &&
!IsFlowCompleted();

private bool IsFlowCompleted() => m_attachedController is { } controller
? controller.IsCompleted
: AreAllItemsCompleted();

private bool AreAllItemsCompleted() => m_items.Count > 0 && m_items.All(item => item.State == StepFlowItemState.Completed);

private void OnItemCardTapped(object? sender, EventArgs e)
{
if (!IsEnabled) return;
Expand All @@ -172,20 +194,72 @@ private void OnItemCardTapped(object? sender, EventArgs e)
var controller = m_attachedController;
if (controller is null)
{
// Escape hatch (no controller): manually enforce single-active.
if (item.State == StepFlowItemState.Completed && item.LockWhenCompleted) return;
foreach (var other in m_items)
ActivateTappedItemWithoutController(item);
return;
}

ActivateTappedItemWithController(controller, item);
}

private void ActivateTappedItemWithoutController(StepFlowItem item)
{
// Escape hatch (no controller): manually enforce single-active.
if (item.State == StepFlowItemState.Completed)
{
ActivateCompletedItemWithoutController(item);
return;
}

DisableOtherActiveItems(item);
item.State = StepFlowItemState.Active;
}

private void ActivateCompletedItemWithoutController(StepFlowItem item)
{
if (!CanActivateCompletedStep(item)) return;

var targetIndex = m_items.IndexOf(item);
if (targetIndex < 0) return;

for (var i = 0; i < m_items.Count; i++)
{
var other = m_items[i];
if (ReferenceEquals(other, item))
{
if (!ReferenceEquals(other, item) && other.State == StepFlowItemState.Active)
{
other.State = StepFlowItemState.Disabled;
}
other.State = StepFlowItemState.Active;
continue;
}

if (i > targetIndex || other.State == StepFlowItemState.Active)
{
other.State = StepFlowItemState.Disabled;
}
}
}

private void DisableOtherActiveItems(StepFlowItem item)
{
foreach (var other in m_items)
{
if (!ReferenceEquals(other, item) && other.State == StepFlowItemState.Active)
{
other.State = StepFlowItemState.Disabled;
}
item.State = StepFlowItemState.Active;
return;
}
}

private void ActivateTappedItemWithController(StepFlowController controller, StepFlowItem item)
{
if (item.Index < 0) return;
if (item.State == StepFlowItemState.Completed)
{
if (CanActivateCompletedStep(item))
{
controller.GoBackTo(item.Index);
}
return;
}

controller.GoTo(item.Index);
}

Expand Down Expand Up @@ -230,7 +304,7 @@ private void OnParentChangedInvalidateScroller(object? sender, EventArgs e)
if (m_scrollerResolved) return m_cachedScroller;
m_scrollerResolved = true;

Element? walker = Parent;
var walker = Parent;
while (walker is not null)
{
if (walker is MauiScrollView sv)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public void Complete(int index)

/// <summary>
/// Activates the step at <paramref name="index"/>. No-op if the step is
/// <see cref="StepFlowItemState.Disabled"/> or <see cref="StepFlowItemState.Completed"/>.
/// <see cref="StepFlowItemState.Completed"/>. Use <see cref="GoBackTo"/> to activate a
/// completed step and require it to be confirmed again.
/// </summary>
public void GoTo(int index)
{
Expand All @@ -114,6 +115,35 @@ public void GoTo(int index)
ActivateInternal(index);
}

/// <summary>
/// Activates a completed previous step and resets that step and all following steps so they
/// must be confirmed again. No-op if the step is not completed or the flow is already
/// completed.
/// </summary>
public void GoBackTo(int index)
{
if (!IsValidIndex(index)) return;
if (IsCompleted) return;
if (m_states[index] != StepFlowItemState.Completed) return;

for (var i = 0; i < m_stepCount; i++)
{
if (i == index)
{
if (m_states[i] != StepFlowItemState.Active)
SetStateInternal(i, StepFlowItemState.Active);
}
else if (i > index || m_states[i] == StepFlowItemState.Active)
{
if (m_states[i] != StepFlowItemState.Disabled)
SetStateInternal(i, StepFlowItemState.Disabled);
}
}

CurrentIndex = index;
StepActivated?.Invoke(this, new StepFlowEventArgs(index));
}

/// <summary>Resets the flow: step 0 becomes <see cref="StepFlowItemState.Active"/>, the rest <see cref="StepFlowItemState.Disabled"/>.</summary>
public void Reset()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ public DataTemplate? IndicatorTemplate
set => SetValue(IndicatorTemplateProperty, value);
}

/// <summary>When <c>true</c> (the default), tapping a completed step does nothing.</summary>
public bool LockWhenCompleted
/// <summary>
/// When <c>true</c>, tapping this completed step activates it again and resets this and the
/// following steps so they must be confirmed again. Only applies while the flow is in
/// progress. Defaults to <c>false</c>.
/// </summary>
public bool CanGoBack
{
get => (bool)GetValue(LockWhenCompletedProperty);
set => SetValue(LockWhenCompletedProperty, value);
get => (bool)GetValue(CanGoBackProperty);
set => SetValue(CanGoBackProperty, value);
}

/// <summary>
Expand Down Expand Up @@ -83,11 +87,12 @@ public StepFlowItemState State
typeof(StepFlowItem),
propertyChanged: (b, _, _) => ((StepFlowItem)b).OnIndicatorTemplateChanged());

public static readonly BindableProperty LockWhenCompletedProperty = BindableProperty.Create(
nameof(LockWhenCompleted),
public static readonly BindableProperty CanGoBackProperty = BindableProperty.Create(
nameof(CanGoBack),
typeof(bool),
typeof(StepFlowItem),
defaultValue: true);
defaultValue: false,
propertyChanged: (b, _, _) => ((StepFlowItem)b).RefreshTapTarget());

public static readonly BindableProperty StateProperty = BindableProperty.Create(
nameof(State),
Expand Down
Loading