webview_flutter小米8的适配

1,973 阅读2分钟

问题 webview_flutter 直接使用会小米8会出现顶部空白

解决方案 用Container包裹,给Container设置宽高

1.依赖

webview_flutter: ^2.0.13

2.自定义WebViewWidget控件

WebViewWidget

import 'dart:async';
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';

import 'javascript_bridge.dart';

class WebViewWidget extends StatefulWidget {
  final String initialUrl;
  final JavascriptBridge? bridge;
  final Completer<WebViewController>? controller;
  final Function(String)? onPageFinished;
  final bool canRefresh;

  //可以自定义webview的宽高,0 的时候设置成屏幕的宽高,默认值是 0
  final double width;
  final double height;
  final Function(WebViewController controller)? onRefreshByBtn;

  WebViewWidget(
      {required this.initialUrl,
      this.bridge,
      this.controller,
      this.onPageFinished,
      this.canRefresh = false,
      this.width = 0,
      this.height = 0,
      this.onRefreshByBtn,
      Key? key})
      : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return WebViewWidgetState();
  }
}

class WebViewWidgetState extends State<WebViewWidget> {
  Completer<WebViewController>? _controller;
  bool _loading = true;
  String? _errorMsg;
  String? _errorDetail;

  @override
  void initState() {
    super.initState();
    if (Platform.isAndroid) {
      WebView.platform = SurfaceAndroidWebView();
    }

    _controller = widget.controller ?? Completer<WebViewController>();
  }

  Widget? webView;

  String _getUrl() {
    String url = widget.initialUrl;
    return Platform.isIOS ? Uri.encodeFull(url) : url;
  }

  @override
  Widget build(BuildContext context) {
    webView = WebView(
      debuggingEnabled: !kReleaseMode,
      initialUrl: _getUrl(),
      javascriptChannels: widget.bridge?.channels.values.toSet() ?? Set(),
      javascriptMode: JavascriptMode.unrestricted,
      gestureRecognizers: Set()
        ..add(Factory<VerticalDragGestureRecognizer>(() {
          return VerticalDragGestureRecognizer()
            ..onStart = (DragStartDetails details) {
              print("@drag_start");
            }
            ..onUpdate = (DragUpdateDetails details) {
              print("@drag_update: $details");
            }
            ..onDown = (DragDownDetails details) {
              if (Platform.isIOS) {
                SystemChannels.textInput.invokeMethod('TextInput.hide');
              }
              print("@drag_down: $details");
            }
            ..onCancel = () {
              print("@drag_cancel");
            }
            ..onEnd = (DragEndDetails details) {
              print("@drag_end");
            };
        })),
      onWebViewCreated: (webViewController) {
        var isCompleted = _controller?.isCompleted ?? false;
        print("@web_created:$isCompleted");
        if (!isCompleted) {
          _controller?.complete(webViewController);
        }
      },
      onWebResourceError: (error) {
        StringBuffer sb = StringBuffer('onWebResourceError:\n');
        if (error.failingUrl != null && error.failingUrl!.isNotEmpty) {
          sb.writeln('url:${error.failingUrl}');
        }
        if (error.errorType != null) {
          sb.writeln('errorType:${error.errorType}');
        }
        if (error.domain != null && error.domain!.isNotEmpty) {
          sb.writeln('domain:${error.domain}');
        }
        sb.writeln('errorCode:${error.errorCode}');
        if (error.description.isNotEmpty) {
          _errorDetail = error.description;
          sb.writeln('description:${error.description}');
        }
        setState(() {
          _loading = false;
          _errorMsg = '发生错误:${error.errorCode}';
        });
        print(sb.toString());
      },
      onPageStarted: (value) {
        setState(() {
          _loading = true;
          _errorMsg = null;
          _errorDetail = null;
        });
      },
      onPageFinished: (value) {
        if (widget.onPageFinished != null) {
          widget.onPageFinished!(value);
        }
        if (widget.canRefresh) {}
        setState(() {
          _loading = false;
          _errorMsg = null;
          _errorDetail = null;
        });
        _controller?.future.then((value) {
          value.currentUrl().then((url) {
            print("web_finished: $url");
          });
        });
      },
    );
    return Stack(
      children: [
        Container(
          width: widget.width <= 0
              ? MediaQuery.of(context).size.width
              : widget.width,
          height: widget.height <= 0
              ? MediaQuery.of(context).size.height
              : widget.height,
          child: webView,
        ),
        Visibility(
            visible: _loading,
            child: Container(
              color: Colors.white,
              child: Center(
                child: CupertinoActivityIndicator(),
              ),
            )),
        Visibility(
            visible: _errorMsg != null,
            child: Container(
              color: Colors.white,
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(_errorMsg ?? ''),
                  RaisedButton(
                    onPressed: () async {
                      var webViewController = await _controller?.future;
                      var url = await webViewController?.currentUrl();
                      webViewController?.loadUrl(url ?? widget.initialUrl);
                      setState(() {
                        _loading = true;
                        _errorMsg = null;
                        _errorDetail = null;
                      });
                    },
                    child: Text('重新加载'),
                  ),
                  Visibility(
                    visible: _errorDetail != null,
                    child: TextButton(
                      onPressed: () {
                        Clipboard.setData(ClipboardData(text: _errorDetail));
                      },
                      child: Text('复制错误信息'),
                    ),
                  ),
                ],
              ),
            )),
        Positioned(
            right: 20,
            bottom: 80,
            child: Visibility(
              visible: widget.canRefresh,
              child: FloatingActionButton(
                mini: true,
                onPressed: () {
                  _controller?.future.then((value) {
                    if (null != widget.onRefreshByBtn) {
                      widget.onRefreshByBtn!(value);
                    } else {
                      value.reload();
                    }
                  });
                },
                child: Icon(Icons.refresh),
              ),
            ))
      ],
    );
  }
}

JavascriptBridge

import 'dart:convert';

import 'package:webview_flutter/webview_flutter.dart';

typedef JavascriptMessageReceiver = void Function(BridgeData data);

class JavascriptBridge {
  Map<String, JavascriptChannel> _channels = {};

  Map<String, JavascriptChannel> get channels => _channels;

  // parse: 是否解析成统一格式
  void add(String name, JavascriptMessageReceiver receiver,
      {bool parse = true}) {
    assert(name != null && receiver != null);
    if (_channels.containsKey(name)) {
      return;
    }
    _channels[name] = JavascriptChannel(
      name: name,
      onMessageReceived: (message) {
        if (parse) {
          try {
            if (message.message != null && message.message.isNotEmpty) {
              receiver(_parseJSMessage(message));
              return;
            }
          } catch (e, s) {
            print(e.toString());
            print(s.toString());
          }
          receiver(BridgeData());
        } else {
          receiver(BridgeData(data: message.message));
        }
      },
    );
  }

  BridgeData _parseJSMessage(JavascriptMessage message) {
    print('Parse JavascriptMessage=>${message.message}');
    var object = json.decode(message.message);
    if (object['data'] != null &&
        object['data'] is Map &&
        object['data']['bridgeName'] != null) {
      // {"data":{"bridgeName":"","callbackKey":"","data":{}}}
      return BridgeData.fromJson(object['data']);
    }
    return BridgeData.fromJson(object);
  }
}

class BridgeData {
  String? bridgeName;
  String? callbackKey;
  dynamic data;

  BridgeData({this.bridgeName, this.callbackKey, this.data});

  BridgeData.fromJson(Map<String, dynamic> json) {
    bridgeName = json['bridgeName'];
    callbackKey = json['callbackKey'];
    data = json['data'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['bridgeName'] = this.bridgeName;
    data['callbackKey'] = this.callbackKey;
    data['data'] = this.data;
    return data;
  }
}

工具类

class StringUtils {
  /// 判断是否数字
  static isDouble(String str) {
    print("${double.tryParse(str)}");
    return null != double.tryParse(str);
  }

  static isNotDouble(String str) {
    return !isDouble(str);
  }

  /// 字符串转double
  static double stringToDouble(String str) {
    if (isDouble(str)) {
      return double.parse(str);
    } else {
      return 0;
    }
  }
}

3.使用

import 'package:flutter/material.dart';
import 'package:uilib/webview/web_view_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'WebView',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: WebViewWidget(
          initialUrl: "https://www.baidu.com",
          canRefresh: true,
        ));
  }
}