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

最近用flutter做了一个评论弹窗的功能,本来以为很简单的烂大街的一个功能,结果却遇到了不少的问题,而且这些问题我觉得很有意义,以至于我觉得我如果分享出来可能会对其他人很有帮助。

要做一件事情可能会很容易,但做好一件事情却很难~

常见产品

粗略的截了一些图:抖音、豆瓣、知乎、番茄小说

这些产品的评论功能基本都是这种弹窗或者说是滑动面板的模式:

抖音/豆瓣

知乎/番茄小说

交互细节

当认真的体验了这几款产品后,发现有这些特点:

列表与下拉手势联动面板顶部下拉手势查看更多回复弹起键盘
抖音支持支持点击展开滚动定位
豆瓣支持支持点击展开评论框顶部显示内容
知乎支持不支持跳转新页面无处理
番茄小说支持支持跳转新页面滚动定位

大概梳理总结这么几点:

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

我相信这几款产品基本都是用android原生去写的,那么flutter是否实现和它们一样的用户体验呢?

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

所以我在flutter社区找到了sliding_up_panel,这是一个很受欢迎的库,我打算基于这个库来实现评论功能

至于实现效果,最终是选择了番茄小说的交互效果:

列表与下拉手势联动面板顶部下拉手势查看更多回复弹起键盘
番茄小说支持支持跳转新页面滚动定位

实现效果

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

接下来说的难点解决暂时也不贴具体的源码了,以讲思路为主,部分是真实的,部分是伪代码

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

难点解决

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

看一下官方文档的说明:

PropertiesDescription
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来判断哪个时机能滚动面板

目前我们有一个列表页,点击列表项还能跳转到一个详情页,两个页面都存在ListView,那么目前在panelBuilder只提供一个ScrollController的情况下,只能做到在列表页与面板有手势的联动,跳转到详情页后将无法联动面板,因为一个ScrollController只能用在一个ListView上

为此我提了一个issue: https://github.com/akshathjain/sliding_up_panel/issues/324

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

解决问题

我们可以发现这个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的位置无法响应手势

具体的表现为:列表不在初始位置时,红框部分下拉无响应

其实这是预期的行为,因为sliding_up_panel并没有提供这方面的能力

同样我又提了一个issue: https://github.com/akshathjain/sliding_up_panel/issues/329

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

解决问题

要想让外部的widget也能像面板内部一样去响应手势,我们得知道源码是怎么实现的

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

那么我们可以把这一部分代码单独封装成一个方法,通过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依旧可以响应下拉手势,符合预期~

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

关于这个功能,我选择找一个开源库来实现:

在监听键盘弹起的时候,调用滚动方法:

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

具体的实现代码就不在这里贴了,本来以为很顺利就可以写完的一个功能

结果又出现了问题:

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

哪怕稍微滑动一点点,再去点击弹起键盘,都可以直接定位

也就是说sliding_up_panel这个库可能有点问题

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

纳闷了半天,怎么回事?

于是又提了一个issue: https://github.com/akshathjain/sliding_up_panel/issues/330

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

解决问题

看看源码:

在执行初始化方法的时候 ,给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();
  }

效果

无论在面板的初始位置,还是列表滚动一些距离,在键盘弹起后,列表都能滚动定位到预期位置,符合预期~

### 4.键盘弹起以后,整个面板仍然会响应手势 这个问题具体的表现是这样的,有时候用户会下拉文本框,会连同面板一起下拉,体验不太友好:

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

解决方案

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

伪代码:

//  面板
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;
}

效果

目前效果符合用户的使用习惯, 符合预期~

6.ios和部分android手机侧滑与下拉手势冲突

因为ios手机自带侧滑手势返回,包括手机物理返回键不是侧滑的android手机,都有这个问题:

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

看了一下番茄小说,是侧滑的时候不允许下拉,看到别人能做,我觉得我们也可以~

解决方案

注意看我们的flutter的NavigatorObserver类的这几个方法

箭头标注的方法它们的调用时机是在:侧滑开始、侧滑取消、页面弹出

思路就是在开始侧滑的时候禁止面板的滑动,取消和弹出的时候再恢复面板的滑动

直接上伪代码:

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页面,其中可能存在什么问题

研究一下源码:

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

也就是说依旧会调用滑动中_onGestureSlide滑动结束_onGestureEnd的方法

那么解决方法就来了:

滑动中_onGestureSlide滑动结束_onGestureEnd方法中添加这一行:

if (!widget.isDraggable) return;

这个可是实时生效的,手势滑动方法中直接return这指定滑不了

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

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

最后注意:还需要在详情页的TopBar手势监听那里添加判断拦截,不然侧滑TopBar的位置依旧会将面板拉下来

if (!isDraggable) return;

效果

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

总结

实现一个发评论看评论的功能其实很简单, 但是能把这一个个细节问题都解决还是挺困难的

我大概完善了如下细节:

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

项目地址

https://github.com/pengboboer/flutter_comment_panel_example

简单使用了provider实现了难点问题(不包含业务及逻辑代码)

改造后的sliding_up_panel:

在该项目sliding_up_panel目录下, 可搜索pengboboer add查看改动点

其他

这篇文章写了好久,如果能帮到你们,希望给个点赞和star~

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/37464.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot之配置文件加载

springboot启动流程参考。Springboot总结。本内容主要解析里面的配置文件的加载过程。 springboot资源加载 入口。SpringApplication#run 我们知道&#xff0c;run方法是构建容器的过程。里面有一个方法&#xff1a;prepareEnvironment。用于构建环境组件Environment&#xf…

cocos2d-js中jsc逆向为js

1.下载脚本https://github.com/tablis/jsc-decompile-mozjs-34 2.安装php7以上的版本 ubuntu $ sudo apt install php7.0 mac $ brew install php7.0 windows just google an binary one 查看php安装的版本这里mac电脑为例子: 输入:php -v 只要7以上的版本即可 3.cd到…

http协议(二)

欢迎来到南方有乔木的博客&#xff01;&#xff01;&#xff01; 博主主页&#xff1a;点击点击&#xff01;戳一戳&#xff01;&#xff01; 博主名:南方有乔木呀 博主简介&#xff1a; 一名在校大学生&#xff0c;正在努力学习Java语言编程。穷且意坚&#xff0c;不坠青云…

Go语言基础教程:变量、基本数据类型、输出、注释、运算符、if-else条件判断、函数

文章目录 一、变量的使用1.1 定义变量1.2 常量1.3 变量的赋值与内存相关 二、变量基本类型2.1 有符号整型2.2 无符号整型2.3 string类型2.4 bool类型 三、输出3.1 常用打印功能3.2 格式化输出3.3 内置输出方法与fmt的区别 四、注释五、运算符六、条件语句6.1 基本使用6.2 条件嵌…

ncnn源码阅读(三)----数据结构Mat

文章目录 数据结构Mat成员变量成员方法构造函数1、普通构造函数2、外部数据指针构造函数3、拷贝构造函数和opertor 深拷贝函数类型转换引用计数的实现其他数据操作函数 数据结构Mat 个人认为一个框架中的比较核心的两个点&#xff0c;一个是数据结构&#xff0c;一个任务调度…

【NLP】Transformer模型原理(2)

接上文 【NLP】Transformer模型原理(1) 六、零层的transformer 观看涵盖与本节类似内容的视频:0 层理论 在进入更复杂的模型之前,简要考虑一下“零层”变压器很有用。这样的模型获取一个令牌,嵌入它,解嵌它以生成预测下一个令牌的对数: ​

AttributeError: module ‘torch.nn‘ has no attribute ‘module‘

import torch import torch.nn as nnclass LinearModel(nn.Module):def _init_(self,ndim):super(LinearModel,self)._init_()self.ndimndimself.weightnn.Parameter(torch.randn(ndim,1))#定义权重self.biasnn.Parameter(torch.randn(1)) #定义偏置def forward(self,x):# y …

点云数据标注方法研究

1.点云可视化工具 1.1 cloudcompare下载安装 sudo snap install cloudcompare 启动方法: #open pointcloud viewer cloudcompare.ccViewer #open the main software cloudcompare.CloudCompare 使用上述第一条命令&#xff0c;读取的点云某一帧数据&#xff0c;我的点云格…

opencv实战--环境配置和文字识别

文章目录 前言一、环境配置二、文字识别2.1 文字单个识别2.2 文字单个带边框 总结 前言 一、环境配置 cmd输入python的时候跳转应用商店的解决方法。https://blog.csdn.net/qq_62294840/article/details/120623501 anaconda官方下载地址&#xff1a;https://www.anaconda.com…

编程语言的优劣评选标准与未来发展趋势——探索最佳编程语言选择

编程语言的优劣评选标准与未来发展趋势——探索最佳编程语言选择 评判标准不同编程语言的优点与缺点分析对编程语言未来发展的猜测和未来趋势 &#x1f495; &#x1f495; &#x1f495; 博主个人主页&#xff1a; 汴京城下君–野生程序员&#x1f495; &#x1f495; &#x…

【力扣JavaScript】1047. 删除字符串中的所有相邻重复项

/*** param {string} s* return {string}*/ var removeDuplicates function(s) {let stack[];for(i of s){let prevstack.pop();if(prev!i){stack.push(prev);stack.push(i);}}return stack.join(); };

4. CSS用户界面样式

4.1什么是界面样式 所谓的界面样式,就是更改一些用户操作样式,以便提高更好的用户体验。 ●更改用户的鼠标样式 ●表单轮廓 ●防止表单域拖拽 4.2鼠标样式cursor li {cursor: pointer; }设置或检索在对象上移动的鼠标指针采用何种系统预定义的光标形状。 4.3轮廓线outline…

Layui之选项卡案例 详细易懂

⭐ 本期精彩&#xff1a; 利用Layui框架实现动态选项卡 ⭐ 继上一篇已经实现了左边的树形菜单栏&#xff0c;这一关卡我们已通过&#xff0c;接下来就是实现右边的动态选项卡的关卡,上个关卡的效果及链接 ⭐ 链接&#xff1a;http://t.csdn.cn/tYccL 目录 ⭐ 本期精彩&#xf…

语义分割混淆矩阵、 mIoU、mPA计算

一、操作 需要会调试代码的人自己改&#xff0c;小白直接运行会出错 这是我从自己的大文件里摘取的一部分代码&#xff0c;可以运行&#xff0c;只是要改的文件地址path比较多&#xff0c;遇到双引号“”的地址注意一下&#xff0c;不然地址不对容易出错 把 calculate.py和 u…

SpringCloud

SpringCloud01 为什么要学习微服务框架知识&#xff1f; 因为互联网发展迅速&#xff0c;业务更新迭代快 微服务符合敏捷开发需求 服 务 网 关&#xff08;请求路由&#xff0c;负载均衡&#xff09; 注册中心&#xff08;拉取或注册服务信息 eureka nacos&#xff09; 配…

tcp转发服务桥(windows)

目的 目的是为了在网关上转发udp数据和tcp数据。对于网络里面隔离的内网来说&#xff0c;有一台可以上网的服务器&#xff0c;那么通过两块网卡就可以转发出去&#xff0c;在服务器上进行数据的转发&#xff0c;有tcp和udp两种&#xff0c;udp已经写过了&#xff0c;这次使用了…

pycharm import的类库修改后要重启问题的解决方法

通过将以下行添加到pycharm中的settings-> Build,Excecution,Deployment-> Console-> Python Console中&#xff0c;可以指示Pycharm在更改时自动重新加载模块&#xff1a; %load_ext autoreload %autoreload 2

APP开发的未来:虚拟现实和增强现实的角色

移动应用程序越来越多地在我们的日常生活中发挥着重要作用。但是&#xff0c;随着技术的不断发展&#xff0c;未来的 APP开发会有什么新的发展方向呢&#xff1f;这是每个人都在关心的问题。在过去的几年中&#xff0c;移动应用程序领域发生了巨大变化。像 VR/AR这样的技术为人…

OpenCv (C++) 使用矩形 Rect 覆盖图像中某个区域

文章目录 1. 使用矩形将图像中某个区域置为黑色2. cv::Rect 类介绍 1. 使用矩形将图像中某个区域置为黑色 推荐参考博客&#xff1a;OpenCV实现将任意形状ROI区域置黑&#xff08;多边形区域置黑&#xff09; 比较常用的是使用 Rect 矩形实现该功能&#xff0c;代码如下&…

大模型与端到端会成为城市自动驾驶新范式吗?

摘要&#xff1a; 最近可以明显看到或者感受到第一梯队的城市自动驾驶量产已经进入快车道&#xff0c;他们背后所依靠的正是当下最热的大模型和端到端的技术。 近期&#xff0c;城市自动驾驶量产在产品和技术上都出现了新的变化。 在产品层面&#xff0c;出现了记性行车或者称…