在 Android 上自訂 Zxing 掃描框樣式與大小位置
前言
Zxing 是知名的條碼辨識函式庫,可以整合在 Android、iOS......等平台的 App 上。網路上已經搜尋的到許多使用教學,不過在深度客製化(如自訂 layout)上,多半都是用修改原始碼的方式去實現。直接修改原始碼固然方便快速,但缺點就是當有兩種以上客製需求時,必須撰寫額外的判斷式去處理。以模組化的概念來看,這樣的作法不是很漂亮。
因此,本篇文章著重在運用繼承和撰寫 layout XML 檔的方式實作客製化。
自訂掃描介面佈局
Zxing 最簡單的使用方式是利用 IntentIntegrator 物件去呼叫掃描器並接收回傳的掃描結果,而缺點也顯而易見的是無法客製化整個介面佈局。基本的客製化方式是自行在 res/layout 目錄下建立 XML 檔,然後加入 <com.journeyapps.barcodescanner.DecoratedBarcodeView> 元件。如此就可以讓掃描畫面呈現在 App 畫面的某個區塊,而不用佔滿整個畫面。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
- app:zxing_framing_rect_width=掃描框的寬度
- app:zxing_framing_rect_height=掃描框的高度
- app:zxing_scanner_layout=套用自訂的 layout
而 app:zxing_scanner_layout 套用的 layout 中必須要包含繼承自 BarcodePreview 和 ViewfinderView 的兩個元件才能構成一個完整的掃描器。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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] 之間的程式碼是我所增加的部份,其他是直接從原始碼複製過來的。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() 方法。下面範例示範如何修改掃描框位置,一般預設是正中間。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
其他
研究中......敬請期待!看官如有任何想法也歡迎討論~
作者已經移除這則留言。
回覆刪除作者已經移除這則留言。
回覆刪除