I've recently had need to customize the UI overlay of the excellent and
free barcode scanning plugin Zxing. While gathering material for this post I discovered that an example is provided of
customizing the overlay via XML, but when I initially searched the first example I came across was the C# method - so that's the route I took. The C# version did not have the ability to turn the flash on/off which was a feature I needed to access, so I figured out how to add that while styling. I'm going to go over how I modified the
example class created by Redth to help anyone else who might attempt the same thing for their own Android app.
For starters, you need to modify your call to Zxing to specify the use of a custom overlay:
var scanner = new ZXing.MobileMobileBarcodeScanner();
scanner.UseCustomOverlay = true;
myCustomOverlayInstance = new ZxingOverlayView(this, scanner);
scanner.CustomOverlay = myCustomOverlayInstance;
scanner.Scan().ContinueWith(t => {
After that, you just need to customize the properties and methods of the ZxingOverlayView example class file. In my example I added the ability to control the torch (flashlight) and display bitamaps so that I could have my own custom buttons. I specified a default color and a pressed color, along with boolean flags so I could change the buttons to confirm to the user that they had pressed a button while waiting for a response:
namespace MyProject
{
public class ZxingOverlayView : View
{
private Paint defaultPaint;
private Paint pressedPaint;
private Android.Graphics.Bitmap resultBitmap;
private Android.Graphics.Bitmap litTorchIcon;
private Android.Graphics.Bitmap unlitTorchIcon;
private Rect torchIconDimRect;
private bool hasTorch = false;
private bool torchOn = false;
private bool cancelPressed = false;
private bool problemPressed = false;
...
public ZxingOverlayView(Context context, MobileBarcodeScanner scanner) : base(context)
{
this.context = context;
this.scanner = scanner;
SetDisplayValues ();
}
...
private void SetDisplayValues ()
{
hasTorch = this.Context.PackageManager.HasSystemFeature (PackageManager.FeatureCameraFlash);
if (hasTorch) {
var metrics = Resources.DisplayMetrics;
int iconHeight = metrics.HeightPixels * 2 / 9;
int iconWidth = iconHeight * 7 / 10;
torchIconDimRect = new Rect (0, 0, iconWidth, iconHeight);
litTorchIcon = ImageHelper.DecodeSampledBitmapFromResource (Resources, Resource.Drawable.bulb_lit, iconWidth, iconHeight);
unlitTorchIcon = ImageHelper.DecodeSampledBitmapFromResource (Resources, Resource.Drawable.bulb_unlit, iconWidth, iconHeight);
}
defaultPaint = new Paint (PaintFlags.AntiAlias);
pressedPaint = new Paint (PaintFlags.AntiAlias);
pressedPaint.Color = new Color (96, 97, 104);
buttonFontColor = Color.White;
For each of my buttons I had to specify a rectangle on the screen. It's best if you specify not in static pixels, but in percentage of the total screen available. This will ensure your overlay renders the same on any size of screen. I created three buttons, plus a rectangle to hold my torch icon bitmap. Notice that the
TorchIconDimRect was created as part of
SetDisplayValues() above as we decoded and resized the bitmap icon for it at the same time:
Rect GetProblemButtonRect()
{
var metrics = Resources.DisplayMetrics;
int width = metrics.WidthPixels * 7 / 16 ;
int height = metrics.HeightPixels * 2 / 9;
int leftOffset = metrics.WidthPixels * 33 / 64;
int topOffset = metrics.HeightPixels * 3 / 4;
var problemRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
return problemRect;
}
Rect GetCancelButtonRect()
{
var metrics = Resources.DisplayMetrics;
int width = metrics.WidthPixels * 7 / 16 ;
int height = metrics.HeightPixels * 2 / 9;
int leftOffset = metrics.WidthPixels / 22;
int topOffset = metrics.HeightPixels * 3 / 4;
var cancelRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
return cancelRect;
}
Rect GetTorchIconDimRect()
{
return torchIconDimRect;
}
Rect GetTorchIconRect()
{
var metrics = Resources.DisplayMetrics;
int height = metrics.HeightPixels / 8;
int width = height * 7 / 10;
int leftOffset = metrics.WidthPixels / 2 - ( width / 2);
int topOffset = metrics.HeightPixels /18;
var torchRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
return torchRect;
}
Rect GetTorchButtonRect()
{
var metrics = Resources.DisplayMetrics;
int width = metrics.WidthPixels * 3 / 8 ;
int height = metrics.HeightPixels * 3 / 16;
int leftOffset = metrics.WidthPixels / 2 - (width / 2);
int topOffset = metrics.HeightPixels / 64;
var torchRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
return torchRect;
}
Here is the
ImageHelper class file which you will only need if you are using bitmaps on your overlay. This helper class will resize your bitmap to the desired height to prevent image distortion:
internal static class ImageHelper
{
public static int CalculateInSampleSize(BitmapFactoryOptions options, int reqWidth, int reqHeight)
{
var height = (float)options.OutHeight;
var width = (float)options.OutWidth;
var inSampleSize = 1D;
if (height > reqHeight || width > reqWidth)
{
inSampleSize = width > height
? height/reqHeight
: width/reqWidth;
}
return (int) inSampleSize;
}
public static Bitmap DecodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
{
var options = new BitmapFactoryOptions { InJustDecodeBounds = true };
BitmapFactory.DecodeResource(res, resId, options);
options.InSampleSize = CalculateInSampleSize(options, reqWidth, reqHeight);
options.InJustDecodeBounds = false;
Bitmap optimalSize = BitmapFactory.DecodeResource (res, resId, options);
return Bitmap.CreateScaledBitmap (optimalSize, reqWidth, reqHeight, false);
}
}
So now that all the colors, shapes and bitmaps have been defined you just need to draw them to the screen. This is done through the
OnDraw() method:
protected override void OnDraw (Canvas canvas)
{
var scale = Resources.DisplayMetrics.Density;
var frame = GetFramingRect();
if (frame == null)
return;
var probBtn = GetProblemButtonRect();
var cancelBtn = GetCancelButtonRect();
var torchBtn = GetTorchButtonRect();
var width = canvas.Width;
var height = canvas.Height;
var textPaint = new TextPaint();
textPaint.Color = buttonFontColor;
textPaint.AntiAlias = true;
textPaint.BgColor = Color.Gray;
textPaint.TextSize = 16 * scale;
defaultPaint.Color = resultBitmap != null ? resultColor : maskColor;
defaultPaint.Alpha = 245;
canvas.DrawRect(0, 0, width, frame.Top, defaultPaint);
canvas.DrawRect(0, frame.Bottom + 1, width, height, defaultPaint);
defaultPaint.Color = buttonFontColor;
defaultPaint.Alpha = 255;
pressedPaint.Color = Color.Black;
canvas.DrawRect (probBtn.Left+1, probBtn.Top +1, probBtn.Right + 1, probBtn.Bottom + 1, problemPressed ? pressedPaint : defaultPaint);
canvas.DrawRect (cancelBtn.Left+1, cancelBtn.Top +1, cancelBtn.Right + 1, cancelBtn.Bottom + 1, cancelPressed ? pressedPaint : defaultPaint);
if (hasTorch)
canvas.DrawRect (torchBtn.Left + 1, torchBtn.Top + 1, torchBtn.Right + 1, torchBtn.Bottom + 1, torchOn ? pressedPaint : defaultPaint);
defaultPaint.Color = buttonColor;
defaultPaint.Alpha = 255;
pressedPaint.Color = new Color(96, 97, 104);
canvas.DrawRect (probBtn, problemPressed ? pressedPaint : defaultPaint);
canvas.DrawRect (cancelBtn, cancelPressed ? pressedPaint : defaultPaint);
if (hasTorch)
{
canvas.DrawRect (torchBtn, torchOn ? pressedPaint : defaultPaint);
canvas.DrawBitmap ((torchOn ? litTorchIcon : unlitTorchIcon), GetTorchIconDimRect(), GetTorchIconRect(), defaultPaint);
}
var btnText = new StaticLayout("Scan problems?", textPaint, probBtn.Width(), Android.Text.Layout.Alignment.AlignCenter, 1.0f, 0.0f, false);
canvas.Save();
canvas.Translate(probBtn.Left, probBtn.Top + (probBtn.Height ()/3)+ (btnText.Height / 2));
btnText.Draw(canvas);
canvas.Restore();
btnText = new StaticLayout("Cancel Scan", textPaint, cancelBtn.Width(), Android.Text.Layout.Alignment.AlignCenter, 1.0f, 0.0f, false);
canvas.Save();
canvas.Translate(cancelBtn.Left, cancelBtn.Top + (cancelBtn.Height ()/3) + (btnText.Height / 2));
btnText.Draw(canvas);
canvas.Restore();
If your buttons are pressed, you can capture that event by overriding the
OnTouchEvent method. Notice that in each button touch event I call
this.Invalidate(). This call causes the screen to refresh and without it the
OnDraw() method would not be invoked, and the screen would not reflect our buttons change in display when pressed.
public override bool OnTouchEvent(MotionEvent me)
{
if (me.Action == MotionEventActions.Down)
{
if (GetCancelButtonRect().Contains((int)me.RawX, (int)me.RawY))
{
cancelPressed = true;
this.Invalidate();
OnUnload();
scanner.Cancel();
}
else if (GetTorchButtonRect().Contains ((int)me.RawX, (int)me.RawY))
{
scanner.ToggleTorch();
torchOn = !torchOn;
this.Invalidate();
}
else if (GetProblemButtonRect().Contains ((int)me.RawX, (int)me.RawY))
{
problemPressed = true;
this.Invalidate();
if (parentActivity == null)
{
Intent intent = new Intent();
intent.SetClass(context, typeof(ManualEntryActivity));
context.StartActivity(intent);
}
else
{
parentActivity.GetManualEntry();
}
OnUnload();
scanner.Cancel();
}
return true;
}
else
return false;
}
Lastly, I read on a
stackoverflow post that graphics and bitmaps are two objects which you must take care to properly dispose of when they are no longer needed If you don't properly dispose of the bitmaps created for your custom overlay then there is a potential for a memory leak. I dispose of my bitmaps by calling this
OnUnload() method that I use at all points of exit:
private void OnUnload()
{
if (hasTorch)
{
litTorchIcon.Dispose();
unlitTorchIcon.Dispose();
}
}
This is how my custom overlay turned out. (Ignore the black & white checkerboard with green square which is just a test display for the android emulator's camera)
Hopefully my post helped you. Feel free to post questions if you are having problems, or need an explanation on any of my code.
No comments:
Post a Comment