Flutter 新聞詳情頁二——WebView和列表豎直滾動
新聞閱讀介面,主要有新聞內容和評論列表或者一些相關推薦新聞。在具體實現時採用WebView和ListView來組合實現。但是WebView的垂直方向的滾動和ListView的垂直滾動會有衝突,解決起來十分麻煩,下面是我的一個方案,希望有讀者能提出更好的方案。
在Android開發的時候,解決ListView(或者 RecyclerView)巢狀WebView的方法主要是,使用WebView自適應高度功能(設定高度wrap_content),讓ListView完全接管上下滑動。這樣滑動十分順暢,但有個限制WebView必須使用使用loadData() 來載入文字內容形式的HTML資料。
然而,在Flutter中,使用AndroidView功能嵌入原生的WebView,須為WebView指定高度。不然高度會預設為0,看不到網頁(顯示不出來)。因此,在固定WebView高度的前提下, 要實現新聞頁面的整體滾動,就必須自己分發滾動事件, 在合適的時機讓應該滾動的控制元件滾動,達到整個頁面滾動的效果。
Flutter中的觸控事件也是按照Layout樹來冒泡傳遞的。查詢了一些相關程式碼和API文件,目前還沒有發現類似於Android中ViewGroup的事件攔截控制的方法(onInterceptTouchEvent); 只提供了事件的回撥方法和一些監聽。例如Listener widget, GestureDetector widget;
詳情參考API文件:https://flutter.io/docs/development/ui/advanced/gestures
調查Flutter中能滾動的控制元件(ListView, GridView, CustomScrollView)之後,發現他們都自動強制攔截了Move事件,子控制元件根本不能獲取到Move事件。但是Flutter也提供了設定禁止滾動的功能 (修改physics屬性,設定 NeverScrollableScrollPhysics),提供了滾動控制和回撥方式ScrollController。而且在禁止滾動之後,WebView就能夠順暢的滑動了。所以可以採用一種動態修改physics屬性的方式來實現整個頁面的滾動。
實現步驟:
- 網頁放在首頁,大小為鋪滿整個螢幕。先載入網頁。
2.列表等其他元件放在網頁下面。
3.先讓WebView滾動,即整體滾動先設定為NeverScrollableScrollPhysics,禁止滾動;當WebView滾動到底的時候設定為ScrollPhysics,開啟滾動。
4.當ScrollView滾動到頂部的時候再設定回NeverScrollableScrollPhysics,讓WebView滾動。
5.就是判斷時機的問題。目前加在觸控回撥Listener中。監聽onPointerMove和onPointerUp,在move事件中確定滾動方向, 在up事件時重新確定狀態,為下次事件準備。
程式碼實現:
1.整個佈局結構。(注意:CustomScrollView的sliver子項不要做多了,不然滑動到底部的時候會回收WebView,會丟失WebView的狀態。因此採用 SliverList 來載入其他的列表資料)
@override
Widget build(BuildContext context) {
var physics = _physics;
return new Scaffold(
appBar: AppBar(
title: Text('news details'),
),
body: Listener(
onPointerMove: (PointerMoveEvent event) {
moveToUp = event.delta.dy < 0;
},
onPointerUp: (PointerUpEvent event) {
resetScrollState();
//慣性滑動
Future.delayed(new Duration(milliseconds: 400), () {
resetScrollState();
});
},
child: CustomScrollView(
physics: physics,
slivers: <Widget>[
SliverToBoxAdapter(
child: htmlBodyWidget,
),
SliverList(delegate: new SliverChildListDelegate(widgetList))
],
controller: _scrollController,
),
),
floatingActionButton: IconButton(
icon: Icon(Icons.call),
color: Colors.yellow,
onPressed: () {
setState(() {
if (_physics is NeverScrollableScrollPhysics) {
_physics = new ScrollPhysics();
} else {
_physics = NeverScrollableScrollPhysics();
}
});
}),
);
}
2.滑動重新設定方法方法:
void resetScrollState() {
print("moveToUp ---- $moveToUp");
if (moveToUp) {
widget.detailsWeb.canScrollDown().then((value) {
print("moveToUp --- canScrollDown --- $value");
print('moveToUp ---- _physics === ${_physics.toString()}');
if (!value) {
if ((_physics is NeverScrollableScrollPhysics)) {
setState(() {
_physics = ScrollPhysics();
});
}
}
});
} else {
bool isScrollViewTop = _scrollController.offset <= 0;
print(
"moveToUp ---- isScrollViewTop = ${isScrollViewTop}");
if (isScrollViewTop) {
widget.detailsWeb.canScrollUp().then((value) {
print("moveToUp --- canScrollUp --- $value");
print('moveToUp ---- _physics === ${_physics.toString()}');
if (value) {
if (!(_physics is NeverScrollableScrollPhysics)) {
setState(() {
_physics = NeverScrollableScrollPhysics();
});
}
}
});
}
}
}
- 判斷CustomScrollView滾動到頂部方式:
bool isScrollViewTop = _scrollController.offset <= 0;
4.判斷WebView是否滾動到頂部和頂部要採用原生的判斷,所以必須是非同步返回。
widget.detailsWeb.canScrollUp() 返回WebView是否能向上滾動,
widget.detailsWeb.canScrollDown() 返回webView是否能向下滾動
Android 的判斷WebView還能不能滾動的原始碼:(參考下拉重新整理的開源庫)
public static boolean canChildScrollUp(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return view.getScrollY() > 0;
}
} else {
return view.canScrollVertically(-1);
}
}
public static boolean canChildScrollDown(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
return absListView.getChildCount() > 0
&& (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1
|| absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
} else if (view instanceof ScrollView) {
ScrollView scrollView = (ScrollView) view;
if (scrollView.getChildCount() == 0) {
return false;
} else {
return scrollView.getScrollY() < scrollView.getChildAt(0).getHeight() - scrollView.getHeight();
}
} else {
return false;
}
} else {
return view.canScrollVertically(1);
}
}
最終效果:
完整程式碼:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class NewsDetailsPage extends StatefulWidget {
final int nid;
DetailsWeb detailsWeb;
NewsDetailsPage(
int this.nid, {
Key key,
DetailsWeb this.detailsWeb,
}) : super(key: key);
@override
NewsDetailsPageState createState() {
return new NewsDetailsPageState();
}
}
abstract class DetailsWeb {
Widget createHtmlWidget(String body, List<Widget> pageWidgetContainer);
Future<bool> canScrollUp();
Future<bool> canScrollDown();
}
class NewsDetailsPageState extends State<NewsDetailsPage> {
List<Widget> widgetList = [];
Widget htmlBodyWidget;
@override
void initState() {
super.initState();
_getNewsDetails();
_init();
}
@override
void dispose() {
super.dispose();
}
void _getNewsDetails() async {
String url = 'http://www.wsrtv.com.cn/services/node/${widget.nid}.json';
var res = await http.get(url);
var resJson = json.decode(res.body);
try {
List<Widget> tempList = [];
String title = resJson['title'];
String titlehtml = '<h1>$title</h1>';
Padding titleWidget = new Padding(
padding: EdgeInsets.all(10.0),
child: new Text(
title,
style: TextStyle(
color: Colors.black,
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
);
int dateTimestamp = int.parse(resJson['changed']);
print('time ---- $dateTimestamp');
var dateTime = DateTime.fromMillisecondsSinceEpoch(dateTimestamp * 1000);
String date = '${dateTime.year}-${dateTime.month}-${dateTime.day}';
String count = resJson['totalcount'];
String dateTimeInfoStr =
'<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; color: rgb(0, 0, 0); font-size: 12px;"> $date 瀏覽量$count</p>';
Widget newsDateInfo = new IntrinsicHeight(
child: Padding(
padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
IntrinsicWidth(
child: Text(
date,
style: TextStyle(color: Colors.black, fontSize: 13.0),
),
),
Expanded(
child: new Padding(
padding: EdgeInsets.only(left: 20.0),
child: Text(
'瀏覽量$count',
style: TextStyle(color: Color(0xff999999), fontSize: 13.0),
),
),
),
],
),
),
);
Widget headerWidget = Container(
color: Colors.white,
child: new Column(
children: <Widget>[titleWidget, newsDateInfo],
),
);
// tempList.add(headerWidget);
// try {
// String videoUrl = resJson['field_news_video_app']['und'][0]['value'];
// print('videoUrl === $videoUrl');
// if (videoUrl != null && videoUrl.isNotEmpty) {
// final playerWidget = new Chewie(
// new VideoPlayerController.network(
// 'https://flutter.github.io/assets-for-api-docs/videos/butterfly.mp4'
// ),
// aspectRatio: 4 / 3,
// autoPlay: false,
// looping: false,
// );
// tempList.add(playerWidget);
// }
// } catch (e) {
// print('e === ${e.toString()}');
// }
String bodyValue = resJson['body']['und'][0]['value'];
String tempvalue = titlehtml + dateTimeInfoStr + bodyValue;
Widget bodyText = widget.detailsWeb == null
? Container()
: widget.detailsWeb.createHtmlWidget(tempvalue, widgetList);
htmlBodyWidget = bodyText;
// if (bodyText != null) {
// tempList.add(bodyText);
// }
for (int i = 0; i < 120; i++) {
tempList.add(AppBar(
title: Text('$i$i$i$i$i$i$i$i$i$i'),
));
}
setState(() {
widgetList = tempList;
});
} on Exception {}
}
ScrollController _scrollController;
bool _scrollAble = true;
ScrollPhysics _physics;
void _init() {
_scrollController = new ScrollController();
_physics = NeverScrollableScrollPhysics();
_scrollController.addListener(() {
print('_scrollController-------------${_scrollController.toString()}');
bool scrollAble = false;
if (scrollAble != _scrollAble) {
_scrollAble = scrollAble;
print('---------------------------$_scrollAble');
setState(() {
print('setState ----- scrollAbleController------');
// widgetList.add(AppBar(title: Text('aaaaaaaaaaaaa')));
// List<Widget> temp = <Widget>[];
// for (var item in widgetList) {
// temp.add(item);
// }
// widgetList = temp;
});
}
});
}
bool canToUp;
bool canToDown;
bool moveToUp = true;
@override
Widget build(BuildContext context) {
var physics = _physics;
return new Scaffold(
appBar: AppBar(
title: Text('news details'),
),
body: Listener(
onPointerMove: (PointerMoveEvent event) {
print(
'event web ---- up == ${widget.detailsWeb.canScrollUp()} ---- down =={${widget.detailsWeb.canScrollDown()}');
moveToUp = event.delta.dy < 0;
},
onPointerUp: (PointerUpEvent event) {
resetScrollState();
//慣性滑動
Future.delayed(new Duration(milliseconds: 400), () {
resetScrollState();
});
},
child: CustomScrollView(
physics: physics,
slivers: <Widget>[
SliverToBoxAdapter(
child: htmlBodyWidget,
),
SliverList(delegate: new SliverChildListDelegate(widgetList))
],
controller: _scrollController,
),
),
floatingActionButton: IconButton(
icon: Icon(Icons.call),
color: Colors.yellow,
onPressed: () {
setState(() {
if (_physics is NeverScrollableScrollPhysics) {
_physics = new ScrollPhysics();
} else {
_physics = NeverScrollableScrollPhysics();
}
});
}),
);
}
void resetScrollState() {
print("moveToUp ---- $moveToUp");
if (moveToUp) {
widget.detailsWeb.canScrollDown().then((value) {
print("moveToUp --- canScrollDown --- $value");
print('moveToUp ---- _physics === ${_physics.toString()}');
if (!value) {
if ((_physics is NeverScrollableScrollPhysics)) {
setState(() {
_physics = ScrollPhysics();
});
}
}
});
} else {
bool isScrollViewTop = _scrollController.offset <= 0;
print(
"moveToUp ---- isScrollViewTop = ${isScrollViewTop}");
if (isScrollViewTop) {
widget.detailsWeb.canScrollUp().then((value) {
print("moveToUp --- canScrollUp --- $value");
print('moveToUp ---- _physics === ${_physics.toString()}');
if (value) {
if (!(_physics is NeverScrollableScrollPhysics)) {
setState(() {
_physics = NeverScrollableScrollPhysics();
});
}
}
});
}
}
}
buildSlivers(List<Widget> list) {
if (list != null) {
Widget sliver = buildChildLayout(context, list);
return <Widget>[sliver];
}
return const <Widget>[];
}
Widget buildChildLayout(BuildContext context, List<Widget> children) {
return SliverList(
delegate: new SliverChildListDelegate(
children,
addAutomaticKeepAlives: true,
addRepaintBoundaries: true,
addSemanticIndexes: true,
));
}
Widget _detailsWidget(BuildContext context, int position) {
return widgetList[position];
}
}
網頁部分
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser_example/native_web_view.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as html;
import 'package:path_provider/path_provider.dart';
class NewsDetailsWeb extends StatefulWidget {
String body;
List<Widget> widgets;
NewsDetailsWebState state;
NewsDetailsWeb(
{Key key, @required String this.body, List<Widget> this.widgets})
: super(key: key);
@override
NewsDetailsWebState createState() {
state = NewsDetailsWebState();
return state;
}
Future<bool> canScrollUp() async {
return state?.canScrollUp();
}
Future<bool> canScrollDown() async {
return state?.canScrollDown();
}
}
class NewsDetailsWebState extends State<NewsDetailsWeb> {
final String fileName = 'wenshan_details.html';
final String fileCssName = 'wenshan_details_css.css';
String _webUrl = '';
double top = 156.899;
@override
void initState() {
super.initState();
_createHtmlContent();
}
void _createHtmlContent() async {
String cssUrl = (await _getLocalCssFile()).uri.toString();
String cssHead =
'''<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
<link rel="stylesheet" type="text/css" href="${cssUrl}" />''';
String newHtml = cssHead + widget.body;
dom.Document doc = html.parse(newHtml);
String htmlContent = doc.outerHtml;
print('htmlContent === $htmlContent');
_writeDataFile(htmlContent);
}
void _checkCssFile() async {
File file = await _getLocalCssFile();
bool isExist = await file.exists();
int fileLength = isExist ? await file.length() : -1;
print('csss file length === $fileLength');
if (!isExist || fileLength <= 0) {
if (isExist) {
await file.delete();
}
await file.create();
String cssStr = await DefaultAssetBundle.of(context)
.loadString('assets/css/main.css');
print('csss ==== $cssStr');
await file.writeAsString(cssStr);
}
}
void _writeDataFile(String data) async {
_checkCssFile();
File file = await _getLocalHtmlFile();
File afterFile = await file.writeAsString(data);
setState(() {
_webUrl = afterFile.uri.toString();
});
print('weburl ==== $_webUrl');
}
Future<File> _getLocalCssFile() async {
// 獲取本地文件目錄
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地檔案目錄
return new File('$dir/$fileCssName');
}
Future<File> _getLocalHtmlFile() async {
// 獲取本地文件目錄
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地檔案目錄
return new File('$dir/$fileName');
}
@override
Widget build(BuildContext context) {
return getNativeWeb();
}
NativeWebView webView;
Widget getNativeWeb() {
webView = _webUrl.isNotEmpty
? NativeWebView(
webUrl: _webUrl,
webRect: Rect.fromLTWH(
0.0,
0.0,
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height -
AppBar().preferredSize.height -
MediaQuery.of(context).padding.top),
)
: null;
return _webUrl.isNotEmpty
? webView
: new Container(
height: 300.0,
color: Colors.yellow,
);
}
Future<bool> canScrollUp() async {
return webView?.canScrollUp();
}
Future<bool> canScrollDown() async {
return webView?.canScrollDown();
}
}
//WebView 外掛使用flutter_inappbrowser
import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';
class NativeWebView extends StatelessWidget {
String webUrl;
final Rect webRect;
InAppWebViewController webView;
NativeWebView({Key key, this.webUrl, this.webRect}) : super(key: key);
@override
Widget build(BuildContext context) {
InAppWebView webWidget = new InAppWebView(
initialUrl: webUrl,
initialHeaders: {},
initialOptions: {},
onWebViewCreated: (InAppWebViewController controller) {
webView = controller;
},
onLoadStart: (InAppWebViewController controller, String url) {
print("started -------------- $url");
this.webUrl = url;
},
onProgressChanged: (InAppWebViewController controller, int progress) {
double prog = progress / 100;
print('prog --------- $prog');
});
return Container(
width: webRect.width,
height: webRect.height,
child: webWidget,
);
}
Future<bool> canScrollUp() async {
if(webView != null) {
print('webView up ---- ${ await webView.canScrollUp()}');
}
return webView == null ? false : webView.canScrollUp();
}
Future<bool> canScrollDown() async{
if(webView != null) {
print('webView down ---- ${await webView.canScrollDown()}');
}
return webView == null ? false : webView.canScrollDown();
}
}
呼叫:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';
import 'package:flutter_inappbrowser_example/news_web_page.dart';
import 'package:flutter_inappbrowser_example/news_web_use.dart';
Future main() async {
runApp(new TestApp());
}
class TestHomeScreen extends StatelessWidget{
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
),
body: new Center(child: new Text("Click Me")),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add),
backgroundColor: Colors.orange,
onPressed: () {
print("Clicked");
Navigator.push(context, new MaterialPageRoute(builder: (context) {
return new NewsDetailsPage(
25266.toInt(),
detailsWeb: new DetailWebUse(),
);
}));
},
),
);
}
}
class TestApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: TestHomeScreen(),
);
}
}
class DetailWebUse implements DetailsWeb {
NewsDetailsWeb web;
@override
Future<bool> canScrollDown() {
return web?.canScrollDown();
}
@override
Future<bool> canScrollUp() {
return web?.canScrollUp();
}
@override
Widget createHtmlWidget(String body, List<Widget> pageWidgetContainer) {
web = new NewsDetailsWeb(
body: body,
widgets: pageWidgetContainer,
);
return web;
}
}
相關文章
- jQuery新聞列表垂直滾動詳解jQuery
- 詳情頁返回列表頁恢復滾動位置
- jQuery具有暫停功效果新聞列表向上滾動jQuery
- android之豎直滾動控制元件-ListViewAndroid控制元件View
- 具有間歇效果的新聞列表垂直滾動程式碼
- Flutter 新聞客戶端 - 09 詳情頁展示、分享、遠端真機除錯Flutter客戶端除錯
- JavaScript垂直新聞公告無縫滾動詳解JavaScript
- 直播系統原始碼,圖片一直滾動,迴圈滾動,豎向和橫向原始碼
- Flutter 頁面滾動吸頂詳解(NestedScrollView)FlutterView
- jQuery單行新聞標題向上滾動詳解jQuery
- 使用 flutter 的ListView實現滾動列表FlutterView
- 列表元件抽象(4):滾動列表及分頁說明元件抽象
- Flutter SingleChildScrollView 滾動頁面FlutterView
- 論移動裝置內容的橫向滾動和豎向滾動
- JavaScript 垂直新聞公告無縫滾動JavaScript
- flutter_web 實戰之文章列表與詳情FlutterWeb
- Flutter版本玩Android(3)——文章詳情頁FlutterAndroid
- flutter實戰3:解析HTTP請求資料和製作新聞分類列表FlutterHTTP
- flutter實戰4:新聞列表的懶載入和下拉手勢重新整理Flutter
- HBuilder開發詞典app(三)--主頁圖文輪播和新聞列表UIAPP
- 主頁和四個詳情頁成功
- Flutter Webview網頁與App通訊FlutterWebView網頁APP
- Flutter滾動動畫Flutter動畫
- 單行新聞公告間歇垂直無縫滾動
- ios自定義類似網易新聞的滾動條iOS
- 安卓開發——WebView+Recyclerview文章詳情頁,解決高度問題安卓WebView
- NOW直播Flutter動態搜尋列表頁實現Flutter
- Nodejs爬取新聞列表NodeJS
- Flutter整合舊專案並重構帖子詳情頁Flutter
- 新聞公告具有時間間隔垂直滾動程式碼
- Bootstrap列表新增滾動條boot
- 帝國CMS列表頁模板新聞關鍵詞帶連結呼叫
- Flutter中scroll_to_index 實現列表滾動到指定索引的庫FlutterIndex索引
- Flutter 新聞客戶端 - 06 程式碼規範、業務程式碼組織、新聞首頁實現Flutter客戶端
- Silverlight之ScrollViewer控制元件的水平和豎直滾動條位置控制View控制元件
- !!!網頁詳情頁成功!!!網頁
- css去掉新聞列表最後一個新聞的下劃線CSS
- 移動新聞網站,掌上移動新聞,移動新聞客戶端,jQuery Mobile移動新聞網站,移動新聞網站demo,新聞閱讀器開發網站客戶端jQuery