10

I am writing a custom overlay for a ZXing's QR reader. Here is the working example:

public partial class CustomScanPage : ContentPage
{
    ZXingScannerView zxing;
    private List<BoxView> _boxes;


    public CustomScanPage() : base()
    {
        zxing = new ZXingScannerView
        {
            HorizontalOptions = LayoutOptions.FillAndExpand,
            VerticalOptions = LayoutOptions.FillAndExpand
        };
        zxing.OnScanResult += (result) =>
            Device.BeginInvokeOnMainThread(async () =>
            {
                zxing.IsAnalyzing = false;

                SetBoxesColor(Color.FromHex("#76ff03"));
                await Task.Delay(2000);

                await DisplayAlert("Scanned Barcode", result.Text, "OK");

                SetBoxesColor(Color.White);
            });
        zxing.Options = new MobileBarcodeScanningOptions {
            PossibleFormats = new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE }
        };
        var overlay = BuildGrid();

        var grid = new Grid
        {
            VerticalOptions = LayoutOptions.FillAndExpand,
            HorizontalOptions = LayoutOptions.FillAndExpand,
        };
        grid.Children.Add(zxing);
        grid.Children.Add(overlay);

        Content = grid;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        zxing.IsScanning = true;
    }

    protected override void OnDisappearing()
    {
        zxing.IsScanning = false;

        base.OnDisappearing();
    }

    private AbsoluteLayout BuildGrid()
    {
        var al = new AbsoluteLayout();
        var mask = new BoxView
        {
            HorizontalOptions = LayoutOptions.Fill,
            VerticalOptions = LayoutOptions.Fill,
            BackgroundColor = Color.Transparent
        };

        var maskSide = 196;

        var yBegin = Math.Round((App.ScreenHeight - maskSide) / 2);
        var xBegin = Math.Round((App.ScreenWidth - maskSide) / 2);
        var barLong = 40;
        var barShort = 4;
        var barColor = Color.White;

        var grid = new Grid { ColumnSpacing = 0, RowSpacing = 0 };
        grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(yBegin, GridUnitType.Absolute) });
        grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(maskSide, GridUnitType.Absolute) });
        grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(App.ScreenHeight - yBegin, GridUnitType.Absolute) });
        grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(xBegin, GridUnitType.Absolute) });
        grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(maskSide, GridUnitType.Absolute) });
        grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(App.ScreenWidth - xBegin, GridUnitType.Absolute) });

        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232"), }, 0, 0);
        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 0, 1);
        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 0, 2);

        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 1, 0);
        grid.Children.Add(mask, 1, 1);
        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 1, 2);

        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 2, 0);
        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 2, 1);
        grid.Children.Add(new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = Color.FromHex("#80323232") }, 2, 2);

        grid.HorizontalOptions = LayoutOptions.FillAndExpand;
        grid.VerticalOptions = LayoutOptions.FillAndExpand;

        AbsoluteLayout.SetLayoutBounds(grid, new Rectangle(0, 0, 1, 1));
        AbsoluteLayout.SetLayoutFlags(grid, AbsoluteLayoutFlags.All);

        al.Children.Add(grid);

        var b1 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b1, new Rectangle(xBegin - barShort, yBegin, barShort, barLong - barShort));
        AbsoluteLayout.SetLayoutFlags(b1, AbsoluteLayoutFlags.None);

        var b2 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b2, new Rectangle(xBegin - barShort, yBegin - barShort, barLong, barShort));
        AbsoluteLayout.SetLayoutFlags(b2, AbsoluteLayoutFlags.None);

        var b3 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b3, new Rectangle(xBegin + maskSide, yBegin, barShort, barLong - barShort));
        AbsoluteLayout.SetLayoutFlags(b3, AbsoluteLayoutFlags.None);

        var b4 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b4, new Rectangle(xBegin - barLong + maskSide + barShort, yBegin - barShort, barLong, barShort));
        AbsoluteLayout.SetLayoutFlags(b4, AbsoluteLayoutFlags.None);

        var b5 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b5, new Rectangle(xBegin - barShort, yBegin + maskSide + barShort - barLong, barShort, barLong - barShort));
        AbsoluteLayout.SetLayoutFlags(b5, AbsoluteLayoutFlags.None);

        var b6 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b6, new Rectangle(xBegin - barShort, yBegin + maskSide, barLong, barShort));
        AbsoluteLayout.SetLayoutFlags(b6, AbsoluteLayoutFlags.None);

        var b7 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b7, new Rectangle(xBegin + maskSide - barLong + barShort, yBegin + maskSide, barLong, barShort));
        AbsoluteLayout.SetLayoutFlags(b7, AbsoluteLayoutFlags.None);

        var b8 = new BoxView { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, BackgroundColor = barColor };

        AbsoluteLayout.SetLayoutBounds(b8, new Rectangle(xBegin + maskSide, yBegin + maskSide - barLong + barShort, barShort, barLong - barShort));
        AbsoluteLayout.SetLayoutFlags(b8, AbsoluteLayoutFlags.None);

        al.Children.Add(b1);
        al.Children.Add(b2);
        al.Children.Add(b3);
        al.Children.Add(b4);
        al.Children.Add(b5);
        al.Children.Add(b6);
        al.Children.Add(b7);
        al.Children.Add(b8);

        _boxes = new List<BoxView> { b1, b2, b3, b4, b5, b6, b7, b8 };

        return al;
    }

    private void SetBoxesColor(Color c)
    {
        foreach (var box in _boxes)
        {
            box.Color = c;
        }
    }
}

As you can see, there is nothing complicated. But the problem is, how this page is rendered. On Xamarin Android Player devices (Nexus 4 KitKat HD and Nexus 5 Lollipop Full HD) everything looks as expected:

enter image description here

However when I run the same application (without pink background on the grid) on my Nexus 5x device (Marshmallow, Full HD), as you can see, pixels aren't precise, there is a little mismatch on the upper right green shape, a little offset on the left shapes and there is a one-pixel transparent gap on the grid row:

enter image description here

The fact that the code is rendered correctly on two devices makes me believe that my point arithmetic is correct. What could be the issue? And more importantly, what can I do to resolve it?

EDIT:

Maybe somehow the reason is that my device's pt/px ratio is different than emulator devices' ratio?

EDIT:

@Cheesebaron's answer was wrong. Here's what I tried to do, which didn't help at all: I created CustomBoxView, CustomBoxViewNative, CustomBoxViewRenderer and used CustomBoxView instead of BoxView. It didn' work.

CustomBoxView:

public class CustomBoxView : View
{
    public static readonly BindableProperty ColorProperty = BindableProperty.Create("Color", typeof (string),
        typeof (CustomBoxView), "#FF0000");

    public string Color
    {
        get { return (string) GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }
}

CustomBoxViewNative:

public class CustomBoxViewNative : View
{
    private Canvas _canvas;
    private Rect _rect;

    public CustomBoxViewNative(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
    {
    }

    public CustomBoxViewNative(Context context) : base(context)
    {
    }

    public CustomBoxViewNative(Context context, IAttributeSet attrs) : base(context, attrs)
    {
    }

    public CustomBoxViewNative(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
    {
    }

    public CustomBoxViewNative(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
    {
    }

    protected override void OnDraw(Canvas canvas)
    {
        var paintCircle = new Paint { Color = Color.White };
        paintCircle.StrokeWidth = Width*Resources.DisplayMetrics.Density;
        _rect = new Rect(0, 0, Width, Height);
        _canvas = canvas;
        _canvas.DrawRect(_rect, paintCircle);
    }
}

CustomBoxViewRenderer:

public class CustomBoxViewRenderer : ViewRenderer<CustomBoxView, CustomBoxViewNative>
{
    protected override void OnElementChanged(ElementChangedEventArgs<CustomBoxView> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement != null || this.Element == null)
            return;

        var nativeControl = new CustomBoxViewNative(Forms.Context);

        SetNativeControl(nativeControl);
    }
}
nicks
  • 2,161
  • 8
  • 49
  • 101
  • Do not trust emulators, try running it on multiple devices to judge. – Rohit Vipin Mathews Aug 18 '16 at 13:47
  • It is a subpixel rendering problem. You need to factor in the screen density for your stroke widths. – Cheesebaron Aug 18 '16 at 13:47
  • @Cheesebaron could you be more specific? maybe provide some resources – nicks Aug 18 '16 at 13:48
  • (I do not have knowledge of Xamarin) problem seems related to different density and size calculation of rect, stroke etc. Round off of float fraction making one pixel gap. To solve this problem use dp values where possible. For some specific cases you can improve calculation like in place of x/y use x+y-1/y etc to get higher value and prevent loss of 1 pixel. – Ramit Aug 26 '16 at 07:11

1 Answers1

5

As written in the comments you do not factor in the display density in your calculations. Hence, it might be a bit off on some devices when you render your lines.

I know Xamarin Forms Labs have a Display class where you can get the density of your screen. If you want to get it yourself through a dependency service you can do something like this:

public interface IDisplayInfo
{
    float Density { get; }
}

On Android:

public class DisplayInfo : IDisplayInfo
{
    public float Density => Application.Context.Resources.DisplayMetrics.Density;
}

On iOS:

public class DisplayInfo : IDisplayInfo
{
    public float Density => UIScreen.MainScreen.Scale;
}

Register your dependency service on each platform:

[assembly: Xamarin.Forms.Dependency (typeof (DisplayInfo))]

Then when you need to use it:

var display = DependencyService.Get<IDisplayInfo>();

var density = display.Density;

Then you just multiply your stroke widths with the density.

There is a small example here: https://stackoverflow.com/a/14405451/368379

Community
  • 1
  • 1
Cheesebaron
  • 24,131
  • 15
  • 66
  • 118
  • your code return the density equal to `3`. does that mean that i should multiply maskSide which is equal to 196 and use the resulting value? this will mess up my sizing. maybe you're saying that it should be the _multiple_ of 3? – nicks Aug 18 '16 at 14:04
  • please, could you elaborate? i don't understand what needs to be done. – nicks Aug 19 '16 at 10:25
  • what does it have to do with strokes anyway? i'm not using any at all – nicks Aug 19 '16 at 10:58
  • I'll have a look later and see if I can come up with something for you :) – Cheesebaron Aug 22 '16 at 12:32
  • @NikaGamkrelidze, regarding _"...what does it have to do with strokes anyway?"_ that means your **lines**!! I'm not familiar with Android code enough to show you, but whatever part of your code that sets green line/box **pixel width** that must be **multiplied by density**. Example like.. `GreenBox.width = myWidth * Density);` Try it and adjust since you can see result, I cannot. Your suspicion is correct though _"...my device's pt/px ratio is different than emulator devices' ratio?"_ and the solution is accounting for density (which itself is affected by display resolution) – VC.One Aug 27 '16 at 11:08