一、前言
去年,我开发一个安卓应用,需要实现生成二维码功能,当时通过学习,使用华为统一扫码服务Scan Kit实现了生产二维码功能,相关内容见我的博文《Android 应用开发学习-生成二维码(使用华为统一扫描服务 Scan Kit)》。最近想实现生成带Logo的二维码,就再对 华为统一扫描服务 Scan Kit进行了学习,并在自己的应用中予以实现。
二、实现方法
通过学习我发现,华为统一扫描服务是支持在二维码中设置Logo的。只需要在创建HmsBuildBitmapOption对象的语句中添加setQRLogoBitmap方法即可。
HmsBuildBitmapOption options = new HmsBuildBitmapOption.Creator()
.setQRLogoBitmap(logoImage).create(); // setQRLogoBitmap方法往二维码里添加图片
try {
qrBitmap = ScanUtil.buildBitmap(qrCodeText,type,width,height, options);
iv_code2.setImageBitmap(qrBitmap); // 将二维码显示在ImageView中
} catch (WriterException e) {
Log.w("buildBitmap", e.getClass().getSimpleName());
}
这一点,在官网上没有看到说明,我是在网上搜到的一篇文章里(文章链接)找到的答案。那篇文章里是生成的圆形Logo,我不打算照抄,决定要实现一个圆角正方形的Logo图片。通过对Bitmap的相关知识点的学习,我自己写了一个生成圆角正方形的Logo图片的方法。
// 获得缩小到指定尺寸的位图对象(length为缩放后的长边)
public static Bitmap getZoomSizeImage(Context ctx, Uri uri, int length) {
// Log.d(TAG, "getZoomSizeImage uri="+uri.toString());
Bitmap zoomBitmap = null;
// 打开指定uri获得输入流对象
try (InputStream is = ctx.getContentResolver().openInputStream(uri)) {
// 从输入流解码得到原始的位图对象
Bitmap originBitmap = BitmapFactory.decodeStream(is);
int w = originBitmap.getWidth();
int h = originBitmap.getHeight();
// Log.d(TAG, "原图尺寸w=" + w + ", h=" + h);
if (w <= length || h <= length) {
return originBitmap;
}
double ratio;
if (h > w) {
ratio = (float)w / length;
} else {
ratio = (float)h / length;
}
// Log.d(TAG, "压缩比ratio=" + ratio);
// 获得比例缩放之后的位图对象
zoomBitmap = getScaleBitmap(originBitmap, 1.0/ratio);
} catch (Exception e) {
e.printStackTrace();
}
return zoomBitmap;
}
// 生成指定边长带边框的圆角图像(length为指定长边,radius是圆角半径,border是边框宽度)
public static Bitmap getLogoImage(Context ctx, Uri uri, int length, int radius, int border) {
Bitmap image = getZoomSizeImage(ctx, uri, length);
// 将图像裁切为正方形
int w = image.getWidth();
int h = image.getHeight();
int wh = Math.min(w, h);
Log.d(TAG, "压缩后的图像短边=" + wh);
if (w != h) {
int retX = w > h ? (w-h)/2 : 0;
int retY = w > h ? 0 : (h-w)/2;
image = Bitmap.createBitmap(image, retX, retY, wh, wh, null , false);
}
Bitmap rCornerImage = Bitmap.createBitmap(wh, wh, Bitmap.Config.ARGB_8888);
// 创建画笔对象
Paint paint = new Paint();
//paint.setStyle(Paint.Style.STROKE); // 设置边框风格为实线
paint.setStrokeWidth(border); // 设置边框粗细
paint.setColor(Color.WHITE); // 边框颜色
paint.setAntiAlias(true);// 设置抗锯齿
// 创建一个位图着色器,CLAMP表示边缘拉伸
BitmapShader shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader); // 设置画笔的着色器对象
int roundRadius = Utils.dip2px(ctx, radius); // 转换圆角半径
RectF rect = new RectF(0, 0, image.getWidth(), image.getHeight());
// 在画布上绘制圆角矩形
Canvas canvas = new Canvas(rCornerImage);
canvas.drawRoundRect(rect, roundRadius, roundRadius, paint);
return rCornerImage;
}
有了上面的方法,就可以生成一个指定边长带圆角和边框的图像了。图像的路径采用Uri,在主程序中添加从手机存储空间获取图片的功能代码,获取到图片后,执行上面的方法后返回生成的圆角正方形图像。然后将这个图像传给最上面的那个语句,就可以获得一个带Logo的二维码了。
我在原有代码上做了一点小修改,最终效果如下:
图1 不带logo的二维码
图2 带logo的二维码
三、代码展示
1.CreateQRCodeActivity.java文件
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bahamutjapp.util.BitmapUtil;
import com.bahamutjapp.util.DateUtil;
import com.bahamutjapp.util.FileUtil;
import com.huawei.hms.hmsscankit.ScanUtil;
import com.huawei.hms.hmsscankit.WriterException;
import com.huawei.hms.ml.scan.HmsBuildBitmapOption;
import com.huawei.hms.ml.scan.HmsScan;
import java.io.File;
import java.util.Locale;
public class CreateQRCodeActivity extends AppCompatActivity implements View.OnClickListener {
private final static String TAG = "CreateQRCodeActivity";
private EditText et_content;
private TextView tv_filePath;
private ImageView iv_code1, iv_code2;
private Bitmap logoImage;
private ActivityResultLauncher<Intent> activityResultLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_qrcode);
et_content = findViewById(R.id.et_content); // 要生成二维码的内容
tv_filePath = findViewById(R.id.tv_filePath); // 显示二维码图片的路径
iv_code1 = findViewById(R.id.iv_code1); // 显示二维码
iv_code2 = findViewById(R.id.iv_code2); // 显示带LOGO的二维码
findViewById(R.id.btn_encode1).setOnClickListener(this); // 生成二维码-无logo
findViewById(R.id.btn_savePic1).setOnClickListener(this); // 生成二维码1
findViewById(R.id.btn_selectPic).setOnClickListener(this); // 选择图片
findViewById(R.id.btn_encode2).setOnClickListener(this); // 生成带LOGO的二维码
findViewById(R.id.btn_savePic2).setOnClickListener(this); // 生成二维码2
// 选择图片 - 注册StartActivityForResult
activityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
// 操作成功的处理逻辑
Intent intent = result.getData(); //获取上一个活动返回的Intent
if (intent != null && intent.getData() != null) { // 从相册选择一张照片
Uri uri = intent.getData(); // 获得已选择照片的路径对象
// 根据指定图片的uri,获得按指定尺寸缩小后的位图对象
logoImage = BitmapUtil.getLogoImage(this, uri, 128, 15, 2);
}
} else {
// 操作失败的处理逻辑
Log.d(TAG,"返回结果异常");
}
});
} // onCreate-end
@Override
public void onClick(View v) {
Bitmap qrBitmap;
if (v.getId() == R.id.btn_encode1) { // 生成二维码
String qrCodeText = et_content.getText().toString(); // 从EditText中获取输入的内容
int type = HmsScan.QRCODE_SCAN_TYPE; // 3 HmsScan.QRCODE_SCAN_TYPE 的值为1
Log.d(TAG, "HmsScan.QRCODE_SCAN_TYPE的值为:" + type);
int width = 600;
int height = 600;
HmsBuildBitmapOption options = new HmsBuildBitmapOption.Creator().create();
try {
qrBitmap = ScanUtil.buildBitmap(qrCodeText,type,width,height, options);
iv_code1.setImageBitmap(qrBitmap); // 将二维码显示在ImageView中
tv_filePath.setText("");
} catch (WriterException e) {
Log.w("buildBitmap", e);
}
} else if (v.getId() == R.id.btn_savePic1) { // 将二维码保存为图片1
this.savePic(iv_code1);
} else if(v.getId() == R.id.btn_selectPic) { // 选择图片
Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT);
albumIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否允许多选
albumIntent.setType("image/*"); // 类型为图像
activityResultLauncher.launch(albumIntent); // 打开系统相册
} else if (v.getId() == R.id.btn_encode2) { // 生成带logo二维码
String qrCodeText = et_content.getText().toString(); // 从EditText中获取输入的内容
int type = HmsScan.QRCODE_SCAN_TYPE; // HmsScan.QRCODE_SCAN_TYPE 的值为1
int width = 600;
int height = 600;
HmsBuildBitmapOption options = new HmsBuildBitmapOption.Creator()
.setQRLogoBitmap(logoImage).create();
try {
qrBitmap = ScanUtil.buildBitmap(qrCodeText,type,width,height, options);
iv_code2.setImageBitmap(qrBitmap); // 将二维码显示在ImageView中
tv_filePath.setText("");
} catch (WriterException e) {
Log.w("buildBitmap", e.getClass().getSimpleName());
}
} else if (v.getId() == R.id.btn_savePic2) { // 将二维码保存为图片2
this.savePic(iv_code2);
}
} // onClick-end
private void savePic(ImageView iv) {
// 从ImageView对象获取图像之前,一定要调用setDrawingCacheEnabled(true)方法
iv.setDrawingCacheEnabled(true);
if (iv.getDrawable() == null) { // 检查是否已生成二维码
Toast.makeText(this, "还未生成二维码", Toast.LENGTH_SHORT).show();
return;
}
Bitmap bitmap = Bitmap.createBitmap(iv.getDrawingCache());
// 从ImageView对象获取图像之后,一定要调用setDrawingCacheEnabled(false)方法
iv.setDrawingCacheEnabled(false);
// 设置图像的保存路径
// 保存路径在/Documents/bahamutjapp/ 文件夹下 DIRECTORY_DOCUMENTS
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "bahamutjapp");
if (!file.exists()) {
boolean success = file.mkdirs();
if (success) {
Log.d(TAG, "文件夹创建成功");
} else {
Log.e(TAG, "文件夹已存在");
}
}
String fileName = "QR" + DateUtil.getNowDateTime() + ".jpeg";
String filePath = file + File.separator + fileName;
FileUtil.saveImage(filePath, bitmap);
Log.d(TAG, "二维码图片路径为:" + filePath); // 完整路径
// 在TextView中显示的路径是通过手机的文件管理app看到的路径
tv_filePath.setText(String.format(Locale.CHINESE, "%s%s", "图片路径:/Documents/bahamutjapp/", fileName));
Toast.makeText(this, "二维码图片已生成", Toast.LENGTH_SHORT).show();
}
}
2.activity_create_qrcode.xml文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
tools:context=".CreateQRCodeActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_weight="1"
android:text="生成二维码"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/et_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="20dp"
android:autofillHints=""
android:hint="输入要生成二位码的内容"
android:inputType="none"
android:imeOptions="actionDone"
android:minHeight="48dp"
android:textSize="17sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp">
<Button
android:id="@+id/btn_encode1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="生成二维码"
tools:ignore="ButtonStyle" />
<Button
android:id="@+id/btn_savePic1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="保存为图片"
tools:ignore="ButtonStyle" />
</LinearLayout>
<ImageView
android:id="@+id/iv_code1"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:layout_marginTop="5dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tv_filePath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" />
<TextView
android:id="@+id/tv_line"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="5dp"
android:background="@color/gray_220"
android:text="TextView" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text="生成带Logo的二维码"
android:textSize="24sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" >
<Button
android:id="@+id/btn_selectPic"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="7dp"
android:text="选择图片"
tools:ignore="ButtonStyle" />
<Button
android:id="@+id/btn_encode2"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="7dp"
android:layout_marginEnd="7dp"
android:text="生成二维码"
tools:ignore="ButtonStyle" />
<Button
android:id="@+id/btn_savePic2"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="7dp"
android:text="保存图片"
tools:ignore="ButtonStyle" />
</LinearLayout>
<ImageView
android:id="@+id/iv_code2"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:layout_marginTop="5dp"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="60dp" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
3.BitmapUtil.java文件部分内容
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import java.io.FileOutputStream;
import java.io.InputStream;
public class BitmapUtil {
private final static String TAG = "BitmapUtil";
// 获得缩小到指定尺寸的位图对象(length为缩放后的长边)
public static Bitmap getZoomSizeImage(Context ctx, Uri uri, int length) {
// Log.d(TAG, "getZoomSizeImage uri="+uri.toString());
Bitmap zoomBitmap = null;
// 打开指定uri获得输入流对象
try (InputStream is = ctx.getContentResolver().openInputStream(uri)) {
// 从输入流解码得到原始的位图对象
Bitmap originBitmap = BitmapFactory.decodeStream(is);
int w = originBitmap.getWidth();
int h = originBitmap.getHeight();
// Log.d(TAG, "原图尺寸w=" + w + ", h=" + h);
if (w <= length || h <= length) {
return originBitmap;
}
double ratio;
if (h > w) {
ratio = (float)w / length;
} else {
ratio = (float)h / length;
}
// Log.d(TAG, "压缩比ratio=" + ratio);
// 获得比例缩放之后的位图对象
zoomBitmap = getScaleBitmap(originBitmap, 1.0/ratio);
} catch (Exception e) {
e.printStackTrace();
}
return zoomBitmap;
}
// 生成指定边长带边框的圆角图像(length为指定长边,radius是圆角半径,border是边框宽度)
public static Bitmap getLogoImage(Context ctx, Uri uri, int length, int radius, int border) {
Bitmap image = getZoomSizeImage(ctx, uri, length);
// 将图像裁切为正方形
int w = image.getWidth();
int h = image.getHeight();
int wh = Math.min(w, h);
Log.d(TAG, "压缩后的图像短边=" + wh);
if (w != h) {
int retX = w > h ? (w-h)/2 : 0;
int retY = w > h ? 0 : (h-w)/2;
image = Bitmap.createBitmap(image, retX, retY, wh, wh, null , false);
}
Bitmap rCornerImage = Bitmap.createBitmap(wh, wh, Bitmap.Config.ARGB_8888);
// 创建画笔对象
Paint paint = new Paint();
//paint.setStyle(Paint.Style.STROKE); // 设置边框风格为实线
paint.setStrokeWidth(border); // 设置边框粗细
paint.setColor(Color.WHITE); // 边框颜色
paint.setAntiAlias(true);// 设置抗锯齿
// 创建一个位图着色器,CLAMP表示边缘拉伸
BitmapShader shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint.setShader(shader); // 设置画笔的着色器对象
int roundRadius = Utils.dip2px(ctx, radius); // 转换圆角半径
RectF rect = new RectF(0, 0, image.getWidth(), image.getHeight());
// 在画布上绘制圆角矩形
Canvas canvas = new Canvas(rCornerImage);
canvas.drawRoundRect(rect, roundRadius, roundRadius, paint);
return rCornerImage;
}
}