Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
47 changes: 45 additions & 2 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,16 @@ private void OnControllerStateChanged(object? sender, StepFlowEventArgs e)

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

internal bool CanGoBackFromCompletedSteps => m_attachedController is { } controller
Comment thread
Vetle444 marked this conversation as resolved.
Outdated
? !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 @@ -173,7 +189,28 @@ private void OnItemCardTapped(object? sender, EventArgs e)
if (controller is null)
{
// Escape hatch (no controller): manually enforce single-active.
if (item.State == StepFlowItemState.Completed && item.LockWhenCompleted) return;
if (item.State == StepFlowItemState.Completed && !item.CanGoBack) return;
if (item.State == StepFlowItemState.Completed && !CanGoBackFromCompletedSteps) return;
if (item.State == StepFlowItemState.Completed && item.CanGoBack)
{
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))
{
other.State = StepFlowItemState.Active;
}
else if (i > targetIndex || other.State == StepFlowItemState.Active)
{
other.State = StepFlowItemState.Disabled;
}
}
return;
}

Comment thread
Vetle444 marked this conversation as resolved.
Outdated
foreach (var other in m_items)
{
if (!ReferenceEquals(other, item) && other.State == StepFlowItemState.Active)
Expand All @@ -186,6 +223,12 @@ private void OnItemCardTapped(object? sender, EventArgs e)
}

if (item.Index < 0) return;
if (item.State == StepFlowItemState.Completed && item.CanGoBack)
{
controller.GoBackTo(item.Index);
return;
}

controller.GoTo(item.Index);
}

Expand Down Expand Up @@ -230,7 +273,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