Reputation: 31
I want to add refresher in flutter web view
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Stack(
children: <Widget>[
Container(
child: Center(
child: Text(_title),
),
),
],
)),
body: SafeArea(
child: WebView(
key: _key,
javascriptMode: JavascriptMode.unrestricted,
initialUrl: _url)),
);
}
}
Upvotes: 2
Views: 4366
Reputation: 51
@peter-koltai: Many thanks for this! I really appreciated your solution the height is working correctly, even the height is coming a bit late (page content is seen, but scrolling height not there), but there were other issues. (Sorry I can't vote you up)
Issues w/ SingleChildScrollView
:
SingleChildScrollView
has always the absolute height of the page e.g. if a text box was not expanded from the beginning (javascript), the scroll height exceeds the page height.WebView
gets the whole scroll area height, but doesn't know the display size, so if a bottom or top modal sheet appears, they are not rendered correctly in the view area of the screen but in the absolute complete height of the scroll area, so then you have to scroll e.g. 6000px up and down.Complete code:
So the solution of @shalin-shah gave me this nice working solution:
I calculate the dragging down distance (>20% of screen height) if you start at the top=0 of the page which then shows the RefreshIndicator
until onPageFinished
.
webview.dart
:
The RefreshIndicator
gets a Completer
if dragging down distance is reached and starts the reloading with the spinning, which is completed if page finishes loading.
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
@override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget> with WidgetsBindingObserver {
late WebViewController _controller;
// Drag to refresh helpers
final DragGesturePullToRefresh pullToRefresh = DragGesturePullToRefresh();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
pullToRefresh.setRefreshDistance(MediaQuery.of(context).size.height);
}
@override
Widget build(context) {
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () {
Completer<void> completer = pullToRefresh.refresh();
_controller.reload();
return completer.future;
},
child: WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
pullToRefresh.dragGestureRecognizer(_refreshIndicatorKey),
},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
pullToRefresh.setController(_controller);
},
onPageStarted: (String url) { pullToRefresh.started(); },
onPageFinished: (finish) { pullToRefresh.finished(); },
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
pullToRefresh.finished();
},
),
);
}
}
pull_to_refresh.dart
:
After drag start from top=0 of the page and is always downward, the moving distance is calculated, and when it exceeds 20% of the screen size the RefreshIndicator
show()
is called.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class AllowVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
@override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
class DragGesturePullToRefresh {
static const double EXCEEDS_LOADING_TIME = 3000;
static const double REFRESH_DISTANCE_MIN = .2;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
bool dragStarted = false;
double dragDistance = 0;
double refreshDistance = 200;
Factory<OneSequenceGestureRecognizer> dragGestureRecognizer(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey) {
return Factory<OneSequenceGestureRecognizer>(() => AllowVerticalDragGestureRecognizer()
// Got the original idea from https://stackoverflow.com/users/15862916/shalin-shah:
// https://stackoverflow.com/questions/57656045/pull-down-to-refresh-webview-page-in-flutter
..onDown = (DragDownDetails dragDownDetails) {
// if the page is still loading don't allow refreshing again
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
}
});
}
}
..onUpdate = (DragUpdateDetails dragUpdateDetails) {
calculateDrag(refreshIndicatorKey, dragUpdateDetails.delta.dy);
}
..onEnd = (DragEndDetails dragEndDetails) { clearDrag(); }
..onCancel = () { clearDrag(); });
}
void setController(WebViewController controller){ _controller = controller; }
void setRefreshDistance(double height){ refreshDistance = height * REFRESH_DISTANCE_MIN; }
Completer<void> refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
return completer;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
void clearDrag() {
dragStarted = false;
dragDistance = 0;
}
void calculateDrag(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey, double dy) async {
if (dragStarted && dy >= 0) {
dragDistance += dy;
// Show the RefreshIndicator
if (dragDistance > refreshDistance) {
debugPrint(
'DragGesturePullToRefresh:refreshPage(): $dragDistance > $refreshDistance');
clearDrag();
unawaited(refreshIndicatorKey.currentState?.show());
}
/*
The web page scrolling is not blocked, when you start to drag down from the top position of
the page to start the refresh process, e.g. like in the chrome browser. So the refresh process
is stopped if you start to drag down from the page top position and then up before reaching
the distance to start the refresh process.
*/
} else {
clearDrag();
}
}
}
This fix was helpful for the gesture events flutter webview VerticalDragGestureRecognizer get no callback but only onDown and onCancel.
The complete code is on github too.
Gif, I am not allowed to post one...
=> Fixed: Go to update
RefreshIndicator
shows no initial animation by dragging it down until the distance is reached to start the refresh process. (Can be added differently)refreshPage()
in the pull_to_refresh.dart
for my solution and the comment.I find the differences irrelevant 🤷♀️ as the issues destroyed the browsing expierence.
I changed using ScrollNotification
which RefreshIndicator
interprets right when FixedScrollMetrics
are set. So we have the original animation like in SingleChildScrollView
or e.g. chrome browser.
Complete code:
webview.dart
:
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
@override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget>
with WidgetsBindingObserver {
late WebViewController _controller;
late DragGesturePullToRefresh dragGesturePullToRefresh;
@override
void initState() {
super.initState();
dragGesturePullToRefresh = DragGesturePullToRefresh();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
dragGesturePullToRefresh.setHeight(MediaQuery.of(context).size.height);
}
@override
Widget build(context) {
return
// NotificationListener(
// onNotification: (scrollNotification) {
// debugPrint('MyWebViewWidget:NotificationListener(): $scrollNotification');
// return true;
// }, child:
RefreshIndicator(
onRefresh: () => dragGesturePullToRefresh.refresh(),
child: Builder(
builder: (context) => WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: {Factory(() => dragGesturePullToRefresh)},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
dragGesturePullToRefresh
.setContext(context)
.setController(_controller);
},
onPageStarted: (String url) { dragGesturePullToRefresh.started(); },
onPageFinished: (finish) { dragGesturePullToRefresh.finished();},
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
dragGesturePullToRefresh.finished();
},
),
),
);
}
}
pull_to_refresh.dart
:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class DragGesturePullToRefresh extends VerticalDragGestureRecognizer {
static const double EXCEEDS_LOADING_TIME = 3000;
late BuildContext _context;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
double height = 200;
bool dragStarted = false;
double dragDistance = 0;
@override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
void _clearDrag() {
dragStarted = false;
dragDistance = 0;
}
DragGesturePullToRefresh setContext(BuildContext context) { _context = context; return this; }
DragGesturePullToRefresh setController(WebViewController controller) { _controller = controller; return this; }
void setHeight(double height) { this.height = height; }
Future refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
_controller.reload();
return completer.future;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
FixedScrollMetrics _getMetrics(double minScrollExtent, double maxScrollExtent,
double pixels, double viewportDimension, AxisDirection axisDirection) {
return FixedScrollMetrics(
minScrollExtent: minScrollExtent,
maxScrollExtent: maxScrollExtent,
pixels: pixels,
viewportDimension: viewportDimension,
axisDirection: axisDirection);
}
DragGesturePullToRefresh() {
onStart = (DragStartDetails dragDetails) {
// debugPrint('MyWebViewWidget:onStart(): $dragDetails');
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
ScrollStartNotification(
metrics: _getMetrics(0, height, 0, height, AxisDirection.down),
dragDetails: dragDetails,
context: _context)
.dispatch(_context);
}
});
}
};
onUpdate = (DragUpdateDetails dragDetails) {
if (dragStarted) {
double dy = dragDetails.delta.dy;
dragDistance += dy;
ScrollUpdateNotification(
metrics: _getMetrics(
dy > 0 ? 0 : dragDistance, height,
dy > 0 ? (-1) * dy : dragDistance, height,
dragDistance < 0 ? AxisDirection.up : AxisDirection.down),
context: _context,
scrollDelta: (-1) * dy)
.dispatch(_context);
if (dragDistance < 0) {
_clearDrag();
}
}
};
onEnd = (DragEndDetails dragDetails) {
ScrollEndNotification(
metrics: _getMetrics(0, height, dragDistance, height, AxisDirection.down),
context: _context)
.dispatch(_context);
_clearDrag();
};
onCancel = () {
ScrollUpdateNotification(
metrics: _getMetrics(0, height, 1, height, AxisDirection.up),
context: _context,
scrollDelta: 0)
.dispatch(_context);
_clearDrag();
};
}
}
Upvotes: 5
Reputation: 9734
It can be done, basic problem is that RefreshIndicator
only works with a scrollable item as child, and WebView
is not scrollable from Flutter's point of view (the loaded contents are scrollable). So you have to wrap it into some kind of scrollable, but there comes the other problem: you have to know the height to do so, and you still need the contents of WebView
to be scrollable, so that you can scroll up and down on the loaded web page.
The solution includes the following steps, partially using accepted answer here.
RefreshIndicator
and add a SingleChildScrollView
as its child.Container
to hold the WebView
as child.Container
, for example the height of the screen.Container
with the acquired height.Container
height accordingly.This solution is not perfect. First, as you will see from print
outputs in debug console, height settings occur not only when strictly necessary. Second, if the content of a web page changes so that the height of the loaded documents changes as well, without actually reloading the page, height will be not synced. (For example if you add rows to a table on a dynamic webpage.)
Complete code:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView with RefreshIndicator',
home: Scaffold(
appBar: AppBar(title: Text('WebView with RefreshIndicator')),
body: SafeArea(
child: MyWebWiew(),
)),
);
}
}
class MyWebWiew extends StatefulWidget {
const MyWebWiew({Key? key}) : super(key: key);
@override
_MyWebWiewState createState() => _MyWebWiewState();
}
class _MyWebWiewState extends State<MyWebWiew> with WidgetsBindingObserver {
WebViewController? _webViewController;
// height of the WebView with the loaded content
double? _webViewHeight;
// is true while a page loading is in progress
bool _isPageLoading = true;
@override
void initState() {
super.initState();
// add listener to detect orientation change
WidgetsBinding.instance!.addObserver(this);
}
@override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
_setWebViewHeight();
}
@override
Widget build(BuildContext context) {
// on initial loading, get height using MediaQuery,
// this will be used until page is loaded
if (_webViewHeight == null) {
final initalWebViewHeight = MediaQuery.of(context).size.height;
print('WebView inital height set to: $initalWebViewHeight');
_webViewHeight = initalWebViewHeight;
}
return RefreshIndicator(
// reload page
onRefresh: () => _webViewController!.reload(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
height: _webViewHeight,
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl: 'https://flutter.dev',
onWebViewCreated: (WebViewController webViewController) {
_webViewController = webViewController;
},
onPageStarted: (String url) {
setState(() {
_isPageLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
_isPageLoading = false;
});
// if page load is finished, set height
_setWebViewHeight();
})),
));
}
void _setWebViewHeight() {
// we don't update if WebView is not ready yet
// or page load is in progress
if (_webViewController == null || _isPageLoading) {
return;
}
// execute JavaScript code in the loaded page
// to get body height
_webViewController!
.evaluateJavascript('document.body.clientHeight')
.then((documentBodyHeight) {
// set height
setState(() {
print('WebView height set to: $documentBodyHeight');
_webViewHeight = double.parse(documentBodyHeight);
});
});
}
}
Upvotes: 1