Scandit: Integración de Matrix Scan

Dentro de las posibilidades que ofrece Scandit, Matrix Scan es su punto fuerte. Permite escanear varios códigos de barras simultáneamente. Por cada frame de la cámara se detectan n códigos y Scandit así nos lo notifica, con un conjunto.

Matrix Scan también ofrece la posibilidad de incluir realidad aumentada pero lo trataré en el siguiente artículo.

Será común que los clientes quieran compaginar Matrix Scan con Barcode Scanning, normalmente a nivel de vista. Esto es así porque en la lógica de negocio es común que existan historias de usuario donde escanear secuencialmente tiene más sentido.

Por lo tanto nuestro objetivo es añadir Matrix Scan al código construido en el anterior artículo ("Integración en Xamarin Forms del SDK"), donde abordamos Barcode Scanning, ¡sin romper nada!

Impedimentos en el SDK. Empezamos bien...

Tras muchas horas de sudor, sangre y lágrimas; creo poder hacer estas afirmaciones:

  • El SDK de Scandit no permite que existan dos instancias o más de DataCaptureContext para la misma licencia.
    • Esto vendría genial para poder dedicar uno a Barcode Scanning y otro Matrix Scan pero no es posible.
  • Una instancia de DataCaptureContext no permite crear objetos para Barcode Scanning y Matrix Scan al mismo tiempo.
  • A día de hoy en la documentación de Scandit no hay ningún ejemplo para Xamarin Forms donde convivan Barcode Scanning y Matrix Scan.

Entonces ¿Ahora qué? Nos queda una alternativa. Podemos hacer que nuestro singleton ScanditManager trabaje con ambos modos reseteando el contexto con cada cambio de modo.

Implementado Matrix Scan en ScanditManager

Necesitamos:

  • Un enumerado para definir los dos modos de Scandit.
  • Una propiedad BarcodeTracking.
  • Una propiedad BarcodeTrackingSettings.
  • Una propiedad del enumerado definido antes.
  • Un método para cambiar de modo.

Nos queda alto tal que así:

public enum ScanditMode

{

    BarcodeScanning,

    MatrixScan

}

public sealed class ScanditManager

{

    #region singleton stuff

    private static readonly Lazy<ScanditManager> lazyInstance = new(

        () => new ScanditManager(),

        LazyThreadSafetyMode.PublicationOnly

    );

    public static ScanditManager Instance => lazyInstance.Value;

    private ScanditManager() { }

    #endregion

    private string licence;

    private readonly ISet<Symbology> symbologies = new HashSet<Symbology>

    {

        Symbology.Code128,

        Symbology.Gs1Databar,

        Symbology.Qr

    };

    public bool IsInitialized { get; private set; } = false;

    public ScanditMode CurrentScanditMode { get; private set; }

    public DataCaptureContext Context { get; private set; }

    public Camera Camera { get; private set; } = Camera.GetCamera(CameraPosition.WorldFacing);

    public CameraSettings CameraSettings { get; } = BarcodeCapture.RecommendedCameraSettings;

    //For Barcode Scanning

    public BarcodeCapture BarcodeCapture { get; private set; }

    public BarcodeCaptureSettings BarcodeCaptureSettings { get; private set; }

    //For Matrix Scan

    public BarcodeTracking BarcodeTracking { get; private set; }

    public BarcodeTrackingSettings BarcodeTrackingSettings { get; private set; }

    public async Task InitializeAsync(string scanditLicence)

    {

        if (IsInitialized)

            return;

        licence = scanditLicence;

        await InitContextAsync(licence);

        InitBarcodeScanning();

        IsInitialized = true;

    }

    public async Task ChangeMode(ScanditMode newMode)

    {

        if (!IsInitialized)

            throw new InvalidOperationException("Must initialize first!");

        if (CurrentScanditMode == newMode)

            return;

        Reset();

        await InitContextAsync(licence);

        switch (newMode)

        {

            case ScanditMode.BarcodeScanning:

                InitBarcodeScanning();

                break;

            case ScanditMode.MatrixScan:

                InitMatrixScan();

                break;

        }

    }

    private async Task InitContextAsync(string scanditLicence)

    {

        Context = DataCaptureContext.ForLicenseKey(scanditLicence);

        await Context.SetFrameSourceAsync(Camera);

    }

    private void InitBarcodeScanning()

    {

        BarcodeCaptureSettings = BarcodeCaptureSettings.Create();

        BarcodeCaptureSettings.CodeDuplicateFilter = TimeSpan.FromSeconds(4);

        BarcodeCaptureSettings.EnableSymbologies(symbologies);

        BarcodeCapture = BarcodeCapture.Create(Context, BarcodeCaptureSettings);

        BarcodeCapture.Enabled = false; //Starts disabled for performance purposes

        BarcodeCapture.Feedback.Success = new Feedback(null, null); //No feedback for this example

        CurrentScanditMode = ScanditMode.BarcodeScanning;

    }

    private void InitMatrixScan()

    {

        BarcodeTrackingSettings = BarcodeTrackingSettings.Create(BarcodeTrackingScenario.A);

        BarcodeTrackingSettings.EnableSymbologies(symbologies);

        BarcodeTracking = BarcodeTracking.Create(Context, BarcodeTrackingSettings);

        BarcodeTracking.Enabled = false; //Starts disabled for performance purposes

        CurrentScanditMode = ScanditMode.MatrixScan;

    }

    private void Reset()

    {

        BarcodeCapture = null;

        BarcodeCaptureSettings = null;

        BarcodeTracking = null;

        BarcodeTrackingSettings = null;

        Context = null;

    }

}

Crear un ViewModel base para MatrixScan

Nada nuevo si recuerdas el anterior artículo, pero esta vez son los objetos de MatrixScan los que propagamos en vez de los Barcode Scanning. Esta vez tenemos que implementar dos interfaces en lugar de una: IbarcodeTrackingListener y IbarcodeTrackingBasicOverlayListener.

Nos queda algo tal que así:

public abstract class MatrixScanViewModelBase : YourViewModelBase, IBarcodeTrackingListener, IBarcodeTrackingBasicOverlayListener

{

    private static readonly object lockObject = new();

    public Camera Camera => ScanditManager.Instance.Camera;

    public DataCaptureContext Context => ScanditManager.Instance.Context;

    public BarcodeTracking BarcodeTracking => ScanditManager.Instance.BarcodeTracking;

    protected abstract void OnMatrixScanScannedBarcodes(IEnumerable<Barcode> scannedBarcodes);

    protected abstract Brush GetBrushFor(Barcode barcode); //Overrides must be thread safe

    protected void StartCameraScanner()

    {

        BarcodeTracking.AddListener(this);

    }

    protected void StopCameraScanner()

    {

        BarcodeTracking.RemoveListener(this);

    }

    protected async Task<bool> StartCameraScannerReadingAsync()

    {

        PermissionStatus permissionStatus = await Permissions.CheckStatusAsync<Permissions.Camera>();

        bool readingStarted = false;

        if (permissionStatus != PermissionStatus.Granted)

        {

            permissionStatus = await Permissions.RequestAsync<Permissions.Camera>();

            if (permissionStatus == PermissionStatus.Granted)

                readingStarted = await Camera.SwitchToDesiredStateAsync(FrameSourceState.On);

        }

        else

        {

            readingStarted = await Camera.SwitchToDesiredStateAsync(FrameSourceState.On);

        }

        if (readingStarted)

            ScanditManager.Instance.BarcodeTracking.Enabled = true;

        return readingStarted;

    }

    protected async Task<bool> StopCameraScannerReadingAsync()

    {

        ScanditManager.Instance.BarcodeTracking.Enabled = false;

        return await Camera.SwitchToDesiredStateAsync(FrameSourceState.Off);

    }

    #region IBarcodeTrackingListener implementation

    public void OnObservationStarted(BarcodeTracking barcodeTracking) { }

    public void OnObservationStopped(BarcodeTracking barcodeTracking) { }

    public void OnSessionUpdated(

        BarcodeTracking barcodeTracking,

        BarcodeTrackingSession session,

        IFrameData frameData)

    {

        if (session == null || !session.TrackedBarcodes.Any())

            return;

        barcodeTracking.Enabled = false;

        lock (lockObject)

        {

            try

            {

                OnMatrixScanScannedBarcodes(

                    session.TrackedBarcodes.Values.Select(it => it.Barcode)

                );

            }

            finally

            {

                barcodeTracking.Enabled = true;

            }

        }

    }

    #endregion

    #region IBarcodeTrackingBasicOverlayListener implementation

    public Brush BrushForTrackedBarcode(

        BarcodeTrackingBasicOverlay overlay,

        TrackedBarcode trackedBarcode)

    {

        return GetBrushFor(trackedBarcode.Barcode);

    }

    public void OnTrackedBarcodeTapped(

        BarcodeTrackingBasicOverlay overlay,

        TrackedBarcode trackedBarcode)

    {

    }

    #endregion

}

Creamos una nueva vista cuyo ViewModel debe heredar del ViewModel base anterior. Creamos su ContentPage y definimos lo siguiente en el xaml:

<scanditCore:DataCaptureView

    DataCaptureContext="{Binding Context}">

 

    <scanditBarcode:BarcodeTrackingBasicOverlay

        BarcodeTracking="{Binding BarcodeTracking}"

        Listener="{Binding .}"/>

</scanditCore:DataCaptureView>

No debemos olvidar añadir la referencia a scanditCore y scanditBarcode:

xmlns:scanditCore="clr-namespace:Scandit.DataCapture.Core.UI.Unified;assembly=ScanditCaptureCoreUnified"

xmlns:scanditBarcode="clr-namespace:Scandit.DataCapture.Barcode.UI.Unified;assembly=ScanditBarcodeCaptureUnified"

Ya sólo nos hace falta sobre escribir OnMatrixScanScannedBarcodesGetBrushFor e invocar los métodos para iniciar/detener la cámara y la lectura cuando necesitemos. No lo incluyo aquí pero es interesante hacer esto cuando la app pasa a segundo plano o vuelve a primer plano. No debemos olvidar inicializar el servicio de dependencia con la licencia de scandit antes de entrar en la vista y cambiar el modo a Matrix Scan.

Conclusión

Vemos que igual que en que en el artículo anterior que el API de Scandit, por mucha y muy útil sea su funcionalidad, es de farragoso uso. Echo de menos en esta API poder trabajar con instancias a interfaces en vez de clases, es decir, esta API no tiene inversión de dependencias.

Estamos obligados a propagar los objetos de la API más allá de lo que debería ser necesario para funcionalidades más avanzadas que no caben ni son objetivo de este artículo. También nos obliga a inventar una solución mediante super clases abstractas para tener una única implementación de las interfaces que necesita a través de patrón Listener, no acostumbrado a usar por programadores C# por la disponibilidad de eventos.