webview_flutter

查看webview内核

https://liulanmi.com/labs/core.html​

h5中获取设备

https://cloud.tencent.com/developer/ask/sof/105938013

https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/mediaDevices

web资源部署后navigator获取不到mediaDevices实例的解决方案(navigator.mediaDevices为undefined)_navigator.mediadevices不存在_乐辞的博客-CSDN博客

自已遇到的坑:

需求是app进入webview 使用h5网页获取摄像头进行人脸识别。但是h5那边一直获取不到摄像头权限

H5 页面通过navigator.mediaDevices.getUserMedia调用手机摄像头拍照上传_navigator.webkitgetusermedia-CSDN博客

web资源部署后navigator获取不到mediaDevices实例的解决方案(navigator.mediaDevices为undefined)_navigator.mediadevices不存在_乐辞的博客-CSDN博客

h5 navigator.mediaDevices 一直是undefined

原因1: 由于浏览器的安全策略导致了这个问题,目前经尝试,在以下几种情况中 navigator.mediaDevices 可以正常使用

  1. 地址为localhost:// 访问时 2. 地址为https:// 时 3. 为文件访问file:///

原因2:排除上面的问题, 使用了navigator兼容性写法获取 getUserMedia 摄像头设备 但是 getUserMedia 依旧为underfined--》怀疑是app的webview 的问题

怀疑是webview版本问题

解决方式1: 我将webview升级到最新版 发现问题不是webview的版本问题❌

最后发现flutter app中 申请权限 使用 permission_handler: ^10.3.0

await Permission.camera.request() ->

Dart 复制代码
if (await KPermiseeUtil.checkAndDoDefault(
   Permission.camera)) {
    _callCamera();
 }

安卓 谷歌内核:-》方向权限在安卓声明文件 AndroidManifest.xml 什么了对应权限 app内权限是有的。那说明只是webview的权限问题

安卓权限配置 和webview权限配置

android - How to access the camera from within a Webview? - Stack Overflow

按照上面的 依旧没有解决

最后发现 webview有个权限申请

controller.platform.setOnPlatformPermissionRequest

虽然app 进入webview申请了权限 方式 webview内部也需要进行权限申请

默认setOnPlatformPermissionRequest 这个函数回调是拒绝的 所以加下面的配置 解决

安卓不能使用h5打开摄像头的问题。

Dart 复制代码
controller.platform.setOnPlatformPermissionRequest(
      (request) {
        request.grant();
      },
    );

苹果 wkwebview的内核

如果开始有权限 流程正常,如果开始app没有权限 申请权限后 webview 依旧没有权限 需要退出app重新进才会有权限

原因解决:

之前在ios的info.plist 中声明的是这样的

html 复制代码
	<key>NSCameraUsageDescription</key>
	<string></string>

app内申请权限 依旧可以正常使用 并获取到,但是webview不行

html 复制代码
<key>NSCameraUsageDescription</key>
<string>Konnect wants to use your camera, is that allowed?</string>

原因是<string></string> =》

对flutter app请求权限做了如下封装<string>Konnect wants to use your camera, is that allowed?</string>

权限虽然声明了 但是,没有说明权限的使用用途,这个描述提示会 展示在app申请权限的弹窗底部

加上了 就解决了 上面的问题:

注意:

ios权限声明 必须添加描述 权限使用来干什么 不然app上架会被拒绝。

NSCameraUsageDescription_camera usage d-CSDN博客

Dart 复制代码
import 'dart:io';

import 'package:app/common/util/k_log_util.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';

class KPermiseeUtil {
  static final bool isIos = Platform.isIOS;
  static final bool isAndroid = Platform.isAndroid;

  // 检查权限 并做相应的处理
  static Future<bool> checkAndDoDefault(Permission permission) async {
    final status = await permission.status;
    KLogUtil.log(["status", status]);
    if (isIos) {
      switch (status) {
        case PermissionStatus.granted:
          return true;
        case PermissionStatus.permanentlyDenied:
          openAppSettings();
          return false;
        case PermissionStatus.denied:
        case PermissionStatus.limited:
        case PermissionStatus.restricted:
        default:
          final newStatus = await permission.request();
          if (newStatus == PermissionStatus.granted) {
            return true;
          }
          return false;
      }
    } else if (isAndroid) {
      switch (status) {
        case PermissionStatus.granted:
          return true;
        default:
          final newStatus = await permission.request();
          KLogUtil.log(["newStatus", newStatus]);
          if (newStatus == PermissionStatus.granted) {
            return true;
          } else if (newStatus == PermissionStatus.denied) {
            return false;
          } else if (newStatus == PermissionStatus.permanentlyDenied) {
            openAppSettings();
            return false;
          }
          return false;
      }
    }
    return await permission.status == PermissionStatus.granted;
  }

  static Future<bool> checkStatusAndDoDefault(
      PermissionState status, Permission permission) async {
    KLogUtil.log(["status", status]);
    if (isIos) {
      switch (status) {
        // notDetermined 未设置授权
        case PermissionState.notDetermined:
          return true;

        // 该应用程序未被授权访问照片库,用户也无法授予此类权限。
        case PermissionState.restricted:
          openAppSettings();
          return false;
        // 用户明确拒绝此应用程序访问照片库。
        case PermissionState.denied:
        // 用户明确授予此应用程序访问照片库的权限。
        case PermissionState.authorized:
        case PermissionState.limited:
        default:
          final newStatus = await permission.request();
          if (newStatus == PermissionStatus.granted) {
            return true;
          }
          return false;
      }
    } else if (isAndroid) {
      switch (status) {
        case PermissionStatus.granted:
          return true;
        default:
          final newStatus = await permission.request();
          KLogUtil.log(["newStatus", newStatus]);
          if (newStatus == PermissionStatus.granted) {
            return true;
          } else if (newStatus == PermissionStatus.denied) {
            return false;
          } else if (newStatus == PermissionStatus.permanentlyDenied) {
            openAppSettings();
            return false;
          }
          return false;
      }
    }
    return await permission.status == PermissionStatus.granted;
  }
}

最后自己的封装如下 (最新版本webview)

webview_flutter: ^4.2.4

webview_flutter_android: ^3.10.1

webview_flutter_wkwebview: ^3.7.4

Dart 复制代码
import 'dart:convert';
import 'dart:io';

import 'package:app/common/theme/app_theme.dart';
import 'package:app/common/util/k_log_util.dart';
import 'package:app/entity/wallet/wallet_entity.dart';
import 'package:app/gen/assets.gen.dart';
import 'package:app/pages/widget/placeholder_widget.dart';
import 'package:app/pages/widget/top_appbar.dart';
import 'package:app/sql/wallet_sql/wallet_sql.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'placeholder_type.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

// ignore: must_be_immutable
class CommonWebView extends StatefulWidget {
  final String title;
  final String url;
  bool isHiddenBar;
  CommonWebView({
    super.key,
    required this.title,
    required this.url,
    this.isHiddenBar = false,
  });

  @override
  State<CommonWebView> createState() => _CommonWebViewState();
}

class _CommonWebViewState extends State<CommonWebView> {
  late double progress = 0.01; //H5加载进度值
  final double VISIBLE = 2;
  final double GONE = 0;
  late double progressHeight = GONE; //H5加载进度条高度
  late WebViewController controller;
  late final PlatformWebViewControllerCreationParams params;
  @override
  void initState() {
    webViewInit();
    super.initState();
  }

  webViewInit() {
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }

    controller = WebViewController.fromPlatformCreationParams(params);
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController)
          .setMediaPlaybackRequiresUserGesture(false);
    }
    controller.platform.setOnPlatformPermissionRequest(
      (request) {
        request.grant();
      },
    );
    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel("konnect", onMessageReceived: onMessageReceived)
      ..setBackgroundColor(AppTheme.themeColor_black)
      ..enableZoom(true)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            // Update loading bar.
            //加载H5页面时触发多次,progress值为0-100
            this.progress = progress.toDouble() / 100.0; //计算成0.0-1.0之间的值
          },
          onPageStarted: (String url) {
            //H5页面开始加载时触发
            setProgressVisible(GONE); //VISIBLE 显示加载进度条
          },
          onPageFinished: (String url) {
            //H5页面加载完成时触发
            setProgressVisible(GONE); //隐藏加载进度条
          },
          onWebResourceError: (WebResourceError error) {
            KLogUtil.log(["error", error]);
            if (error.isForMainFrame == true) {
              switch (error.errorType) {
                case WebResourceErrorType.badUrl:
                case WebResourceErrorType.timeout:
                  break;
                case WebResourceErrorType.hostLookup:
                  Get.off(() => const PageError404());
                default:
                  break;
              }
            }
          },
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith('https://www.youtube.com/')) {
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse(widget.url));
  }

  //显示或隐藏进度条
  void setProgressVisible(double isVisible) {
    setState(() {
      progressHeight = isVisible;
    });
  }

  // 接受h5发送来的数据
  onMessageReceived(message) async {
    //接收H5发过来的数据
    String sendMesStr = message.message;
    print("H5发过来的数据1: $sendMesStr");
    KLogUtil.log(["H5发过来的数据1", sendMesStr]);
    Map<String, dynamic> msg = json.decode(sendMesStr);
    int type = msg["type"] ?? -1;
    String method = msg["method"] ?? "";
    Map<String, dynamic> data = msg["data"] ?? {};
    if (type != -1) {
      switch (type) {
        case 1:
          break;
        case 2:
          break;
        case 3:
        default:
          break;
      }
    }

    if (method.isNotEmpty) {
      switch (method) {
        case "back":
          KLogUtil.log(["back", data, msg]);
          Get.back<Map<String, dynamic>>(result: data);

          break;
        case "getWalletAddress":
          WalletEntity? myWallet = await WalletSql.getDefaultWallet();
          if (myWallet != null) {
            String walletAddress = myWallet.walletAddress;
            String signature = myWallet.signature ?? "";
            controller.runJavaScript(
                "appCallMethod('walletAddress','$walletAddress')");
            controller.runJavaScript("appCallMethod('signature','$signature')");
            KLogUtil.log(["获取到钱包", walletAddress, signature]);
          } else {
            KLogUtil.log(["没有获取到钱包"]);
          }
        default:
          break;
      }
    }
  }

  backPress() async {
    Get.back();
    return;
    // 有问题
    // String? cur = await _webControl.currentUrl();
    // KLogUtil.log(["backPress", initUrl, cur]);
    // if (initUrl == cur) {
    //   // 最后一页
    //   KLogUtil.log(["最后一页"]);
    //   Get.back();
    // }
    // if (await _webControl.canGoBack()) {
    //   _webControl.goBack(); //返回上个网页
    //   return false;
    // }
    // Get.back();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: AppTheme.themeColor_black,
        appBar: widget.isHiddenBar
            ? null
            : WebViewTopAppBar(title: widget.title, backPress: backPress),
        body: WebViewWidget(controller: controller),
      ),
    );
  }
}

class PageError404 extends StatefulWidget {
  const PageError404({super.key});

  @override
  State<PageError404> createState() => _PageError404State();
}

class WebViewTopAppBar extends PreferredSize {
  WebViewTopAppBar({
    key,
    required String title, //标题
    final Color? titleColor, //title颜色
    final Color? backgroundColor, //导航栏背景颜色
    final Widget? leadWidget, //左边按钮
    final PreferredSizeWidget? bottom, //底部组件
    final double? elevation, //AppBar底部阴影效果,0为无阴影。
    final bool statusBarColor = true, //控制状态栏的颜色true为灰色,false为白色
    final List<Widget>? rightActions, //右边按钮组
    final double? preferredSize, //状态栏高度
    final double bottomHeight = 0.0, //appBar底部高度
    final TextStyle? titleStyle,
    Function()? backPress, //返回按钮回调函数
  }) : super(
          key: key,
          preferredSize: Size.fromHeight(44.h),
          child: AppBar(
            title: Text(
              title,
              style: titleStyle ??
                  TextStyle(
                    fontSize: 16.sp,
                    fontWeight: FontWeight.bold,
                    color: titleColor ?? Colors.white,
                  ),
            ),
            centerTitle: true,
            leading: leadWidget ??
                InkWell(
                  onTap: backPress,
                  child: Padding(
                    padding: EdgeInsets.all(6.r),
                    child: Assets.icon.arrowBack.image(),
                  ),
                ),
            actions: rightActions,
            bottom: bottom,
            elevation: elevation ?? 0,
            // titleTextStyle: Theme.of(context).textTheme.bodySmall,
            backgroundColor: backgroundColor ?? AppTheme.themeColor_black,
            systemOverlayStyle: SystemUiOverlayStyle(
              statusBarColor: Colors.transparent,
              statusBarBrightness:
                  Platform.isAndroid ? Brightness.light : Brightness.dark,
              statusBarIconBrightness:
                  Platform.isAndroid ? Brightness.light : Brightness.dark,
              systemNavigationBarIconBrightness: Brightness.light,
              systemNavigationBarColor: AppTheme.themeColor_black,
            ),
          ),
        );
}

class _PageError404State extends State<PageError404> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: AppTheme.themeColor_black,
        appBar: TopAppBar(context, "404"),
        body: PlaceholderWidget.emptyPlaceholder(
            placeholderType: PlaceholderType.notFound404),
      ),
    );
  }
}
相关推荐
problc7 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
lqj_本人15 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
lqj_本人18 小时前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
起司锅仔1 天前
Flutter启动流程(2)
flutter
hello world smile1 天前
最全的Flutter中pubspec.yaml及其yaml 语法的使用说明
android·前端·javascript·flutter·dart·yaml·pubspec.yaml
lqj_本人1 天前
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
flutter·harmonyos
iFlyCai1 天前
极简实现酷炫动效:Flutter隐式动画指南第二篇之一些酷炫的隐式动画效果
flutter
lqj_本人1 天前
Flutter&鸿蒙next 中使用 MobX 进行状态管理
flutter·华为·harmonyos
lqj_本人1 天前
Flutter&鸿蒙next 中的 setState 使用场景与最佳实践
flutter·华为·harmonyos
hello world smile1 天前
Flutter常用命令整理
android·flutter·移动开发·android studio·安卓