在 Android 上自訂 Zxing 掃描框樣式與大小位置

前言

Zxing 是知名的條碼辨識函式庫,可以整合在 Android、iOS......等平台的 App 上。
網路上已經搜尋的到許多使用教學,不過在深度客製化(如自訂 layout)上,多半都是用修改原始碼的方式去實現。直接修改原始碼固然方便快速,但缺點就是當有兩種以上客製需求時,必須撰寫額外的判斷式去處理。以模組化的概念來看,這樣的作法不是很漂亮。
因此,本篇文章著重在運用繼承和撰寫 layout XML 檔的方式實作客製化。

自訂掃描介面佈局

Zxing 最簡單的使用方式是利用 IntentIntegrator 物件去呼叫掃描器並接收回傳的掃描結果,而缺點也顯而易見的是無法客製化整個介面佈局。
基本的客製化方式是自行在 res/layout 目錄下建立 XML 檔,然後加入 <com.journeyapps.barcodescanner.DecoratedBarcodeView> 元件。如此就可以讓掃描畫面呈現在 App 畫面的某個區塊,而不用佔滿整個畫面。

<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.journeyapps.barcodescanner.DecoratedBarcodeView
android:id="@+id/barcodeView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:zxing_scanner_layout="@layout/custom_barcode_view"
app:zxing_framing_rect_width="@dimen/barcode_scanner_frame_width"
app:zxing_framing_rect_height="@dimen/barcode_scanner_frame_width" />
...
</android.support.constraint.ConstraintLayout>
其中有些屬性可以再進一步客製化 DecoratedBarcodeView 本身的外觀樣式,常用的有:

  • app:zxing_framing_rect_width=掃描框的寬度
  • app:zxing_framing_rect_height=掃描框的高度
  • app:zxing_scanner_layout=套用自訂的 layout

而 app:zxing_scanner_layout 套用的 layout 中必須要包含繼承自 BarcodePreview 和 ViewfinderView 的兩個元件才能構成一個完整的掃描器。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.zxing.CustomBarcodePreview
android:id="@+id/zxing_barcode_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.zxing.CustomViewfinderView
android:id="@+id/zxing_viewfinder_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

客製化掃描框樣式

ViewfinderView 負責描繪掃描畫面中的掃描框,我們可以繼承並覆寫它的 onDraw() 方法。
下面範例的頂端幾個變數和 onDraw() 方法中的 [Custom start/end] 之間的程式碼是我所增加的部份,其他是直接從原始碼複製過來的。

public class CustomViewfinderView extends ViewfinderView {
// Length rate of line and frame
private float mLineRate = 0.2f;
// Line depth
private float mLineDepth =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
// Line color
private int mLineColor = Color.rgb(244, 165, 37);
@Override
public void onDraw(Canvas canvas) {
refreshSizes();
if (framingRect == null || previewFramingRect == null) {
return;
}
final Rect frame = framingRect;
final Rect previewFrame = previewFramingRect;
final int width = canvas.getWidth();
final int height = canvas.getHeight();
// [Custom start] Draw 4 corner lines
paint.setColor(mLineColor);
canvas.drawRect(
frame.left,
frame.top,
frame.left + frame.width() * mLineRate,
frame.top + mLineDepth,
paint);
canvas.drawRect(
frame.left,
frame.top,
frame.left + mLineDepth,
frame.top + frame.height() * mLineRate,
paint);
canvas.drawRect(
frame.right - frame.width() * mLineRate,
frame.top,
frame.right,
frame.top + mLineDepth,
paint);
canvas.drawRect(
frame.right - mLineDepth,
frame.top,
frame.right,
frame.top + frame.height() * mLineRate,
paint);
canvas.drawRect(
frame.left,
frame.bottom - mLineDepth,
frame.left + frame.width() * mLineRate,
frame.bottom,
paint);
canvas.drawRect(
frame.left,
frame.bottom - frame.height() * mLineRate,
frame.left + mLineDepth,
frame.bottom,
paint);
canvas.drawRect(
frame.right - frame.width() * mLineRate,
frame.bottom - mLineDepth,
frame.right,
frame.bottom,
paint);
canvas.drawRect(
frame.right - mLineDepth,
frame.bottom - frame.height() * mLineRate,
frame.right,
frame.bottom,
paint);
// [Custom end]
// Draw the exterior (i.e. outside the framing rect) darkened
paint.setColor(resultBitmap != null ? resultColor : maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
canvas.drawRect(0, frame.bottom + 1, width, height, paint);
if (resultBitmap != null) {
// Draw the opaque result bitmap over the scanning rectangle
paint.setAlpha(CURRENT_POINT_OPACITY);
canvas.drawBitmap(resultBitmap, null, frame, paint);
} else {
// Draw a red "laser scanner" line through the middle to show decoding is active
paint.setColor(laserColor);
paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
final int middle = frame.height() / 2 + frame.top;
canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1, middle + 2, paint);
final float scaleX = frame.width() / (float) previewFrame.width();
final float scaleY = frame.height() / (float) previewFrame.height();
final int frameLeft = frame.left;
final int frameTop = frame.top;
// draw the last possible result points
if (!lastPossibleResultPoints.isEmpty()) {
paint.setAlpha(CURRENT_POINT_OPACITY / 2);
paint.setColor(resultPointColor);
float radius = POINT_SIZE / 2.0f;
for (final ResultPoint point : lastPossibleResultPoints) {
canvas.drawCircle(
frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
radius, paint
);
}
lastPossibleResultPoints.clear();
}
// draw current possible result points
if (!possibleResultPoints.isEmpty()) {
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setColor(resultPointColor);
for (final ResultPoint point : possibleResultPoints) {
canvas.drawCircle(
frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
POINT_SIZE, paint
);
}
// swap and clear buffers
final List<ResultPoint> temp = possibleResultPoints;
possibleResultPoints = lastPossibleResultPoints;
lastPossibleResultPoints = temp;
possibleResultPoints.clear();
}
// Request another update at the animation interval, but only repaint the laser line,
// not the entire viewfinder mask.
postInvalidateDelayed(ANIMATION_DELAY,
frame.left - POINT_SIZE,
frame.top - POINT_SIZE,
frame.right + POINT_SIZE,
frame.bottom + POINT_SIZE);
}
}
}
下圖是客製化的結果,在掃描框四個角落加上了橘色邊邊。


客製化掃描框位置

BarcodePreview 負責顯示相機拍到的畫面以及定義解析區域在畫面上的範圍大小(前面提到的 ViewfinderView 只是像濾鏡一樣覆蓋在 BarcodePreview 之上,所謂的掃描框也只是剛好跟這邊定義的解析區域重疊而已)。我們可以繼承並覆寫它的 calculateFramingRect() 方法。
下面範例示範如何修改掃描框位置,一般預設是正中間。

public class CustomBarcodePreview extends BarcodeView {
...
@Override
protected Rect calculateFramingRect(Rect container, Rect surface) {
// intersection is the part of the container that is used for the preview
Rect intersection = new Rect(container);
boolean intersects = intersection.intersect(surface);
// [Custom] Get private variables by getter
Size framingRectSize = getFramingRectSize();
double marginFraction = getMarginFraction();
if(framingRectSize != null) {
// Specific size is specified. Make sure it's not larger than the container or surface.
int horizontalMargin = Math.max(0, (intersection.width() - framingRectSize.width) / 2);
int verticalMargin = Math.max(0, (intersection.height() - framingRectSize.height) / 2);
intersection.inset(horizontalMargin, verticalMargin);
// [Custom] Move down the framing rectangle
intersection.offset(0, 120);
return intersection;
}
// margin as 10% (default) of the smaller of width, height
int margin = (int)Math.min(intersection.width() * marginFraction, intersection.height() * marginFraction);
intersection.inset(margin, margin);
if (intersection.height() > intersection.width()) {
// We don't want a frame that is taller than wide.
intersection.inset(0, (intersection.height() - intersection.width()) / 2);
}
return intersection;
}
}

其他

研究中......敬請期待!
看官如有任何想法也歡迎討論~

留言

張貼留言

這個網誌中的熱門文章

Android 藍牙連接通訊實作心得