第一个安卓app-改造成扫码复制链接

@

改造

提示:代码是google帮我写的,这边仅仅就是记录遇到的坑
gradle分为项目和模块,见图

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

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

app操作流程

其实就是打开相机,然后扫码弹出toast框,再可以复制扫的二维码的链接
视频传送门

小结

这样一个想做的app大致功能已经完成了,上架需要企业资质和软著,暂时不搞了

一下是google给我的教程,只是参考,因为后面还有改动的
好的,要在你的应用中实现自动打开摄像头、显示扫码UI、扫描二维码并弹出结果(带复制按钮)的功能,你需要做以下几件事情:

  1. 添加权限:访问摄像头需要权限。
  2. 集成相机库和扫码库:我们将使用 CameraX (Google 推荐的相机库) 和 ML Kit Barcode Scanning (Google 提供的机器学习套件中的条码扫描功能)。
  3. 设计UI布局:需要一个 PreviewView 来显示摄像头预览,可能还有一个提示文本。
  4. 编写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扫描器资源
        }
    }
}

代码解释

  1. 常量和变量

    • REQUEST_CODE_PERMISSIONS: 用于权限请求的请求码。
    • REQUIRED_PERMISSIONS: 需要的权限数组 (这里是 CAMERA)。
    • previewView: 用于显示相机预览。
    • scanningInfoText: 显示提示信息的 TextView
    • cameraExecutor: 一个单独的线程执行器,用于相机相关的耗时操作,如图像分析。
    • barcodeScanner: ML Kit 的条码扫描器实例。
    • isScanningPaused: 一个布尔标志,用于在检测到二维码并显示对话框后,暂时停止处理新的图像帧,防止快速连续弹出多个对话框。
  2. onCreate()

    • 获取 PreviewViewTextView 的实例。
    • 初始化 cameraExecutor
    • 配置 BarcodeScannerOptions,使其只检测 FORMAT_QR_CODE
    • 获取 BarcodeScanner 实例。
    • 检查相机权限,如果已授予则调用 startCamera(),否则请求权限。
  3. allPermissionsGranted():检查所有必需的权限是否已授予。

  4. onRequestPermissionsResult():处理权限请求的结果。如果权限被授予,则启动相机。

  5. startCamera()

    • 异步获取 ProcessCameraProvider 的实例。ProcessCameraProvider 用于将相机的生命周期绑定到 LifecycleOwner (例如 Activity)。
    • 获取到 cameraProvider 后,调用 bindPreviewAndAnalysis() 来设置和绑定相机用例。
    • 相机启动后,显示 scanningInfoText
  6. bindPreviewAndAnalysis()

    • Preview 用例:创建 Preview 对象,并将其 SurfaceProvider 设置为 previewView 的 SurfaceProvider。这样相机预览就会显示在 previewView 上。
    • CameraSelector:选择使用后置摄像头 (LENS_FACING_BACK)。
    • ImageAnalysis 用例
      • 创建 ImageAnalysis 对象。setTargetResolution 可以影响分析的图像大小,setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 表示分析器只处理最新的可用图像帧,如果处理速度跟不上,旧的帧会被丢弃。
      • setAnalyzer():这是核心!它接收一个 Executor 和一个 ImageAnalysis.Analyzer 接口的实现。
        • Analyzeranalyze(@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 的生命周期。
  7. showResultDialog()

    • 创建一个 AlertDialog.Builder
    • 设置标题和消息(二维码内容)。
    • "复制" 按钮
      • 获取 ClipboardManager 系统服务。
      • 创建 ClipData 对象,包含要复制的文本。
      • ClipData 设置到剪贴板。
      • 显示一个 Toast 提示用户。
      • 调用 resumeScanningAfterDialog()
    • "关闭" 按钮
      • 调用 resumeScanningAfterDialog()
    • 设置 setCancelable(false) 防止用户通过点击对话框外部来关闭它,确保他们通过按钮操作。
    • 显示对话框。
  8. resumeScanningAfterDialog():

    • 关闭对话框。
    • 通过 postDelayed 延迟一小段时间后,将 isScanningPaused 设置回 false,这样相机就可以继续分析图像帧。这个延迟是为了避免对话框刚关闭,摄像头立刻又扫描到同一个码。
  9. 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 服务的设备,当前配置即可。

这个教程提供了一个功能完整的二维码扫描基础。祝你编码愉快!

posted @ 2025-05-27 15:09  蜗牛使劲冲  阅读(40)  评论(0)    收藏  举报