MAUI Blazor 调用相机扫码(Android)
MAUI Blazor 与原生 MAUI 存在核心差异:其基于 WebView 嵌套实现,页面由 Razor 组件构成,无法直接复用原生 MAUI 的二维码扫描能力。为此,针对 Android 平台,采用以下方案实现二维码扫描功能。
-
权限管理:通过
Permissions接口检查并请求相机权限,处理权限拒绝场景; -
视图构建:初始化
CameraBarcodeReaderView(相机预览与识别)、扫描线(视觉引导)、提示文本与取消按钮,采用AbsoluteLayout实现元素精确定位; -
流程控制:封装扫描启动(页面跳转、动画触发)、结果处理(震动反馈、响应返回)、资源清理(相机停止、页面关闭)全流程,同时通过
CustomScanPage重写返回键事件,确保取消逻辑统一; -
异常处理:针对上下文为空、屏幕尺寸获取失败、导航容器未初始化等场景,定义专属错误提示与响应对象,保障功能稳定性;
一、NuGet 包管理器:安装 ZXing.Net.Maui、 ZXing.Net.Maui.Controls 包

二、修改 AndroidManifest.xml 添加相机、震动权限
点击查看代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true" android:usesCleartextTraffic="true" android:label="xx">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.jiajing.iotplatform.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- 震动权限 -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
三、自定义 IQrCodeScanner.cs 接口
点击查看代码
/// <summary>
/// 二维码扫描接口类
/// </summary>
public interface IQrCodeScanner
{
/// <summary>
/// 启动相机扫描二维码
/// </summary>
/// <returns>扫描到的二维码内容</returns>
Task<QrScannerResponse> ScanQrCodeAsync();
}
四、在 Platforms/Android 目录下创建 QrCodeScanner.cs 文件,实现 IQrCodeScanner 接口
点击查看代码
/// <summary>
/// Android平台二维码扫描器实现
/// 提供二维码扫描功能,包括相机权限管理、扫描界面展示和扫描结果处理
/// </summary>
public class QrCodeScanner : IQrCodeScanner
{
#region 私有字段
/// <summary>
/// 安卓上下文对象,用于访问系统功能
/// </summary>
private readonly Context _androidContext;
/// <summary>
/// 扫描视图,负责相机预览和二维码识别
/// </summary>
private CameraBarcodeReaderView _barcodeReaderView;
/// <summary>
/// 扫描线控件,提供视觉引导
/// </summary>
private BoxView _scanLine;
/// <summary>
/// 扫描提示文字控件,显示操作指引
/// </summary>
private Label _scanLabel;
/// <summary>
/// 扫描状态标识,控制扫描动画的运行与停止
/// </summary>
private bool _isScanning = false;
/// <summary>
/// 当前扫描页面引用
/// </summary>
private ContentPage _currentScanPage;
/// <summary>
/// 扫描结果任务完成源
/// </summary>
private TaskCompletionSource<QrScannerResponse> _resultTcs;
/// <summary>
/// 震动管理器,用于扫描成功后的触觉反馈
/// </summary>
private Vibrator _vibrator;
#endregion
#region 常量定义
/// <summary>
/// 扫描框大小(像素)
/// </summary>
private const int ScanAreaSize = 300;
/// <summary>
/// 扫描线最大高度(中间部分)
/// </summary>
private const int ScanLineMaxHeight = 2;
/// <summary>
/// 扫描提示文字与扫描框的间距
/// </summary>
private const int LabelMarginTop = 35;
#endregion
#region 错误提示文本
private const string PermissionDeniedTip = "需要相机权限才能扫描二维码,请在系统设置中开启";
private const string NavigationNullTip = "导航容器未初始化,无法显示扫描页面";
private const string ViewInitFailedTip = "扫描视图初始化失败";
private const string ContextNullTip = "安卓上下文不能为空";
private const string ScreenSizeErrorTip = "无法获取屏幕尺寸,扫描功能无法使用";
private const string CancelTip = "未识别,已取消扫描";
#endregion
/// <summary>
/// 初始化二维码扫描器
/// </summary>
/// <param name="context">安卓上下文</param>
public QrCodeScanner(Context context)
{
_androidContext = context;
if (_androidContext != null)
{
// 初始化震动管理器
InitializeVibrator();
}
}
/// <summary>
/// 启动二维码扫描流程
/// </summary>
/// <returns>扫描结果响应对象</returns>
public async Task<QrScannerResponse> ScanQrCodeAsync()
{
// 基础校验
if (_androidContext == null)
return CreateErrorResponse(ApiCode.Exception, ContextNullTip);
// 检查相机权限
var permissionResult = await CheckCameraPermission();
if (!string.IsNullOrEmpty(permissionResult))
return CreateErrorResponse(ApiCode.Exception, permissionResult);
// 创建任务完成源,用于异步返回扫描结果
_resultTcs = new TaskCompletionSource<QrScannerResponse>();
try
{
// 初始化扫描相关视图
InitializeScannerViews();
// 创建扫描页面布局
_currentScanPage = CreateScanPage(_resultTcs);
// 显示扫描页面
var showPageResult = await ShowScanPageAsync(_currentScanPage);
if (!string.IsNullOrEmpty(showPageResult))
{
await StopScanning();
return CreateErrorResponse(ApiCode.Exception, showPageResult);
}
// 等待页面渲染完成后再定位扫描元素和启动动画
await Task.Delay(300);
// 定位扫描元素
var positionResult = PositionScanElements();
if (!string.IsNullOrEmpty(positionResult))
{
await StopScanning();
return CreateErrorResponse(ApiCode.Exception, positionResult);
}
// 启动扫描动画
StartScanLineAnimation();
// 返回扫描结果
return await _resultTcs.Task;
}
catch (Exception ex)
{
await StopScanning();
return CreateErrorResponse(ApiCode.Exception, $"扫描初始化失败:{ex.Message}");
}
finally
{
// 清理引用
_currentScanPage = null;
_resultTcs = null;
}
}
#region 初始化扫描相关视图组件 private void InitializeScannerViews()
/// <summary>
/// 初始化扫描相关视图组件
/// </summary>
private void InitializeScannerViews()
{
// 初始化条形码扫描视图
_barcodeReaderView = new CameraBarcodeReaderView
{
Options = new BarcodeReaderOptions
{
Formats = ZXing.Net.Maui.BarcodeFormat.QrCode, // 只识别二维码
AutoRotate = true, // 自动旋转
TryHarder = true // 提高识别准确率
},
IsEnabled = true,
IsDetecting = true
};
// 初始化扫描线(白色)
_scanLine = new BoxView
{
Color = Colors.White,
HeightRequest = ScanLineMaxHeight,
HorizontalOptions = LayoutOptions.Fill
};
// 初始化扫描提示文字
_scanLabel = new Label
{
Text = "请将设备二维码对准扫描框",
TextColor = Colors.White,
FontSize = 16,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
Opacity = 0.85 // 稍微降低不透明度,避免过于刺眼
};
}
/// <summary>
/// 创建扫描页面及其布局
/// </summary>
/// <param name="resultTcs">用于返回扫描结果的任务完成源</param>
/// <returns>构建好的扫描页面</returns>
private CustomScanPage CreateScanPage(TaskCompletionSource<QrScannerResponse> resultTcs)
{
// 创建主布局容器(绝对布局,用于精确定位扫描线和提示文字)
var mainLayout = new AbsoluteLayout
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand
};
// 添加相机视图到主布局
AbsoluteLayout.SetLayoutBounds(_barcodeReaderView, new Rect(0, 0, 1, 1));
AbsoluteLayout.SetLayoutFlags(_barcodeReaderView, AbsoluteLayoutFlags.All);
mainLayout.Children.Add(_barcodeReaderView);
// 创建取消按钮
var cancelButton = CreateCancelButton(resultTcs);
// 绑定扫描结果事件
_barcodeReaderView.BarcodesDetected += async (sender, args) =>
{
// 确保只处理一次结果
if (args.Results.Any() && !resultTcs.Task.IsCompleted)
{
var result = args.Results[0].Value;
// 扫描成功后震动手机
VibrateOnSuccess();
await StopScanning();
resultTcs.SetResult(CreateSuccessResponse(result));
}
};
// 构建自定义扫描页面
var scanPage = new CustomScanPage(resultTcs)
{
BackgroundColor = Colors.Black,
Content = new Grid
{
Children = {
mainLayout, // 扫码区域
cancelButton // 取消按钮
}
}
};
// 设置取消操作的委托
scanPage.CancelAction = () => CancelScan(resultTcs);
return scanPage;
}
/// <summary>
/// 创建取消按钮并绑定事件
/// </summary>
/// <param name="resultTcs">用于返回扫描结果的任务完成源</param>
/// <returns>构建好的取消按钮</returns>
private Button CreateCancelButton(TaskCompletionSource<QrScannerResponse> resultTcs)
{
var cancelButton = new Button
{
Text = "×",
TextColor = Colors.White,
BackgroundColor = Color.FromArgb("#80000000"), // 半透明黑色
CornerRadius = 22, // 圆形按钮
WidthRequest = 44,
HeightRequest = 44,
FontSize = 24, // 文字大小
FontAttributes = FontAttributes.Bold,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.Start,
Margin = new Thickness(20, 30, 0, 0),
Padding = new Thickness(0), // 移除内边距,确保符号居中
MinimumWidthRequest = 44, // 设置按钮点击区域
MinimumHeightRequest = 44
};
// 绑定取消按钮点击事件
cancelButton.Clicked += (s, e) => CancelScan(resultTcs);
return cancelButton;
}
#endregion
#region 页面显示与控制 private async Task<string> ShowScanPageAsync(ContentPage scanPage)
/// <summary>
/// 显示扫描页面
/// </summary>
private async Task<string> ShowScanPageAsync(ContentPage scanPage)
{
try
{
await MainThread.InvokeOnMainThreadAsync(async () =>
{
if (Application.Current?.MainPage?.Navigation == null)
{
throw new Exception(NavigationNullTip);
}
await Application.Current.MainPage.Navigation.PushModalAsync(scanPage, false);
});
return null;
}
catch (Exception ex)
{
return ex.Message;
}
}
/// <summary>
/// 准确定位扫描线和提示文字位置
/// 扫描线位于扫描框内,提示文字位于扫描框下方居中
/// </summary>
private string PositionScanElements()
{
try
{
MainThread.BeginInvokeOnMainThread(() =>
{
// 获取屏幕尺寸
var mainPage = Application.Current?.MainPage;
if (mainPage == null)
throw new Exception(ViewInitFailedTip);
double screenWidth = GetScreenDimension(mainPage.Width, DeviceDisplay.MainDisplayInfo.Width);
double screenHeight = GetScreenDimension(mainPage.Height, DeviceDisplay.MainDisplayInfo.Height);
if (screenWidth <= 0 || screenHeight <= 0)
throw new Exception(ScreenSizeErrorTip);
// 计算扫描框位置(屏幕中央)
var scanAreaX = (screenWidth - ScanAreaSize) / 2;
var scanAreaY = (screenHeight - ScanAreaSize) / 2;
// 设置扫描线初始位置(扫描框内偏上位置)
SetElementLayout(_scanLine, scanAreaX, scanAreaY + 60, ScanAreaSize, ScanLineMaxHeight);
// 设置提示文字位置(扫描框下方居中)
SetElementLayout(_scanLabel, scanAreaX,
scanAreaY + ScanAreaSize + LabelMarginTop,
ScanAreaSize, 30);
// 将扫描线和提示文字添加到主布局(确保只添加一次)
if (_barcodeReaderView.Parent is AbsoluteLayout mainLayout)
{
AddElementToLayout(mainLayout, _scanLine);
AddElementToLayout(mainLayout, _scanLabel);
}
});
return null;
}
catch (Exception ex)
{
return $"扫描元素定位失败:{ex.Message}";
}
}
/// <summary>
/// 获取屏幕尺寸(处理可能的空值和0值)
/// </summary>
/// <param name="pageDimension">页面尺寸</param>
/// <param name="displayDimension">显示信息尺寸</param>
/// <returns>计算后的屏幕尺寸</returns>
private double GetScreenDimension(double pageDimension, double displayDimension)
{
return pageDimension > 0
? pageDimension
: displayDimension / DeviceDisplay.MainDisplayInfo.Density;
}
/// <summary>
/// 设置元素在绝对布局中的位置和大小
/// </summary>
private void SetElementLayout(View element, double x, double y, double width, double height)
{
AbsoluteLayout.SetLayoutBounds(element, new Rect(x, y, width, height));
AbsoluteLayout.SetLayoutFlags(element, AbsoluteLayoutFlags.None);
}
/// <summary>
/// 将元素添加到布局中(确保只添加一次)
/// </summary>
private void AddElementToLayout(AbsoluteLayout layout, View element)
{
if (element.Parent != layout)
{
if (element.Parent is Layout parentLayout)
parentLayout.Children.Remove(element);
layout.Children.Add(element);
}
}
#endregion
#region 扫描动画控制 private void StartScanLineAnimation()
/// <summary>
/// 启动扫描线动画
/// 实现扫描线在扫描区域内上下移动的效果
/// </summary>
private void StartScanLineAnimation()
{
_isScanning = true;
MainThread.BeginInvokeOnMainThread(async () =>
{
try
{
// 获取屏幕尺寸和扫描区域参数
var (scanAreaX, scanAreaY, endY) = CalculateScanAreaParameters();
if (scanAreaX < 0 || scanAreaY < 0 || endY < 0)
return;
// 初始化扫描线位置和移动方向(从偏下位置开始)
var currentY = scanAreaY + 50; // 初始位置偏下
var direction = 1; // 1 向下移动, -1 向上移动
const int speed = 1; // 移动速度(像素/帧)
// 动画循环
while (_isScanning)
{
// 更新扫描线位置
currentY += direction * speed;
// 到达边界时反向移动
if (currentY >= endY)
{
currentY = endY;
direction = -1;
}
else if (currentY <= scanAreaY)
{
currentY = scanAreaY;
direction = 1;
}
// 应用新位置到扫描线
AbsoluteLayout.SetLayoutBounds(_scanLine, new Rect(
scanAreaX,
currentY,
ScanAreaSize,
ScanLineMaxHeight
));
// 控制动画帧率(约60fps)
await Task.Delay(16);
}
}
catch
{
// 动画异常时停止扫描,不抛出异常
_isScanning = false;
}
});
}
/// <summary>
/// 计算扫描区域参数
/// </summary>
/// <returns>扫描区域的X坐标、Y坐标和结束Y坐标</returns>
private (double scanAreaX, double scanAreaY, double endY) CalculateScanAreaParameters()
{
var mainPage = Application.Current?.MainPage;
if (mainPage == null)
return (-1, -1, -1);
double screenWidth = GetScreenDimension(mainPage.Width, DeviceDisplay.MainDisplayInfo.Width);
double screenHeight = GetScreenDimension(mainPage.Height, DeviceDisplay.MainDisplayInfo.Height);
if (screenWidth <= 0 || screenHeight <= 0)
return (-1, -1, -1);
// 计算扫描区域位置和范围
var scanAreaX = (screenWidth - ScanAreaSize) / 2;
var scanAreaY = (screenHeight - ScanAreaSize) / 2;
var endY = scanAreaY + ScanAreaSize - ScanLineMaxHeight;
return (scanAreaX, scanAreaY, endY);
}
#endregion
#region 停止扫描并清理资源 private async Task StopScanning()
/// <summary>
/// 停止扫描并清理资源
/// </summary>
private async Task StopScanning()
{
// 停止扫描动画
_isScanning = false;
// 停止相机检测
if (_barcodeReaderView != null)
{
_barcodeReaderView.IsDetecting = false;
_barcodeReaderView.IsEnabled = false;
}
// 关闭扫描页面
try
{
await MainThread.InvokeOnMainThreadAsync(async () =>
{
if (Application.Current?.MainPage?.Navigation != null)
{
await Application.Current.MainPage.Navigation.PopModalAsync(false);
}
});
}
catch
{
// 页面关闭异常不处理,避免影响主流程
}
}
#endregion
#region 权限管理 private async Task<string> CheckCameraPermission()
/// <summary>
/// 检查并请求相机权限
/// </summary>
/// <returns>权限正常返回null,异常返回提示文本</returns>
private async Task<string> CheckCameraPermission()
{
try
{
// 检查相机权限状态
var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
// 已授权则直接返回
if (status == PermissionStatus.Granted)
return null;
// 未授权则请求权限
status = await Permissions.RequestAsync<Permissions.Camera>();
return status == PermissionStatus.Granted ? null : PermissionDeniedTip;
}
catch (Exception ex)
{
return $"相机权限请求失败:{ex.Message}";
}
}
#endregion
#region 取消扫描处理 private async void CancelScan(TaskCompletionSource<QrScannerResponse> resultTcs)
/// <summary>
/// 统一处理取消扫描的逻辑
/// 无论是点击取消按钮还是按返回键都调用此方法
/// </summary>
private async void CancelScan(TaskCompletionSource<QrScannerResponse> resultTcs)
{
if (resultTcs.Task.IsCompleted)
return;
await StopScanning();
resultTcs.SetResult(CreateCancelResponse());
}
#endregion
#region 震动功能初始化 private void InitializeVibrator()
/// <summary>
/// 安全初始化震动管理器
/// </summary>
private void InitializeVibrator()
{
try
{
#if ANDROID
if (_androidContext != null)
{
var vibratorService = _androidContext.GetSystemService(Context.VibratorService);
if (vibratorService is Vibrator vibrator)
{
_vibrator = vibrator;
}
}
#endif
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"震动服务初始化失败: {ex.Message}");
_vibrator = null;
}
}
/// <summary>
/// 扫描成功时震动手机
/// </summary>
private void VibrateOnSuccess()
{
try
{
#if ANDROID
if (_vibrator != null && HasVibrator())
{
// 震动100毫秒
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
_vibrator.Vibrate(VibrationEffect.CreateOneShot(100, VibrationEffect.DefaultAmplitude));
}
else
{
_vibrator.Vibrate(100);
}
}
#endif
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"震动失败: {ex.Message}");
// 震动失败不影响主要功能,静默处理
}
}
/// <summary>
/// 检查设备是否支持震动
/// </summary>
private bool HasVibrator()
{
#if ANDROID
return _vibrator?.HasVibrator == true;
#else
return false;
#endif
}
#endregion
#region 响应对象创建方法 private QrScannerResponse CreateSuccessResponse(string data)
/// <summary>
/// 创建成功响应
/// </summary>
/// <param name="data">二维码数据</param>
/// <returns>成功响应对象</returns>
private QrScannerResponse CreateSuccessResponse(string data)
{
return new QrScannerResponse
{
Code = ApiCode.Success,
Msg = "扫描成功",
Data = data
};
}
/// <summary>
/// 创建取消响应
/// </summary>
/// <returns>取消响应对象</returns>
private QrScannerResponse CreateCancelResponse()
{
return new QrScannerResponse
{
Code = ApiCode.Exception,
Msg = CancelTip,
Data = CancelTip
};
}
/// <summary>
/// 创建错误响应
/// </summary>
/// <param name="code">错误代码</param>
/// <param name="message">错误信息</param>
/// <returns>错误响应对象</returns>
private QrScannerResponse CreateErrorResponse(ApiCode code, string message)
{
return new QrScannerResponse
{
Code = code,
Msg = message,
Data = null
};
}
#endregion
}
/// <summary>
/// 自定义扫描页面
/// </summary>
public class CustomScanPage : ContentPage
{
private readonly TaskCompletionSource<QrScannerResponse> _resultTcs;
public CustomScanPage(TaskCompletionSource<QrScannerResponse> resultTcs)
{
_resultTcs = resultTcs;
}
protected override bool OnBackButtonPressed()
{
// 调用取消扫描的逻辑
CancelAction?.Invoke();
return true; // 表示已处理返回键事件,不执行默认行为
}
public Action CancelAction { get; set; }
}
QrScannerResponse.cs 识别结果返回类
/// <summary>
/// 识别结果,返回类
/// </summary>
public class QrScannerResponse
{
/// <summary>
/// 状态码
/// </summary>
public ApiCode Code { get; set; }
/// <summary>
/// 提示信息
/// </summary>
public string Msg { get; set; }
/// <summary>
/// 识别数据
/// </summary>
public string Data { get; set; }
}
/// <summary>
/// 状态码,枚举类
/// </summary>
public enum ApiCode : int
{
/// <summary>
/// 成功
/// </summary>
Success = 10001,
/// <summary>
/// 异常
/// </summary>
Exception = 10002,
/// <summary>
/// 无效
/// </summary>
InValidData = 10003,
}
五、修改 MauiProgram.cs 文件,注册相关服务
#if ANDROID 是必不可少的,该条件编译指令能确保相关逻辑只在 Android 环境中执行.
点击查看代码
// Add the using to the top
using ZXing.Net.Maui.Controls;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseBarcodeReader(); // Make sure to add this line
#if ANDROID
// 注册Android扫描服务(注入上下文)
builder.Services.AddSingleton<IQrCodeScanner>(sp =>
new Platforms.Android.QrCodeScanner(
Android.App.Application.Context // 应用级上下文,避免内存泄漏
)
);
#endif
return builder.Build();
}
}
六、通过调用 QrCodeScanner.ScanQrCodeAsync() 方法识别二维码
点击查看代码
// 注入QR识别组件服务
[Inject] private IQrCodeScanner QrCodeScanner { get; set; }
#region 调用ScanQrCodeAsync实现QR码识别功能
/// <summary>
/// 调用二维码扫描器
/// </summary>
public void OnQrCodeScanner()
{
// 扫描QR码并处理结果
QrCodeScanner.ScanQrCodeAsync()
.ContinueWith(async task =>
{
// 检查任务是否成功完成
if (!task.IsCompletedSuccessfully || task.Result == null)
return;
QrScannerResponse result = task.Result;
// 检查扫描结果是否成功
if (result.Code != ApiCode.Success)
{
await dialogService.InfoSnackbarAsync($"扫码失败:{result.Msg}")
.ConfigureAwait(false);
return;
}
try
{
// 解析二维码数据为JObject
if (string.IsNullOrEmpty(result.Data))
{
await dialogService.ErrorSnackbarAsync("二维码数据为空")
.ConfigureAwait(false);
return;
}
// 添加数据处理逻辑
JObject jObject = JObject.Parse(result.Data);
string xx= jObject["xx"]?.ToString() ?? string.Empty;
}
catch (Exception)
{
// 解析失败时显示原始内容
await dialogService.InfoSnackbarAsync($"识别内容:{result.Data}")
.ConfigureAwait(false);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
#endregion
七、效果图演示


浙公网安备 33010602011771号