最近用flutter做了一个谈论弹窗的功用,原本以为很简单的烂大街的一个功用,成果却遇到了不少的问题,而且这些问题我觉得很有意义,以致于我觉得我假如共享出来或许会对其他人很有协助。

要做一件工作或许会很简单,但做好一件工作却很难~

常见产品

大略的截了一些图:抖音、豆瓣、知乎、西红柿小说

这些产品的谈论功用基本都是这种弹窗或许说是滑动面板的模式:

抖音/豆瓣

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践
Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

知乎/西红柿小说

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践
Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

交互细节

当仔细的体会了这几款产品后,发现有这些特点:

列表与下拉手势联动 面板顶部下拉手势 检查更多回复 弹起键盘
抖音 支撑 支撑 点击翻开 翻滚定位
豆瓣 支撑 支撑 点击翻开 谈论框顶部显现内容
知乎 支撑 不支撑 跳转新页面 无处理
西红柿小说 支撑 支撑 跳转新页面 翻滚定位

大约梳理总结这么几点:

  1. 这个弹窗并不是一个简单的dialog,其实是一个带有手势交互的滑动面板
  2. 手势向上能够正常滑动列表,手势向下滑动列表到顶部后可触发下拉手势封闭弹窗
  3. 即使列表向上翻滚一段距离后,仍然能够滑动顶部触发下拉手势封闭弹窗
  4. 点击列表某一项弹起键盘后,遍及是将要回复的那一条定位到谈论框的上方
  5. 检查更多回复,抖音/豆瓣是直接在当前页面翻开,知乎/西红柿小说是一个跳转到新页面

我相信这几款产品基本都是用android原生去写的,那么flutter是否完成和它们相同的用户体会呢?

其实我最初想到的便是用showModalBottomSheet去从底部弹一个窗出来,但是BottomSheet本身并没有处理与列表的滑动交互问题,假如凑合着其实也还行,便是缺少了下拉手势的交互。

所以我在flutter社区找到了sliding_up_panel,这是一个很受欢迎的库,我计划依据这个库来完成谈论功用

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

至于完成作用,最终是选择了西红柿小说的交互作用:

列表与下拉手势联动 面板顶部下拉手势 检查更多回复 弹起键盘
西红柿小说 支撑 支撑 跳转新页面 翻滚定位

完成作用

那些基本的代码就不细讲了,一顿操作下来,基本功用成型:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

接下来说的难点处理暂时也不贴详细的源码了,以讲思路为主,部分是实在的,部分是伪代码

最终的源码我会贴到文章末尾~

难点处理

1. 面板中存在多个列表时,下拉手势异常(部分路由存在多个页面引起)

看一下官方文档的说明:

Properties Description
panelBuilder [beta] NOTE: This feature is still in beta and may have some problems. Please open an issue on GitHub if you encounter something unexpected.Provides a ScrollController to attach to a scrollable object in the panel that links the panel position with the scroll position. Useful for implementing an infinite scroll behavior. If panel and panelBuilder are both non-null, panel will be used.

panelBuilder会回调一个ScrollController,ListView能跟面板手势衔接的原因便是依据ScrollController的offset来判别哪个时机能翻滚面板

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

现在咱们有一个列表页,点击列表项还能跳转到一个详情页,两个页面都存在ListView,那么现在在panelBuilder只供给一个ScrollController的状况下,只能做到在列表页与面板有手势的联动,跳转到详情页后将无法联动面板,由于ScrollController的offset办法只适用于ScrollController仅仅attach一个翻滚视图的状况

/// Returns the attached [ScrollPosition], from which the actual scroll offset
/// of the [ScrollView] can be obtained.
///
/// Calling this is only valid when only a single position is attached.
ScrollPosition get position {
  assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
  assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
  return _positions.single;
}
/// The current scroll offset of the scrollable widget.
///
/// Requires the controller to be controlling exactly one scrollable widget.
double get offset => position.pixels;

为此我提了一个issue: github.com/akshathjain…

但是这个库的作者良久都没有更新了,只能自己想办法了

处理问题

咱们能够发现这个ScrollController是在sliding_up_panel内部创建的:

// prevent the panel content from being scrolled only if the widget is
// draggable and panel scrolling is enabled
_sc = new ScrollController();
_sc.addListener(() {
  if (widget.isDraggable && !_scrollingEnabled) _sc.jumpTo(0);
});

那么咱们实际上能够扩展sliding_up_panel的PanelController,使他具有能够set ScrollController的才能

完成办法:

在panelState中增加ScrollController的set办法

然后在PanelController增加一个setScrollController的办法:

 void setScrollController(ScrollController sc) {
   _panelState!.sc = sc;
 }

在列表页要向详情页跳转的时分,将面板的ScrollController变为详情页的ScrollController

当从详情页退出到列表页的时分,再将面板的ScrollController变回列表页的ScrollController

大约是这样的伪代码:

double offset = listController?.offset ?? 0;
panelController.setScrollController(listScrollController);
Navigator.push(...).then((value) {
  panelController.setScrollController(detailScrollController);
  listController?.jumpTo(offset);
});

作用

不管是在谈论列表页仍是在谈论详情页,列表都能很好的和面板手势交互衔接起来,契合预期~

2. 当列表向上滑动一定距离后,向下拉TopBar的方位无法呼应手势

详细的表现为:列表不在初始方位时,红框部分下拉无呼应

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

其实这是预期的行为,由于sliding_up_panel并没有供给这方面的才能

同样我又提了一个issue: github.com/akshathjain…

同样的成果,这个库的作者良久都没有更新了,只能自己想办法了

处理问题

要想让外部的widget也能像面板内部相同去呼应手势,咱们得知道源码是怎样完成的

经过阅读源码发现,手势滑动面板的代码主要在这里:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

那么咱们能够把这一部分代码独自封装成一个办法,经过PanelController对外供给出去

然后再对TopBar做一个手势监听,监听的一同去调用这个办法,那么就能够让TopBar去呼应手势了

PanelController新增的代码:

 void onChildWidgetPointerMove(PointerMoveEvent p) {
   _panelState!.onChildWidgetPointerMove(p);
 }

在TopBar的外部包裹Listener组件,对手势进行呼应:

Listener(
    behavior: HitTestBehavior.opaque,
    onPointerMove: (p) => panelController.onChildWidgetPointerMove(p),
    child:TopBar()
)

作用

在TopBar上加上手势后,即使列表不在初始方位,TopBar仍旧能够呼应下拉手势,契合预期~

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

3.点击列表某一条弹起键盘,将要回复的那一条定位到谈论框的上方

关于这个功用,我选择找一个开源库来完成:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

在监听键盘弹起的时分,调用翻滚办法:

  @override
  void didChangeMetrics() {
    // 回复他人的时分翻滚调整,推迟300毫秒滑动动画更天然
    if (index != -1) {
      Future.delayed(Duration(milliseconds: 300), () {
        scrollController?.scrollToIndex(index);
      });
    }
    super.didChangeMetrics();
  }

详细的完成代码就不在这里贴了,原本以为很顺利就能够写完的一个功用

成果又呈现了问题:

在谈论列表没有翻滚过的状况下,点击列表某一条弹起键盘,列表不会定位到那一项

哪怕略微滑动一点点,再去点击弹起键盘,都能够直接定位

也便是说sliding_up_panel这个库或许有点问题

在面板初始方位调用ScrollController的任何jumpTo或许animateTo办法都是无效的!

纳闷了半响,怎样回事?

所以又提了一个issue: github.com/akshathjain…

同样的成果,这个库的作者良久都没有更新了,只能自己想办法了

处理问题

看看源码:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

在履行初始化办法的时分 ,给ScrollController注册了一个监听

当面板处于可翻滚的状况而且_scrollingEnabled为false的时分,会直接翻滚到0的方位

而这个_scrollingEnabled是一个私有变量,而且初始值便是false,这个值会在面板翻滚的时分调用onGestureSlide办法然后被从头赋值。

这便是为什么在刚刚翻开面板时调用ScrollController的翻滚办法不起作用的原因了,它会一直jumpTo(0)

所以我做了一个对其他代码影响规模最小的改动点:改动私有变量_scrollingEnabled的初始值

由所以私有变量,所以仍是经过PanelController来注入的办法改动:

  void setScrollEnable(bool enable) {
    _panelState!.scrollingEnabled = enable;
  }

在调用翻滚办法前将_scrollEnable的值改为true

  @override
  void didChangeMetrics() {
    // 回复他人的时分翻滚调整,推迟300毫秒滑动动画更天然
    if (index != -1) {
      Future.delayed(Duration(milliseconds: 300), () {
        panelController.setScrollEnable(true);
        scrollController?.scrollToIndex(index);
      });
    }
    super.didChangeMetrics();
  }

作用

不管在面板的初始方位,仍是列表翻滚一些距离,在键盘弹起后,列表都能翻滚定位到预期方位,契合预期~

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

4.键盘弹起以后,整个面板仍然会呼应手势

这个问题详细的表现是这样的,有时分用户会下拉文本框,会连同面板一同下拉,体会不太友爱:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

这个问题不是很难,但是是一个细节问题

处理方案

在键盘弹起的时分禁用面板的拖拽功用,键盘消失的时分再恢复面板的拖拽功用

伪代码:

//  面板
SlidingUpPanel(
    xxx: 0,
    xxx: 0,
    xxx: false,
    xxx: xxx,
    isDraggable: isDraggable,
);
/// 是否显现编辑框
void setShowInput(bool isShow) {
    isDraggable = !isShow;
    isShowTextField = isShow;
    if (mounted) setState(() {});
}

作用

键盘弹起以后,谈论面板不行再拖动,契合预期~

5.按手机回来键直接退出了页面,没有回来到部分路由或许退出面板

这个也是一个预期的问题,由于咱们并没有做特别处理

由于有部分路由的原因,所以咱们应该:

  1. 面板没呈现按回来键,退出页面
  2. 在列表页按回来键,应该退出面板
  3. 在详情页按回来键,应该退出到列表页

处理方案

直接上一些伪代码:

在谈论详情页的build办法中给context变量赋值

currentNavigatorContext = context;

给内容页面增加WillPopScope

Future<bool> _willPanelPop() async {
    // 存在详情页部分路由弹出
    if (currentNavigatorContext != null) {
      Navigator.popUntil(currentNavigatorContext!, (route) => route.isFirst);
      return false;
    }
    // 谈论面板是翻开的状况,那么封闭它
    if (panelController.isPanelOpen) {
      panelController.close();
      return false;
    }
    return true;
}

作用

现在作用契合用户的运用习惯, 契合预期~

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

6.ios和部分android手机侧滑与下拉手势抵触

由于ios手机自带侧滑手势回来,包含手机物理回来键不是侧滑的android手机,都有这个问题:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

这个是不太能接受的,用户本想侧滑回来,但是略微往下一拉就把面板拉下来了,这体会很差劲

看了一下西红柿小说,是侧滑的时分不允许下拉,看到他人能做,我觉得咱们也能够~

处理方案

留意看咱们的flutter的NavigatorObserver类的这几个办法

箭头标注的办法它们的调用时机是在:侧滑开端、侧滑撤销、页面弹出

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

思路便是在开端侧滑的时分禁止面板的滑动,撤销和弹出的时分再恢复面板的滑动

直接上伪代码:

class CommentDetailNavigator extends NavigatorObserver {
  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    isDraggable = true;
  }
  @override
  void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) {
    isDraggable = false;
    panelController.panelPosition = 1.0;
  }
  @override
  void didStopUserGesture() {
    isDraggable = true;
  }
}

这个代码试了一下如同并不管用,改动了isDraggable需要从头setState页面,其中或许存在什么问题

研究一下源码:

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

我认为问题是在手势没抬起之前,就算从头build了以后,之前的手势其实仍然收效

也便是说仍旧会调用滑动中_onGestureSlide滑动完毕_onGestureEnd的办法

那么处理办法就来了:

滑动中_onGestureSlide滑动完毕_onGestureEnd办法中增加这一行:

if (!widget.isDraggable) return;

这个但是实时收效的,手势滑动办法中直接return这指定滑不了

该库的作者不重复加这个判别估计也是由于认定了isDraggable为false的时分直接回来的是child对象,不会和手势有任何关系了

可谁曾想还有我现在遇到的这种场景。。。

最后留意:还需要在详情页的TopBar手势监听那里增加判别拦截,不然侧滑TopBar的方位仍旧会将面板拉下来

if (!isDraggable) return;

作用

横向侧滑的时分,并不会把面板下拉下来,契合预期~

Flutter 仿抖音、豆瓣、知乎、番茄小说的评论弹窗开发实践

总结

完成一个发谈论看谈论的功用其实很简单, 但是能把这一个个细节问题都处理仍是挺困难的

我大约完善了如下细节:

  1. 列表和面板手势的滑动衔接
  2. 存在部分路由时,多个列表和面板手势的滑动衔接
  3. 面板顶部TopBar的手势监听
  4. 键盘弹起将对应的列表项定位到谈论框上方
  5. 键盘弹起后,对面板手势的禁用,避免用户误触
  6. 监听手机侧滑回来,使其契合咱们的预期
  7. 处理ios侧滑手势与下拉手势的抵触

项目地址

github.com/pengboboer/…

简单运用了provider完成了难点问题(不包含事务及逻辑代码)

改造后的sliding_up_panel:

在该项目sliding_up_panel目录下, 可查找pengboboer add检查改动点

其他

这篇文章写了良久,假如能帮到你们,期望给个点赞和star~

你们的鼓励是对我最大的必定~