Flutter iOS 点击状态栏回到顶部

一、会回到顶部的原理

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,
        ),
      ),
    );

End

你可能感兴趣的:(Flutter iOS 点击状态栏回到顶部)