第一个安卓app-改造成扫码复制链接
@
改造
提示:代码是google帮我写的,这边仅仅就是记录遇到的坑
gradle分为项目和模块,见图

修改一次gradle文件需要右上角点一次“sync now”加载

之前报错说我的版本是jdk1.8需要改成11

app操作流程
其实就是打开相机,然后扫码弹出toast框,再可以复制扫的二维码的链接
视频传送门
小结
这样一个想做的app大致功能已经完成了,上架需要企业资质和软著,暂时不搞了
一下是google给我的教程,只是参考,因为后面还有改动的
好的,要在你的应用中实现自动打开摄像头、显示扫码UI、扫描二维码并弹出结果(带复制按钮)的功能,你需要做以下几件事情:
- 添加权限:访问摄像头需要权限。
- 集成相机库和扫码库:我们将使用
CameraX(Google 推荐的相机库) 和ML Kit Barcode Scanning(Google 提供的机器学习套件中的条码扫描功能)。 - 设计UI布局:需要一个
PreviewView来显示摄像头预览,可能还有一个提示文本。 - 编写Java/Kotlin代码:
- 请求并处理相机权限。
- 初始化相机并将其绑定到
PreviewView。 - 设置图像分析器 (
ImageAnalysis) 来处理相机帧并进行二维码检测。 - 当检测到二维码时,显示一个包含二维码内容和“复制”按钮的对话框。
- 实现复制功能。
下面是详细步骤,以 Java 为例,并假设你的项目是基于 Empty Views Activity 创建的。
步骤 1:更新 build.gradle (Module: app) 文件
打开你的 app/build.gradle 文件,在 dependencies 代码块中添加以下库:
dependencies {
// ... 其他已有的依赖 ...
// CameraX 核心库
def camerax_version = "1.3.0" // 你可以检查最新的稳定版本
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}" // 可选,提供一些扩展功能
// ML Kit Barcode Scanning
implementation 'com.google.mlkit:barcode-scanning:17.2.0' // 你可以检查最新的稳定版本
// 如果你想在没有Google Play服务的设备上使用ML Kit,可以添加下面的依赖
// (但对于大部分情况,上面的就够了)
// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0'
}
添加完后,点击 Android Studio 顶部出现的 "Sync Now" 链接,让 Gradle 同步项目依赖。
步骤 2:在 AndroidManifest.xml 中添加权限和特性声明
打开 app/src/main/AndroidManifest.xml 文件,在 <manifest> 标签内,<application> 标签外添加以下内容:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myfirstapp"> {/* 确保这是你的包名 */}
<!-- 声明相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 声明应用需要相机硬件 (可选但推荐) -->
<uses-feature android:name="android.hardware.camera.any" />
<application
...>
{/* ... 你的 Activity 定义 ... */}
</application>
</manifest>
步骤 3:修改布局文件 activity_main.xml
打开 app/res/layout/activity_main.xml 文件。我们将用一个 FrameLayout 来容纳 PreviewView 和一个提示文本。
将其内容修改为:
<?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"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/scanningInfoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
android:background="#80000000"
android:padding="10dp"
android:text="请将二维码置于框内"
android:textColor="@android:color/white"
android:textSize="16sp"
android:visibility="gone" /> {/* 初始隐藏,相机启动后显示 */}
<!-- 你也可以在这里添加一个更复杂的扫描框UI,例如用ImageView覆盖 -->
</FrameLayout>
PreviewView: 用于显示相机实时预览。scanningInfoText: 一个简单的文本提示。
步骤 4:修改 MainActivity.java 文件
这是核心部分,代码会比较长。
打开 app/java/com.example.myfirstapp/MainActivity.java (你的包名可能不同)。
package com.example.myfirstapp; // 确保这是你的包名
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.barcode.BarcodeScanner;
import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
import com.google.mlkit.vision.barcode.BarcodeScanning;
import com.google.mlkit.vision.barcode.common.Barcode;
import com.google.mlkit.vision.common.InputImage;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int REQUEST_CODE_PERMISSIONS = 101;
private final String[] REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA};
private PreviewView previewView;
private TextView scanningInfoText;
private ExecutorService cameraExecutor;
private BarcodeScanner barcodeScanner;
private boolean isScanningPaused = false; // 标志位,防止重复处理同一个码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
previewView = findViewById(R.id.previewView);
scanningInfoText = findViewById(R.id.scanningInfoText);
cameraExecutor = Executors.newSingleThreadExecutor();
// 配置条码扫描器,只识别二维码
BarcodeScannerOptions options =
new BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build();
barcodeScanner = BarcodeScanning.getClient(options);
// 检查相机权限
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
}
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera();
} else {
Toast.makeText(this, "相机权限被拒绝,无法使用扫码功能。", Toast.LENGTH_LONG).show();
finish(); // 如果没有权限,可以关闭应用或提示用户去设置
}
}
}
private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
bindPreviewAndAnalysis(cameraProvider);
// 相机启动后显示提示文本
runOnUiThread(() -> scanningInfoText.setVisibility(View.VISIBLE));
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "获取 CameraProvider 失败: ", e);
Toast.makeText(this, "无法启动相机", Toast.LENGTH_SHORT).show();
}
}, ContextCompat.getMainExecutor(this));
}
private void bindPreviewAndAnalysis(@NonNull ProcessCameraProvider cameraProvider) {
// 预览用例
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
// 相机选择器,选择后置摄像头
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build();
// 图像分析用例
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(1280, 720)) // 根据需要设置分辨率
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 只处理最新的图像帧
.build();
imageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
@SuppressLint("UnsafeOptInUsageError")
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
if (isScanningPaused) { // 如果已暂停扫描(例如已显示结果对话框)
imageProxy.close();
return;
}
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
barcodeScanner.process(image)
.addOnSuccessListener(barcodes -> {
if (!isScanningPaused && !barcodes.isEmpty()) {
isScanningPaused = true; // 标记为已扫描到,暂停进一步扫描
Barcode barcode = barcodes.get(0); // 获取第一个码
String rawValue = barcode.getRawValue();
Log.d(TAG, "二维码内容: " + rawValue);
// 在主线程显示结果对话框
runOnUiThread(() -> showResultDialog(rawValue));
}
})
.addOnFailureListener(e -> Log.e(TAG, "条码扫描失败", e))
.addOnCompleteListener(task -> imageProxy.close()); // 确保ImageProxy被关闭
} else {
imageProxy.close(); // 如果mediaImage为null,也关闭
}
}
});
// 在绑定用例前,先解绑所有用例
cameraProvider.unbindAll();
try {
// 绑定用例到相机
cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, preview, imageAnalysis);
} catch (Exception e) {
Log.e(TAG, "用例绑定失败", e);
Toast.makeText(this, "无法绑定相机用例", Toast.LENGTH_SHORT).show();
}
}
private void showResultDialog(String qrCodeContent) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("扫描结果");
builder.setMessage("二维码内容:\n" + qrCodeContent);
builder.setPositiveButton("复制", (dialog, which) -> {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("QR Code Content", qrCodeContent);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, "已复制到剪贴板", Toast.LENGTH_SHORT).show();
resumeScanningAfterDialog(dialog);
});
builder.setNegativeButton("关闭", (dialog, which) -> {
resumeScanningAfterDialog(dialog);
});
builder.setCancelable(false); // 禁止点击外部区域关闭对话框
AlertDialog dialog = builder.create();
dialog.show();
}
// 在对话框关闭后恢复扫描
private void resumeScanningAfterDialog(DialogInterface dialog) {
dialog.dismiss();
// 延迟一小段时间再恢复扫描,给用户反应时间,避免立即又扫到同一个码
previewView.postDelayed(() -> isScanningPaused = false, 500);
}
@Override
protected void onDestroy() {
super.onDestroy();
cameraExecutor.shutdown();
if (barcodeScanner != null) {
barcodeScanner.close(); // 释放ML Kit扫描器资源
}
}
}
代码解释:
-
常量和变量:
REQUEST_CODE_PERMISSIONS: 用于权限请求的请求码。REQUIRED_PERMISSIONS: 需要的权限数组 (这里是CAMERA)。previewView: 用于显示相机预览。scanningInfoText: 显示提示信息的TextView。cameraExecutor: 一个单独的线程执行器,用于相机相关的耗时操作,如图像分析。barcodeScanner: ML Kit 的条码扫描器实例。isScanningPaused: 一个布尔标志,用于在检测到二维码并显示对话框后,暂时停止处理新的图像帧,防止快速连续弹出多个对话框。
-
onCreate():- 获取
PreviewView和TextView的实例。 - 初始化
cameraExecutor。 - 配置
BarcodeScannerOptions,使其只检测FORMAT_QR_CODE。 - 获取
BarcodeScanner实例。 - 检查相机权限,如果已授予则调用
startCamera(),否则请求权限。
- 获取
-
allPermissionsGranted():检查所有必需的权限是否已授予。 -
onRequestPermissionsResult():处理权限请求的结果。如果权限被授予,则启动相机。 -
startCamera():- 异步获取
ProcessCameraProvider的实例。ProcessCameraProvider用于将相机的生命周期绑定到LifecycleOwner(例如 Activity)。 - 获取到
cameraProvider后,调用bindPreviewAndAnalysis()来设置和绑定相机用例。 - 相机启动后,显示
scanningInfoText。
- 异步获取
-
bindPreviewAndAnalysis():Preview用例:创建Preview对象,并将其 SurfaceProvider 设置为previewView的 SurfaceProvider。这样相机预览就会显示在previewView上。CameraSelector:选择使用后置摄像头 (LENS_FACING_BACK)。ImageAnalysis用例:- 创建
ImageAnalysis对象。setTargetResolution可以影响分析的图像大小,setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)表示分析器只处理最新的可用图像帧,如果处理速度跟不上,旧的帧会被丢弃。 setAnalyzer():这是核心!它接收一个Executor和一个ImageAnalysis.Analyzer接口的实现。Analyzer的analyze(@NonNull ImageProxy imageProxy)方法会在cameraExecutor线程上为每一帧相机图像调用。- 在
analyze方法中:- 检查
isScanningPaused标志。 - 从
imageProxy获取mediaImage。 - 将
mediaImage和旋转角度包装成InputImage,这是 ML Kit 需要的格式。 - 调用
barcodeScanner.process(image)来处理图像。 addOnSuccessListener: 如果成功检测到条码:- 设置
isScanningPaused = true。 - 获取第一个条码 (
barcodes.get(0)) 的原始值 (getRawValue())。 - 使用
runOnUiThread在主线程上调用showResultDialog()显示结果。
- 设置
addOnFailureListener: 处理扫描失败的情况。addOnCompleteListener: 无论成功还是失败,务必调用imageProxy.close()来释放图像资源,否则相机预览会卡住。
- 检查
- 创建
cameraProvider.unbindAll(): 在绑定新的用例集之前,最好先解绑所有旧的用例。cameraProvider.bindToLifecycle(...): 将相机选择器、预览用例和图像分析用例绑定到 Activity 的生命周期。
-
showResultDialog():- 创建一个
AlertDialog.Builder。 - 设置标题和消息(二维码内容)。
- "复制" 按钮:
- 获取
ClipboardManager系统服务。 - 创建
ClipData对象,包含要复制的文本。 - 将
ClipData设置到剪贴板。 - 显示一个
Toast提示用户。 - 调用
resumeScanningAfterDialog()。
- 获取
- "关闭" 按钮:
- 调用
resumeScanningAfterDialog()。
- 调用
- 设置
setCancelable(false)防止用户通过点击对话框外部来关闭它,确保他们通过按钮操作。 - 显示对话框。
- 创建一个
-
resumeScanningAfterDialog():- 关闭对话框。
- 通过
postDelayed延迟一小段时间后,将isScanningPaused设置回false,这样相机就可以继续分析图像帧。这个延迟是为了避免对话框刚关闭,摄像头立刻又扫描到同一个码。
-
onDestroy():- 关闭
cameraExecutor以释放线程资源。 - 调用
barcodeScanner.close()释放 ML Kit 扫描器占用的资源。
- 关闭
步骤 5:运行应用
现在,点击 Android Studio 工具栏上的绿色播放按钮,将应用安装到你的真实安卓设备上 (模拟器通常没有摄像头或扫码效果不好)。
- 应用启动后,会请求相机权限。
- 授权后,摄像头会打开,你应该能在屏幕上看到预览。
scanningInfoText会显示 "请将二维码置于框内"。- 将一个二维码对准摄像头。
- 一旦检测到二维码,会弹出一个对话框显示二维码的 URL (或其他内容)。
- 对话框下面有 "复制" 和 "关闭" 按钮。
- 点击 "复制" 会将内容复制到剪贴板。
- 关闭对话框后,你可以继续扫描其他二维码。
进一步的改进和注意事项:
- UI美化:当前的 "正在扫码中" UI 很简单。你可以设计一个更专业的扫描框,例如中间透明,四周半透明,并带有一个扫描线动画。这通常通过在
FrameLayout中叠加一个自定义 View 或ImageView来实现。 - 对焦:CameraX 通常会自动处理对焦,但对于某些场景,你可能需要添加手动对焦的逻辑(例如点击屏幕对焦)。
- 闪光灯:可以添加一个按钮来控制闪光灯的开关。
- 错误处理:更完善的错误处理,例如当相机无法打开时给用户更明确的提示。
- 性能:
setTargetResolution可以调整,更高的分辨率可能识别更准但更耗性能。 - ML Kit 依赖: 如果你的应用需要在没有 Google Play 服务的设备 (例如某些国内定制安卓系统或华为设备) 上运行 ML Kit 的条码扫描功能,你需要引入 "standalone" 版本的 ML Kit 库,并在
AndroidManifest.xml中添加额外配置。但对于大部分有 Google Play 服务的设备,当前配置即可。
这个教程提供了一个功能完整的二维码扫描基础。祝你编码愉快!

浙公网安备 33010602011771号