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),
      ),
    );
  }
}
相关推荐
江上清风山间明月1 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter