一、会回到顶部的原理
ios中一个常见的交互是:点击顶部栏时,自动将当前的滚动区滚到顶部。在flutter中,大部分时候这件事是“自然完成”的,但是也有时候会遇到这个行为失效的情况。要解决这个问题首先自然是要看这个feature是如何实现的。
其实大部分都是Scaffold里面干的事:
Scaffold里有这样一段代码:
switch (themeData.platform) {
case TargetPlatform.iOS:
_addIfNonNull(
children,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleStatusBarTap,
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar
excludeFromSemantics: true,
),
_ScaffoldSlot.statusBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: true,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
break;
}
这里命名很舒服,可以直接看出来在干什么:如果是ios的话,那就给Scaffold加一个在状态栏上的点击区,点击的话就会触发一个函数,这个函数干的事情如下:
final ScrollController _primaryScrollController = ScrollController();
void _handleStatusBarTap() {
if (_primaryScrollController.hasClients) {
_primaryScrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
);
}
}
也就是,Scaffold会提供一个默认的ScrollController,而点击顶部栏会使得这个controller滚到顶部,在ScrollView的build函数中则会取这个controller:
@override
Widget build(BuildContext context) {
final List slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
// 注意,此处的primary不是传入的primary,
// primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical)
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
return primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
如果指定controller的话,就不会使用PrimaryScrollview,如果不指定的话,则在primary为true时使用PrimaryController,而默认情况下controller为null,primary为true,因此一个裸体的ListView是会相应屏幕点击的。
知道了原理,就很容易分析自己代码里出的问题是什么,常见的可能就是:
1、没加Scaffold,这个其实并不常见(自相矛盾草),不过可以检查一下,一般总是会有Scaffold的
2、真正常见的:指定了controller,如果自己创建了一个Controller丢给ScrollView,那必然是会失效的。但是使用controller又是一个很常见且重要的需求,怎么办呢?也很简单,就是不要自己创建新的ScrollController,而是直接取PrimaryScrollController.of(context)这个controller,对其进行自己要做的操作。
3、相对不太常见且需要分析具体代码的:多个Scaffold导致的冲突。
注意到其实flutter里的这个点击状态栏并不是真的点击了状态栏,而是点击了“Scaffold提供的位于状态栏的可点击区域”,也就是说,如果有多个Scaffold就会有多个这样的区域。实际情况是,只有最内部的Scaffold的状态栏会有响应,而如果ScrollView所处位置取到的和点击的Scaffold不一致,自然也就不会有滚动到顶部的feature
二、错误示例
今天遇到ios点击状态栏无法回到顶部(原理在文章后)的问题。研究后发现,Scaffold组件虽然会自带这个功能。但使用时候,必须遵循指定规则才行。
我们以点击 TapStatusNormalPage 上的状态栏返回顶部来举例。
// tap_status_normal_page.dart
import 'package:flutter/material.dart';
class TapStatusNormalPage extends StatefulWidget {
const TapStatusNormalPage({Key key}) : super(key: key);
@override
State createState() => _TapStatusNormalPageState();
}
class _TapStatusNormalPageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold( // 注意这个页面已经包了 Scaffold 了
appBar: AppBar(
title: const Text('状态栏点击-Normal5'),
),
body: ListView.builder(
itemCount: 200,
itemBuilder: (context, index) {
return Text('$index');
},
),
);
}
}
错误一:如果Scaffold里面又套了一个Scaffold,那么这个回到顶部就会失效。
失效示例1:
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: TapStatusNormalPage(), // 失效原因:TapStatusNormalPage里已经有 Scaffold 了
);
}
}
这种情况,点击状态栏便不会回到顶部,我们需要保证的就是每个页面仅有一个Scaffold。
错误二:app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部
失效示例2:
虽然home里只有一个 Scaffold ,但app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: () => MaterialApp(
navigatorKey: navigatorKey,
title: 'wish',
home: TapStatusNormalPage(),
builder: EasyLoading.init(builder: (context, widget) {
return MediaQuery(
///设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
// child: Scaffold( // 注意:app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部
// resizeToAvoidBottomInset: false,
// body: widget,
// ),
child: widget, // OK
);
}),
),
);
错误三:一说要滚动就自定义controller
点击页面上的某个按钮,让页面上的列表滚动到顶部(不需要自定义controller)。
PrimaryScrollController.of(context).jumpTo(0);
PrimaryScrollController.of(context).animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
);
如果还要监听滚动
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_hasEverInitListener) {
PrimaryScrollController.of(context).addListener(_handleScrollViewEvent);
_hasEverInitListener = true;
}
}
@override
void deactivate() {
super.deactivate();
PrimaryScrollController.of(context).removeListener(_handleScrollViewEvent);
}
_handleScrollViewEvent() {
// 滚动距离
double offsetY = PrimaryScrollController.of(context).offset;
}
错误四:Another exception was thrown: ScrollController attached to multiple scroll views.
问题:
flutter_swiper:Another exception was thrown: ScrollController attached to multiple scroll views
翻译一下:引发了另一个异常:ScrollController连接到多个滚动视图。
原因:
Flutter Swiper是一个轮播图组件,内部包含一个Widget List,当这个Widget List数量大于1,就可能会有这种情况
解决方案:给Swiper加一个Key即可解决
return Container(
child: AspectRatio(
aspectRatio: 1.5 / 1, // 宽高比450/300
child: Swiper(
key: UniqueKey(), // 这个必须添加,代表唯一
itemBuilder: (BuildContext context, int index) {
return new Image.network(
imgList[index]['url'],
fit: BoxFit.fill,
);
},
itemCount: imgList.length,
pagination: new SwiperPagination(),
control: new SwiperControl(),
autoplay: true,
),
),
);