一、引言
前几天我完成了生成带Logo的二维码方法的学习(详见 安卓应用开发学习:生成带Logo的二维码),这几天再接再厉里,又实现了对网址二维码的识别,并在应用中提供跳转链接的功能。特分享。
二、实现方法
网址二维码,就是将一串网址链接生成二维码。
我在去年学会了手机扫描功能(详见 Android应用开发学习-使用华为统一扫码服务实现扫码功能),这次的识别网址二维码就在这个功能模块的基础上进行改进。
1.如何识别网址二维码
用我的手机扫码应用扫描二维码后,就获取其中的内容,结果是一个字符串。如何判断这个字符串是不是一个网址链接呢?经过在网上搜索,我找到了答案。
安卓的android.util.Patterns库有识别字符串是否是网址的方法:
// obj.originalValue是扫码得到的结果
boolean isHyperlink = Patterns.WEB_URL.matcher(obj.originalValue).matches();
通过上面的语句来判断扫描得到的结果是不是一个网址链接。
2.如何在应用页面中创建一个超链接
通过上面的判断语句如果确认扫描结果是一个网址链接的话,我希望在应用页面中显示一个超链接,点击超链接就可以访问这个网址。通过搜索我也找到了答案(参考资料 android TextView显示成超链接)。
我的实现步骤:
(1)在这个Activity对应的xml文件中添加一个TextView组件。id设置为"@+id/tv_hyperlink"
// 页面使用的ConstraintLayout布局
<TextView
android:id="@+id/tv_hyperlink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="http://"
android:textSize="17sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_qrCodeText" />
(2)在这个Activity的java文件中添加一些代码:
private TextView hyperlink;
// 以下语句添加在onCreate方法中
hyperlink = findViewById(R.id.tv_hyperlink); // TextView组件显示网页链接
hyperlink.setVisibility(View.INVISIBLE); // 初始化不显示此组件
hyperlink.setClickable(true); // 设置为可点击
// 为TextView设置点击事件处理
hyperlink.setOnClickListener(v -> {
// 打开链接的操作
String url = hyperlink.getText().toString();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
});
// 以下语句添加在获得扫描结果之后
hyperlink.setText("");
hyperlink.setVisibility(View.INVISIBLE);
// 判断扫码结果是否为超链接
boolean isHyperlink = Patterns.WEB_URL.matcher(obj.originalValue).matches();
if (isHyperlink) {
// 创建一个SpannableString对象
SpannableString spanString = new SpannableString(obj.originalValue);
// 创建一个URLSpan对象,指定超链接的文本和URL
URLSpan urlSpan = new URLSpan(obj.originalValue);
// 将URLSpan应用于SpannableString对象的相应文本段
spanString.setSpan(urlSpan, 0, spanString.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置TextView支持超链接点击事件
hyperlink.setMovementMethod(LinkMovementMethod.getInstance());
hyperlink.setVisibility(View.VISIBLE);
hyperlink.setText(obj.originalValue);
}
以上代码让代码为tv_hyperlink的TextView组件能够支持超链接点击事件。且该组件只有在识别出网址链接后才会显示,未识别出网址链接是不会显示。
三、效果测试
应用修改好后就要进行测试,我用“草料二维码”网站生成了几个二维码进行测试。
1.用一个有效的网址链接生成二维码。
用我的扫码应用扫描这个二维码,正确识别出网址,并用TextView组件生成一个可点击的超链接。
点击这个链接,我的手机自动打开了CSDN的手机端APP,并显示出这个链接的网页内容。
2. 用一个无效的链接生成二维码。
这是我随便输入的一个无效链接,扫码后仍然被识别为网址链接。
虽然可以点击,但这个网址不存在,得到的是一个“网址暂时无法打开”的错误提示。
3. 网址和文本混合生成二维码。
这次用一个有效的网址与文本一同生成二维码。扫码后,其中的网址无法识别。
4.使用不完整的网址生成二维码。
这次输入的网址不含http前缀,在浏览器中这么输入是能够被识别,并能够补全网址,正常访问的。在我的应用中,也可以识别出来,并生成超链接。但是一点击这个超链接就会闪退出这个扫描应用模块。
我将手机连接电脑,用Android Studio查看错误日志,显示的问题如下:
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat= }
我用百度搜了一下,百度给出的答案如下:
这个异常表示系统找不到能够处理你的请求的Activity。这通常发生在启动一个Activity时,如果系统无法在已安装的应用中找到与指定的Intent匹配的Activity,就会抛出这个异常。
前面的第一个测试手机调用的CSDN的手机端APP,第二个测试手机调用的浏览器APP,第四个测试出错,说明手机应用不能自动对不完整的网址进行补全。要解决这个问题需要开发者人在代码中添加相应的判断逻辑,进行补全。
对此问题,我对代码进行了修改,在判断出二维码的内容是网址后,对扫码结果是不是以“http”开头进行了判断,如果不是,则进行补全。补全后,再次进行测试,成功实现打开网页。
补全网址后,点击超链接,手机自动打开浏览器APP,显示了百度的首页内容。
四、代码展示
最后上完整的代码:
1. activity_scan_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=".ScanQRCodeActivity">
<TextView
android:id="@+id/tv_scanTitle"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="10dp"
android:text="扫描二维码"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:text="扫一扫"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_scanTitle" />
<TextView
android:id="@+id/tv_qrCodeText"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginTop="20dp"
android:text="扫描结果"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_scan"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/tv_hyperlink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="http://"
android:textSize="17sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_qrCodeText" />
</androidx.constraintlayout.widget.ConstraintLayout>
2. ScanQRCodeActivity.java文件代码
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.widget.Toast;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.util.Patterns;
import com.huawei.hms.hmsscankit.ScanUtil;
import com.huawei.hms.ml.scan.HmsScan;
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions;
public class ScanQRCodeActivity extends AppCompatActivity implements View.OnClickListener {
private final static String TAG = "ScanQRCodeActivity";
private TextView qrCodeText, hyperlink;
private static final int REQUEST_CODE_SCAN_ONE = 99;
private static final int REQUEST_CODE_PERMISSION = 98;
// 扫码必须的权限
private final String[] permissions = {Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan_qrcode);
findViewById(R.id.btn_scan).setOnClickListener(this); // 扫描二维码
qrCodeText = findViewById(R.id.tv_qrCodeText); // 显示扫码结果
hyperlink = findViewById(R.id.tv_hyperlink); // 显示网页链接
hyperlink.setVisibility(View.INVISIBLE); // 初始化不显示
hyperlink.setClickable(true); // 设置为可点击
// 为TextView设置点击事件处理
hyperlink.setOnClickListener(v -> {
// 打开链接的操作
String url = hyperlink.getText().toString();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
});
}
@Override
public void onClick(View v) {
if(v.getId() == R.id.btn_scan ) { // 扫一扫
// 判断操作系统的版本,Android 6.0及以上需要动态申请权限
Log.d(TAG, "开始扫描");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
this.requestPermissions(permissions, REQUEST_CODE_PERMISSION);
} else {
startDefaultScanMode();
}
}
} // onClick-end
//开启默认扫码模式
private void startDefaultScanMode() {
// 要设置扫码类型,修改setHmsScanTypes()方法的参数 HmsScan.QRCODE_SCAN_TYPE, HmsScan.DATAMATRIX_SCAN_TYPE
HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions
.Creator()
.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE)
.create();
// 调用ScanUtil的静态方法startScan启动Default View扫码页面
ScanUtil.startScan(this, REQUEST_CODE_SCAN_ONE, options);
}
// 申请权限后的返回结果处理
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSION) {
for (int grantResult : grantResults) {
if (grantResult == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, "需要开启该相机和存储权限才能正常使用扫码功能!", Toast.LENGTH_SHORT).show();
return;
}
}
startDefaultScanMode(); // 使用默认扫码模式
}
}
// 处理扫码结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK || data == null) {
return;
}
if (requestCode == REQUEST_CODE_SCAN_ONE) {
//解析出扫码结果对象,并toast一下
HmsScan obj = data.getParcelableExtra(ScanUtil.RESULT);
String str = "";
if (obj != null) {
int scanType = obj.getScanType(); // 获取条码类型
Rect border = obj.getBorderRect(); // 获取码在图中位置
int typeForm = obj.getScanTypeForm(); // 结构化数据
//Toast.makeText(this, obj.originalValue, Toast.LENGTH_LONG).show();
str = "扫描结果:\n" + obj.originalValue + "\n\n条码类型:" + scanType +
"\n码在图中位置:\n" + border + "\n结构化数据:" + typeForm;
qrCodeText.setText(str);
hyperlink.setText("");
hyperlink.setVisibility(View.INVISIBLE);
// 判断扫码结果是否为超链接
boolean isHyperlink = Patterns.WEB_URL.matcher(obj.originalValue).matches();
if (isHyperlink) {
// 检查扫码的网址是否以http开头
String linkStr;
String checkStr = "http";
boolean startsWithStr = obj.originalValue.startsWith(checkStr);
if(startsWithStr) {
linkStr = obj.originalValue;
} else {
// 对网址进行补全
linkStr = String.format("%s%s", "http://", obj.originalValue);
}
// 创建一个SpannableString对象
SpannableString spanString = new SpannableString(linkStr);
// 创建一个URLSpan对象,指定超链接的文本和URL
URLSpan urlSpan = new URLSpan(linkStr);
// 将URLSpan应用于SpannableString对象的相应文本段
spanString.setSpan(urlSpan, 0, spanString.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
// 设置TextView支持超链接点击事件
hyperlink.setMovementMethod(LinkMovementMethod.getInstance());
hyperlink.setVisibility(View.VISIBLE);
hyperlink.setText(linkStr);
}
}
}
}
}