Nguyên tác: http://blog.csdn.net/xiaanming/article/details/10163203
Khi WeChat ra mắt tính năng quét mã QR, mình cảm thấy rất thú vị. Việc chỉ cần quét một hình ảnh để thêm bạn bè thật sự khiến mình không thể tin được. Lúc đó mình chưa hiểu rõ về mã QR, nhưng sau này khi làm dự án, quản lý yêu cầu tích hợp tính năng quét mã QR, mình đã tìm kiếm trên Google và tìm thấy nhiều hướng dẫn từ cộng đồng. Dựa vào những bài viết đó, mình đã xây dựng thành công chức năng quét mã QR trong dự án của mình.
Từ khi WeChat xuất hiện, mã QR ngày càng phổ biến và có mặt khắp nơi như cửa hàng, nhà hàng, KFC... Để thực hiện chức năng quét mã QR, chúng ta sử dụng thư viện mã nguồn mở Zxing của Google. Bạn có thể tải mã nguồn và file Jar tại http://code.google.com/p/zxing/. Trong dự án trước đây, mình chỉ thực hiện chức năng quét mà không chú trọng đến giao diện người dùng, điều này khiến giao diện trông rất kém. Một ứng dụng tốt không chỉ cần chức năng tốt mà còn phải có giao diện thân thiện. Vì vậy, mình đã bắt chước giao diện quét mã QR của WeChat, dù không hoàn hảo như WeChat nhưng vẫn đạt được hiệu quả mong muốn. Dưới đây là mã nguồn đã chỉnh sửa để chia sẻ với mọi người.
Cấu trúc dự án
- Nếu bạn muốn thêm tính năng này vào dự án của mình, hãy sao chép ba package sau:
com.mining.app.zxing.camera,com.mining.app.zxing.decoding,com.mining.app.zxing.viewvào dự án của bạn, đồng thời thêm các tài nguyên tương ứng. Ngoài ra, bạn cũng cần thêm tệp jar của Zxing. - Trong package
com.example.qr_codescancó một lớpMipcaActivityCaptuređược lấy trực tiếp từ dự án trước. Lớp này xử lý giao diện quét, ví dụ như âm thanh và rung khi quét thành công. Phương thức chính cần chú ý làhandleDecode(Result result, Bitmap barcode). Sau khi quét xong, kết quả và ảnh mã QR sẽ được truyền vào phương thức này để xử lý. Mình xử lý hiển thị kết quả và ảnh chụp.
/**
* Xử lý kết quả quét
* @param result
* @param barcode
*/
public void handleDecode(Result result, Bitmap barcode) {
inactivityTimer.onActivity();
playBeepSoundAndVibrate();
String resultString = result.getText();
if (resultString.equals("")) {
Toast.makeText(MipcaActivityCapture.this, "Quét thất bại!", Toast.LENGTH_SHORT).show();
}else {
Intent resultIntent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("result", resultString);
bundle.putParcelable("bitmap", barcode);
resultIntent.putExtras(bundle);
this.setResult(RESULT_OK, resultIntent);
}
MipcaActivityCapture.this.finish();
}
Chỉnh sửa bố cục giao diện
Mình đã thay đổi bố cục của MipcaActivityCapture, sử dụng FrameLayout chứa RelativeLayout. Hình ảnh được lấy từ WeChat, vì mình không có thiết kế đồ họa nên thường tìm hình ảnh ở đây.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<SurfaceView
android:id="@+id/preview_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center" />
<com.mining.app.zxing.view.ViewfinderView
android:id="@+id/viewfinder_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<include
android:id="@+id/include1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
layout="@layout/activity_title" />
</RelativeLayout>
</FrameLayout>
Bố cục phần đầu trang được đặt trong một file riêng và được nhúng vào bằng include. Vì activity_title được sử dụng bởi nhiều activity khác trong dự án, mình cũng sao chép trực tiếp từ dự án cũ.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/mmtitle_bg_alpha" >
<Button
android:id="@+id/button_back"
android:layout_width="75.0dip"
android:text="Quay lại"
android:background="@drawable/mm_title_back_btn"
android:textColor="@android:color/white"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="2dip" />
<TextView
android:id="@+id/textview_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/button_back"
android:layout_alignBottom="@+id/button_back"
android:layout_centerHorizontal="true"
android:gravity="center_vertical"
android:text="Quét mã QR"
android:textColor="@android:color/white"
android:textSize="18sp" />
</RelativeLayout>
Giao diện chính MainActivity
Trong demo này có một giao diện chính MainActivity với một nút bấm, một hình ảnh và một văn bản. Khi nhấn nút, chuyển sang màn hình quét mã QR. Sau khi quét xong, quay lại màn hình chính và hiển thị kết quả vào văn bản, ảnh vào hình ảnh. Bố cục đơn giản như sau:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffe1e0de" >
<Button
android:id="@+id/button1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="Quét mã QR" />
<TextView
android:id="@+id/result"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/button1"
android:lines="2"
android:gravity="center_horizontal"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:id="@+id/qrcode_bitmap"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true"
android:layout_below="@+id/result"/>
</RelativeLayout>
Mã nguồn của MainActivity:
package com.example.qr_codescan;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
public class MainActivity extends Activity {
private final static int SCANNIN_GREQUEST_CODE = 1;
/**
* Hiển thị kết quả quét
*/
private TextView mTextView ;
/**
* Hiển thị ảnh chụp từ quét
*/
private ImageView mImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.result);
mImageView = (ImageView) findViewById(R.id.qrcode_bitmap);
// Nhấn nút để chuyển sang màn hình quét mã QR
Button mButton = (Button) findViewById(R.id.button1);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setClass(MainActivity.this, MipcaActivityCapture.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivityForResult(intent, SCANNIN_GREQUEST_CODE);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case SCANNIN_GREQUEST_CODE:
if(resultCode == RESULT_OK){
Bundle bundle = data.getExtras();
// Hiển thị nội dung đã quét
mTextView.setText(bundle.getString("result"));
// Hiển thị ảnh
mImageView.setImageBitmap((Bitmap) data.getParcelableExtra("bitmap"));
}
break;
}
}
}
Thiết kế khung quét
Mã nguồn phía trên khá đơn giản, nhưng để tạo hiệu ứng giống WeChat, bạn cần sửa lớp ViewfinderView trong package com.mining.app.zxing.view. Trong WeChat, khung quét được vẽ bằng hình ảnh, còn mình vẽ thủ công. Mã nguồn có chú thích rõ ràng, bạn chỉ cần đọc là hiểu. Nếu muốn thay đổi kích thước khung quét, hãy chỉnh sửa trong lớp CameraManager.
/*
* Copyright (C) 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mining.app.zxing.view;
import java.util.Collection;
import java.util.HashSet;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
import com.example.qr_codescan.R;
import com.google.zxing.ResultPoint;
import com.mining.app.zxing.camera.CameraManager;
public final class ViewfinderView extends View {
private static final String TAG = "log";
private static final long ANIMATION_DELAY = 10L;
private static final int OPAQUE = 0xFF;
private int ScreenRate;
private static final int CORNER_WIDTH = 10;
private static final int MIDDLE_LINE_WIDTH = 6;
private static final int MIDDLE_LINE_PADDING = 5;
private static final int SPEEN_DISTANCE = 5;
private static float density;
private static final int TEXT_SIZE = 16;
private static final int TEXT_PADDING_TOP = 30;
private Paint paint;
private int slideTop;
private int slideBottom;
private Bitmap resultBitmap;
private final int maskColor;
private final int resultColor;
private final int resultPointColor;
private Collection<ResultPoint> possibleResultPoints;
private Collection<ResultPoint> lastPossibleResultPoints;
boolean isFirst;
public ViewfinderView(Context context, AttributeSet attrs) {
super(context, attrs);
density = context.getResources().getDisplayMetrics().density;
ScreenRate = (int)(20 * density);
paint = new Paint();
Resources resources = getResources();
maskColor = resources.getColor(R.color.viewfinder_mask);
resultColor = resources.getColor(R.color.result_view);
resultPointColor = resources.getColor(R.color.possible_result_points);
possibleResultPoints = new HashSet<ResultPoint>(5);
}
@Override
public void onDraw(Canvas canvas) {
Rect frame = CameraManager.get().getFramingRect();
if (frame == null) {
return;
}
if(!isFirst){
isFirst = true;
slideTop = frame.top;
slideBottom = frame.bottom;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
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) {
paint.setAlpha(OPAQUE);
canvas.drawBitmap(resultBitmap, frame.left, frame.top, paint);
} else {
paint.setColor(Color.GREEN);
canvas.drawRect(frame.left, frame.top, frame.left + ScreenRate, frame.top + CORNER_WIDTH, paint);
canvas.drawRect(frame.left, frame.top, frame.left + CORNER_WIDTH, frame.top + ScreenRate, paint);
canvas.drawRect(frame.right - ScreenRate, frame.top, frame.right, frame.top + CORNER_WIDTH, paint);
canvas.drawRect(frame.right - CORNER_WIDTH, frame.top, frame.right, frame.top + ScreenRate, paint);
canvas.drawRect(frame.left, frame.bottom - CORNER_WIDTH, frame.left + ScreenRate, frame.bottom, paint);
canvas.drawRect(frame.left, frame.bottom - ScreenRate, frame.left + CORNER_WIDTH, frame.bottom, paint);
canvas.drawRect(frame.right - ScreenRate, frame.bottom - CORNER_WIDTH, frame.right, frame.bottom, paint);
canvas.drawRect(frame.right - CORNER_WIDTH, frame.bottom - ScreenRate, frame.right, frame.bottom, paint);
slideTop += SPEEN_DISTANCE;
if(slideTop >= frame.bottom){
slideTop = frame.top;
}
canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH/2, frame.right - MIDDLE_LINE_PADDING,slideTop + MIDDLE_LINE_WIDTH/2, paint);
paint.setColor(Color.WHITE);
paint.setTextSize(TEXT_SIZE * density);
paint.setAlpha(0x40);
paint.setTypeface(Typeface.create("System", Typeface.BOLD));
canvas.drawText(getResources().getString(R.string.scan_text), frame.left, (float) (frame.bottom + (float)TEXT_PADDING_TOP *density), paint);
Collection<ResultPoint> currentPossible = possibleResultPoints;
Collection<ResultPoint> currentLast = lastPossibleResultPoints;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new HashSet<ResultPoint>(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(OPAQUE);
paint.setColor(resultPointColor);
for (ResultPoint point : currentPossible) {
canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 6.0f, paint);
}
}
if (currentLast != null) {
paint.setAlpha(OPAQUE / 2);
paint.setColor(resultPointColor);
for (ResultPoint point : currentLast) {
canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 3.0f, paint);
}
}
postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);
}
}
public void drawViewfinder() {
resultBitmap = null;
invalidate();
}
public void drawResultBitmap(Bitmap barcode) {
resultBitmap = barcode;
invalidate();
}
public void addPossibleResultPoint(ResultPoint point) {
possibleResultPoints.add(point);
}
}
Nếu bạn muốn làm cho đường quét giống hình ảnh trong WeChat, hãy thay đoạn mã sau:
canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH/2, frame.right - MIDDLE_LINE_PADDING,slideTop + MIDDLE_LINE_WIDTH/2, paint);
Bằng đoạn mã sau:
Rect lineRect = new Rect();
lineRect.left = frame.left;
lineRect.right = frame.right;
lineRect.top = slideTop;
lineRect.bottom = slideTop + 18;
canvas.drawBitmap(((BitmapDrawable)(getResources().getDrawable(R.drawable.qrcode_scan_line))).getBitmap(), null, lineRect, paint);
Để căn giữa văn bản phía dưới khung quét, hãy chỉnh sửa đoạn mã sau:
paint.setColor(Color.WHITE);
paint.setTextSize(TEXT_SIZE * density);
paint.setAlpha(0x40);
paint.setTypeface(Typeface.DEFAULT_BOLD);
String text = getResources().getString(R.string.scan_text);
float textWidth = paint.measureText(text);
canvas.drawText(text, (width - textWidth)/2, (float) (frame.bottom + (float)TEXT_PADDING_TOP *density), paint)
Hiệu ứng khi chạy sẽ cho thấy đường quét di chuyển lên xuống, gần giống với WeChat. Tuy nhiên, bạn cần đảm bảo cấp quyền truy cập camera trong ứng dụng. Nếu có hứng thú, bạn có thể tải demo để thử nghiệm.
Bạn cũng có thể tham khảo bài viết khác của mình: "Triển khai chức năng quét mã QR từ ảnh trong điện thoại dựa trên Zxing". Trong bài đó mình đã thay thế đường quét bằng hình ảnh và mô phỏng hiệu ứng WeChat hơn. Mã nguồn có thể tải tại: http://pan.baidu.com/s/1hqzuskc (Baidu Pan)