Flutter webview的定制化

发布于:2023-01-21 ⋅ 阅读:(448) ⋅ 点赞:(0)

目前我有一个需求,就是开发一个新闻APP,类似于头条,有新闻的展示,用户的评论和点赞、收藏等等,这个后端接口已经设计好了,这个功能主要通过访问url使用webview来实现该功能,一言不合就开始撸代码了:

  • 加载flutter webview插件
  • 定制化webview的显示界面
  • 对官方插件的二次定制
  • 完成对url的加载监听

1. 加载flutter webview插件

flutter_webview_plugin: ^0.3.0+2

2. 定制化webview的显示界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZKov487-1660268716054)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c955e9a094b74537a4b2f17ec18b84da~tplv-k3u1fbpfcp-watermark.image)]

本页面主要由三部分组成,最上面那部分是flutter的原生页面,中间部分是webview页面,最下方是由bottomNavigationBar组件实现,如代码所示。

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
          title: Text('网页详情'),
          leading: IconButton(
            icon:Icon(Icons.arrow_back_ios,size:20) ,
            onPressed: (){
              _webviewReference.hide();
              String url = 'http://www.appshuo.club/appview/p/';
              MyRouter.pushNoParams(context, MyRouter.homePage);
            },),
          bottom: PreferredSize(
            child: _progressBar(lineProgress, context),
            preferredSize: Size.fromHeight(0.1),
          )
      ),
      body: SafeArea(
      child:  SingleChildScrollView(
              child: Padding(
          padding: EdgeInsets.all(10),
      child: Column(
          children: [
            Padding(
              padding: EdgeInsets.only(bottom: 10),
             child:Row(
              children: [
                CircleAvatar(
                  radius: 20.0,
                  backgroundImage: NetworkImage(widget.article.user.photo),
                  backgroundColor: Colors.white,
                ),
                Padding(
                  padding: const EdgeInsets.only(left: 10.0),
                  child: Text(widget.article.user.nickname),
                ),
                Padding(
                  padding: const EdgeInsets.only(left: 15.0),
                  child: Text('阅读',style: TextStyle(color: Colors.black38,fontSize: 13.0)),
                ),
                Padding(
                  padding: const EdgeInsets.only(left: 3.0),
                  child: Text('${widget.article.readcount}',style: TextStyle(color: Colors.black38,fontSize: 13.0)),
                ),
                Expanded(
                  child: GestureDetector(
                    child: Padding(
                        padding: EdgeInsets.only(left: ScreenUtils.screenW()/2-50,right: 10.0),
                        child: new SizedBox(
                            width: 50,
                            child: FlatButton(
                                  shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(0.0),
                                  side: BorderSide(color: Colors.green)),
                                  color: Colors.white,
                                  textColor: Colors.green,
                                  padding: EdgeInsets.all(8.0),
                                  onPressed: () {},
                                  child: Text(
                                    "+ 关注",
                                    style: TextStyle(
                                      fontSize: 14.0,
                                    ),
                              ),
                            ),
                        ),
                    ),
                    onTap: () {
                      MyRouter.pushNoParams(context,MyRouter.userPage);
                    },
                  ),
                )
              ],
            )),
            Container(
              height: ScreenUtils.screenH()-ScreenUtils.padTopH() - 120.0 - 40.0,
              child: MyFlutterWebViewPage(url),
            ),
          ],
        ),
      )),
    ),
      bottomNavigationBar: BottomAppBar(
        child: talkFeild(),
      ),

    );
  }

Widget talkFeild(){
    return Row(
      children: <Widget>[
        Expanded(
          child: Container(
            margin: EdgeInsets.only(left: 20.0,top:5.0,bottom: 5.0),
            width: MediaQuery.of(context).size.width,
            alignment: AlignmentDirectional.center,
            height: this.h,
            decoration: BoxDecoration(
                color: Color.fromARGB(255, 237, 236, 237),
                borderRadius: BorderRadius.circular(24.0)),
            child: TextField(
              // onSubmitted: onSubmitted,
              onTap: (){
                _webviewReference.hide();
                MyRouter.push(context, MyRouter.writeCommentPage, {'articleId': widget.article.id, 'article': widget.article});
              },
              cursorColor: Color.fromARGB(255, 0, 189, 96),
              decoration: InputDecoration(
                  contentPadding: const EdgeInsets.only(left: 10.0,bottom:12.0),
                  border: InputBorder.none,
                  hintText: '说两句儿...',
                  hintStyle: TextStyle(
                      fontSize: 15, color: Color.fromARGB(255, 192, 191, 191)),
                  ),
              style: TextStyle(fontSize: 15),
            ),
          ),
        ),
        GestureDetector(
          child: Padding(
            padding: EdgeInsets.only(left: 20.0),
            child: IconText(context, widget.article.commentCount == 0 ? '评论' : widget.article.commentCount, Constant.ASSETS_IMG +
                'ic_notification_tv_calendar_comments.png'),
          ),
          onTap: () {
            _webviewReference.hide();
            MyRouter.push(context, MyRouter.commentList, {'articleId': widget.article.id, 'article': widget.article});
          },
        ),
        GestureDetector(
          child: Padding(
              padding: EdgeInsets.only(left: 20.0, right: 20.0),
              child: IconText(context, widget.article.praiseCount == 0 ? '赞' : widget.article.praiseCount, Constant.ASSETS_IMG + 'ic_vote.png'),
          ),
          onTap: () {
            Navigator.pop(context);
          },
        )
      ],
    );
  }

  Row textField() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      crossAxisAlignment: CrossAxisAlignment.end,
      children: <Widget>[
        Expanded(
          child: new TextField(
            decoration: InputDecoration(
              hintText: '期待您的评论...',
              border: null,
              focusedBorder: UnderlineInputBorder(
                borderSide: BorderSide(color: Colors.blue[300]),
              ),
            ),
            keyboardType: TextInputType.text,
            maxLength: 250,
            maxLines: 1,
          ),
        ),
        IconButton(
          icon: Icon(Icons.send),
          onPressed: () {
            _webviewReference.show();
            Navigator.of(context).pop();
          },
        )
      ],
    );
  }

  TextStyle getStyle(Color color, double fontSize, {bool bold = false}) {
    return TextStyle(
        color: color,
        fontSize: fontSize,
        fontWeight: bold ? FontWeight.bold : FontWeight.normal);
  }
}

3. 对官方插件的二次定制

先通过notifyRect()在界面上面一个矩形,然后使用_webviewReference.launch方法在指定的矩形中加载url

import 'dart:async';

import 'package:duo_zui/Util/screen_utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class MyFlutterWebViewPage extends StatefulWidget {
  final String url;
  bool hidden = false;

  MyFlutterWebViewPage(this.url, {Key key}) : super(key: key);

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

class _WebViewWidgetState extends State<MyFlutterWebViewPage>  {

  final webviewReference = FlutterWebviewPlugin();

  bool _closed = false;

  Rect _rect;
  bool needFullScreen = false;
  Timer _resizeTimer;

  StreamSubscription<WebViewStateChanged> _onStateChanged;

  @override
  void initState() {
    super.initState();
    webviewReference.close();
    if (widget.hidden) {
      _onStateChanged =
          webviewReference.onStateChanged.listen((WebViewStateChanged state) {
            if (state.type == WebViewState.finishLoad) {
              webviewReference.show();
            }
          });
    }
  }

  @override
  void dispose() {
    super.dispose();
    webviewReference.close();
    webviewReference.dispose();
    if (widget.hidden) {
      _onStateChanged.cancel();
    }
  }

  @override
  Widget build(BuildContext context) {
    print('build widget.url=\\\\${widget.url}');
    return _WebviewPlaceholder(onRectChanged: (Rect value) {
      if (_rect == null || _closed) {
        if(_rect != value){
          _rect = value;
        }
        print('_webviewReference.launch');
        webviewReference.launch(widget.url,
            withJavascript: true,
            withLocalStorage: true,
            scrollBar: true,
            rect: getRect());
      } else {
        print('_webviewReference.launch else');
        // if (_rect != value) {
        //   _rect = value;
        // }
        // webviewReference.reloadUrl(widget.url);
        if (_rect != value) {
          _rect = value;
          _resizeTimer?.cancel();
          _resizeTimer = Timer(const Duration(milliseconds: 250), () {
            // avoid resizing to fast when build is called multiple time
            webviewReference.resize(_rect);
          });
        }
      }
    }, child: const Center(child: const CircularProgressIndicator()),);
  }

  getRect() {
    if(needFullScreen){
      return null;
    }else{
      return Rect.fromLTRB(0.0, ScreenUtils.padTopH() + 120.0,
          ScreenUtils.screenW(), ScreenUtils.screenH() - 40.0);
    }
  }

}

class _WebviewPlaceholder extends SingleChildRenderObjectWidget {
  const _WebviewPlaceholder({
    Key key,
    @required this.onRectChanged,
    Widget child,
  }) : super(key: key, child: child);

  final ValueChanged<Rect> onRectChanged;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _WebviewPlaceholderRender(
      onRectChanged: onRectChanged,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, _WebviewPlaceholderRender renderObject) {
    renderObject..onRectChanged = onRectChanged;
  }
}

class _WebviewPlaceholderRender extends RenderProxyBox {
  _WebviewPlaceholderRender({
    RenderBox child,
    ValueChanged<Rect> onRectChanged,
  })  : _callback = onRectChanged,
        super(child);

  ValueChanged<Rect> _callback;
  Rect _rect;

  Rect get rect => _rect;

  set onRectChanged(ValueChanged<Rect> callback) {
    if (callback != _callback) {
      _callback = callback;
      notifyRect();
    }
  }

  void notifyRect() {
    if (_callback != null && _rect != null) {
      _callback(_rect);
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    super.paint(context, offset);
    final rect = offset & size;
    if (_rect != rect) {
      _rect = rect;
      notifyRect();
    }
  }
}

4. 完成对url的加载监听

最后来实现加载url的进度条:

  _progressBar(double progress, BuildContext context) {
    return Container(
      child: LinearProgressIndicator(
        backgroundColor: Colors.blueAccent.withOpacity(0),
        value: progress == 1.0 ? 0 : progress,
        valueColor: new AlwaysStoppedAnimation<Color>(Colors.lightBlue),
      ),
      height: 1,
    );
  }

整个定制的过程就写完了,在网上定制webview的资料还是很少见的,通常都是整个页面的加载,以上是其中的一个方案,希望可以帮助到大家,以后我会持续更新我的app进度,将我遇到的难点和我的方案分享给大家。

最后我将我的疑问抛出来,希望哪位大佬看到我的疑问,可以为我答疑解惑,共同进步,1、flutter的webview的页面都是会覆盖flutter的原生Widget,导致我的评论框要评论的时候都显示不出来,我实现的方案是跳转到另一个页面,但是感觉不是友好,2、flutter的webview如何在文章后追加flutter原生的widget,比如我的评论列表,以及以下推荐新闻,我现在的实现方案是将评论列表放到了网页中。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到