[toc]
Flutter iOS 点击状态栏回到顶部 一、会回到顶部的原理 ios中一个常见的交互是:点击顶部栏时,自动将当前的滚动区滚到顶部。在flutter中,大部分时候这件事是“自然完成”的,但是也有时候会遇到这个行为失效的情况。要解决这个问题首先自然是要看这个feature是如何实现的。 其实大部分都是Scaffold里面干的事: Scaffold里有这样一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 switch (themeData.platform) { case TargetPlatform.iOS: _addIfNonNull( children, GestureDetector( behavior: HitTestBehavior.opaque, onTap: _handleStatusBarTap, excludeFromSemantics: true , ), _ScaffoldSlot.statusBar, removeLeftPadding: false , removeTopPadding: true , removeRightPadding: false , removeBottomPadding: true , ); break ; case TargetPlatform.android: case TargetPlatform.fuchsia: break ; }
这里命名很舒服,可以直接看出来在干什么:如果是ios的话,那就给Scaffold加一个在状态栏上的点击区,点击的话就会触发一个函数,这个函数干的事情如下:
1 2 3 4 5 6 7 8 9 10 11 final ScrollController _primaryScrollController = ScrollController();void _handleStatusBarTap () { if (_primaryScrollController.hasClients) { _primaryScrollController.animateTo( 0.0 , duration: const Duration (milliseconds: 300 ) , curve: Curves.linear, ) ; } }
也就是,Scaffold会提供一个默认的ScrollController,而点击顶部栏会使得这个controller滚到顶部,在ScrollView的build函数中则会取这个controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @override Widget build(BuildContext context) { final List<Widget> slivers = buildSlivers(context); final AxisDirection axisDirection = getDirection(context); 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 上的状态栏返回顶部来举例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import 'package:flutter/material.dart' ;class TapStatusNormalPage extends StatefulWidget { const TapStatusNormalPage({Key key}) : super (key: key); @override State<TapStatusNormalPage> createState() => _TapStatusNormalPageState(); } class _TapStatusNormalPageState extends State <TapStatusNormalPage > { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('状态栏点击-Normal5' ), ), body: ListView.builder( itemCount: 200 , itemBuilder: (context, index) { return Text('$index'); }, ), ); } }
错误一:如果Scaffold里面又套了一个Scaffold,那么这个回到顶部就会失效。 失效示例1:
1 2 3 4 5 6 7 8 9 10 import 'package:flutter/material.dart' ; class HomePage extends StatelessWidget { @override Widget build (BuildContext context ) { return Scaffold( body: TapStatusNormalPage(), ); } }
这种情况,点击状态栏便不会回到顶部,我们需要保证的就是每个页面仅有一个Scaffold。
失效示例2:
虽然home里只有一个 Scaffold ,但app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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: widget, ); }), ), );
错误三:一说要滚动就自定义controller 点击页面上的某个按钮,让页面上的列表滚动到顶部(不需要自定义controller)。
1 2 3 4 5 6 7 8 PrimaryScrollController.of(context).jumpTo(0 ); PrimaryScrollController.of(context).animateTo( 0 , duration: const Duration (milliseconds: 300 ), curve: Curves.linear, );
如果还要监听滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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; }
问题:
flutter_swiper:Another exception was thrown: ScrollController attached to multiple scroll views
翻译一下:引发了另一个异常:ScrollController连接到多个滚动视图。
原因:
Flutter Swiper是一个轮播图组件,内部包含一个Widget List,当这个Widget List数量大于1,就可能会有这种情况
解决方案:给Swiper加一个Key即可解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 return Container ( child : AspectRatio( aspectRatio : 1.5 / 1 , 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, ), ), );
End