Integracion APP

Scandit. Integração de Matrix Scan

Entre as possibilidades oferecidas pela Scandit, o Matrix Scan é o seu ponto forte. Permite a leitura simultânea de vários códigos de barras. Para cada frame da câmara, são detectados n códigos e a Scandit notifica-nos assim, com um conjunto.

O Matrix Scan também oferece a possibilidade de incluir a realidade aumentada, mas iremos falar sobre isso no próximo artigo.

Será comum que os clientes queiram combinar o Matrix Scan com o Barcode Scanning, normalmente ao nível da visualização. Isto porque na lógica empresarial é comum que haja histórias de utilizadores em que a digitalização sequencial faz mais sentido.

Portanto, o nosso objetivo é adicionar o Matrix Scan ao código construído no artigo anterior, onde abordámos o Barcode Scanning, sem esquecer nada!

Impedimentos. Começamos bem...

Após muitas horas de suor, sangue e lágrimas, creio poder fazer estas declarações:

  • O Scandit SDK não permite que existam duas ou mais instâncias de DataCaptureContext para a mesma licença.
    • Isto seria ótimo para poder dedicar uma ao Barcode Scanning e outra Matrix Scan, mas não é possível.
  • Uma instância de DataCaptureContext não permite criar objetos para Barcode Scanning e Matrix Scan ao mesmo tempo.
  • Atualmente, na documentação da Scandit não existe nenhum exemplo de Xamarin Forms onde coexistam Barcode Scanning e Matrix Scan.

E agora? Ficamos com uma alternativa. Podemos fazer com que o nosso singleton ScanditManager funcione com ambos os modos, redefinindo o contexto com cada mudança de modo.

Implementação do Matrix Scan no ScanditManager

Precisamos de:

  • Uma enumeração para definir os dois modos Scandit.
  • Uma propriedade BarcodeTracking.
  • Uma propriedade BarcodeTrackingSettings.
  • Uma propriedade enumeração acima definida.
  • Um método para mudar de modo.

Ficamos com algo do género:

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;

    }

}

Criar um ViewModel base para o MatrixScan

Nada de novo, se te recordares do artigo anterior, mas desta vez são objetos do MatrixScan que propagamos em vez dos Barcode Scanning. Desta vez, temos de implementar duas interfaces em vez de uma: IbarcodeTrackingListener e IbarcodeTrackingBasicOverlayListener.

Ficamos com algo assim:

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

}

Criamos uma nova vista cujo ViewModel deve herdar do ViewModel base anterior. Criamos a sua ContentPage e definimos o seguinte no xaml:

<scanditCore:DataCaptureView

    DataCaptureContext="{Binding Context}">

 

    <scanditBarcode:BarcodeTrackingBasicOverlay

        BarcodeTracking="{Binding BarcodeTracking}"

        Listener="{Binding .}"/>

</scanditCore:DataCaptureView>

Não devemos esquecer de acrescentar a referência a scanditCore e scanditBarcode:

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

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

Já só nos falta escrever sobre OnMatrixScanScannedBarcodes e GetBrushFor e invocar os métodos para iniciar/parar a câmara e a leitura quando for necessário. Não o incluo aqui, mas é interessante fazer isto quando a aplicação passa para segundo plano ou volta para o primeiro plano. Não devemos esquecer de rubricar o serviço de dependência com a licença scandit antes de entrar na visualização e mudar o modo para Matrix Scan.

Conclusão

Vemos que, tal como no artigo anterior, o API da Scandit, por muito útil que seja a sua funcionalidade, é complicado de utilizar. Falta-me nesta API poder trabalhar com instâncias para interfaces em vez de classes, ou seja, esta API não tem inversão de dependência.

Somos forçados a propagar os objetos API para além do que deveria ser necessário para funcionalidades mais avançadas que não se encaixam e não são o foco deste artigo. Também nos obriga a inventar uma solução utilizando super classes abstratas para ter uma única implementação das interfaces necessárias através do padrão Listener, habitualmente não utilizado pelos programadores C# devido à disponibilidade de eventos.