详解Flutter混排瀑布流解决方案

背景

流式布局,这是一种当前无论是前端,还是Native都比较流行的一种页面布局。特别是对于商品这样的Feeds流,无论是淘宝,京东,美团,还是闲鱼。都基本上以多列瀑布流进行呈现,容器列数固定,然后每个卡片高度不一,形成参差不齐的多栏布局。

对于Native来说,无论是iOS还是Android,CollectionView和RecyclerView都能满足我们的绝大部分场景了。不过目前闲鱼很多业务场景都是在Flutter上进行实现的,当时Flutter官方只提供了ListView和GridView的实现,没有对瀑布流进行支持。

目前社区中有两个开源的解决方案,分别是WaterFallFlow和FlutterStaggeredGridView。但是在闲鱼的场景中都有一些无法满足的痛点。前者无法支持RecyclerView中StaggeredGridLayoutManager中setFullSpan这样的横跨全屏的横条卡片混排能力能力,后者在不提前预设置卡片高度的情况下有比较严重的性能问题,以及在多Sliver的场景下会有滚动错误的功能性问题。而在目前闲鱼的业务中,无论是搜索结果还是首页的同城页面,都会有混排瀑布流的需求。

所以我们决定参考RecyclerView中StaggeredGridLayoutManager的布局思路实现一套支持普通流式卡片和横跨全屏的横条卡片混排的流式布局,如图所示:

原理分析与布局流程

其实瀑布流布局和ListView和GridView一样,就是按照不同的策略将多个卡片进行尺寸计算和位置计算,然后将它们排列到一起,组成一个超过一屏,可滚动的布局。所以整个布局策略包括两个过程,首先是对卡片进行尺寸计算,计算结果决定了卡片在滚动布局中的大小。然后卡片进行位置计算,计算结果决定了卡片在滚动布局中的坐标。有了大小和坐标,就可以完成整个滚动容器的布局。下面我会对网格布局(GridView)和瀑布流布局(FlowView)的布局策略进行一个对比,让大家能更清楚的了解布局过程的细节。

Flutter中网格布局整个布局的源码都在flutter/lib/src/rendering/sliver_grid.dart的performLayout方法中,我们下面跟着源码来分析一下整个布局流程。感兴趣的同学也可以结合源码食用本文,风味更佳。

网格布局

尺寸计算过程

我们先来分析一下网格布局的卡片尺寸计算过程。这是一个GridView的常用初始化参数,我省略了一些和尺寸计算无关的参数。

GridView.count({
 @required int crossAxisCount,
 double childAspectRatio = 1.0,
})

影响布局的参数其实就是crossAxisCount(列数)和childAspectRatio(卡片纵横比)。有了这两个参数其实卡片的尺寸就很好计算了,首先先用crossAxisCount来对屏幕宽度进行等分,确定卡片的宽度,然后我们再根据这个childAspectRatio参数来计算得到卡片的高度。网格布局的卡片尺寸就可以确定下来了。计算过程如图所示:

位置计算过程

在端侧,因为一个滚动容器中的卡片数量可能会非常大,所以我们不可能对所有的卡片都进行布局,内存和运算时间都是无法接受的。我们只会布局在屏幕中以及缓存区里的卡片,之外的卡片我们会进行回收。等用户向下滑动的时候,把屏幕下方的卡片创建并布局,然后把已经划出屏幕的卡片进行回收。向上滑动的过程也是一样。所以我们会对从上到下和从下到上的位置计算过程进行分析。

我们先分析从上到下布局的过程。对于网格布局来说,每一个卡片的宽度和高度都是在位置计算流程开始之前就可以提前计算得出的。我们暂且把每个卡片的左上角叫做布局坐标点,我们来分析一下网格布局中这个坐标如何计算得出。

我们先来计算一下纵坐标,我们用卡片的index对crossAxisCount进行整除,然后再用结果乘上卡片的高度,就可以得到卡片的纵坐标了。

对于横坐标,我们已经根据crossAxisCount来对屏幕宽度进行了等分,那么每个卡片的横坐标就很容易得到了,我们用卡片的index对crossAxisCount进行整除取余,这样就能得到卡片在某一行中的顺序(即第几列),然后再乘上卡片的宽度,这样就可以得到卡片的横坐标了。

例如列数为2,卡片宽度和高度都为100的一个网格布局,那么第四个卡片(index为3)的横坐标为(3%2)×100为1,纵坐标为 (3~/ 2)×100为100,所以坐标为(100,100)。

计算过程如图所示:

整个布局关键源码如下:

// 卡片尺寸计算
final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
  final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
  final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;

// 卡片坐标计算
SliverGridGeometry getGeometryForChildIndex(int index) {
 final double crossAxisStart = (index % crossAxisCount) * crossAxisStride; //横坐标
 return SliverGridGeometry(
  scrollOffset: (index ~/ crossAxisCount) * mainAxisStride, //纵坐标
  crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
  mainAxisExtent: childMainAxisExtent,
  crossAxisExtent: childCrossAxisExtent,
 );
} 

// 对卡片进行遍历布局
for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) {
   final SliverGridGeometry gridGeometry = layout.getGeometryForChildIndex(index); //获取尺寸和位置信息
   final RenderBox child = insertAndLayoutLeadingChild(
    gridGeometry.getBoxConstraints(constraints),
   ); //使用计算好的尺寸信息来限制卡片大小
   final SliverGridParentData childParentData = child.parentData;
   childParentData.layoutOffset = gridGeometry.scrollOffset; //卡片的纵轴坐标赋值
   childParentData.crossAxisOffset = gridGeometry.crossAxisOffset; // 卡片的横轴坐标赋值
   assert(childParentData.index == index);
   trailingChildWithLayout ??= child;
   trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
  }

由此可见,网格布局中,每个卡片的位置坐标跟index是有一一对应关系的。所以无论是向下滚动对后面的卡片进行布局,还是向上滚动对前面的卡片进行布局。都使用这个策略就可以得出所有卡片的坐标。

瀑布流布局

尺寸计算过程

然后我们对瀑布流布局的卡片尺寸计算过程进行分析,反推出我们需要传入的初始化参数。首先,我们需要考虑到在瀑布流布局中一共有两种卡片,一种是宽度由屏幕宽度被布局列数均分的普通卡片,另一种是宽度充满整个屏幕的特殊卡片,我们后续叫它横条卡片。我们会分别对这两种卡片进行尺寸计算。

普通卡片

首先对于普通卡片来说,卡片的尺寸宽度和网格布局中的卡片一样,是由列数和屏幕宽度决定的,所以我们同样需要crossAxisCount这个参数。宽度确定之后,我们需要确定卡片的高度。在瀑布流布局中,每个卡片的高度是不同的,这也是瀑布流布局和网格布局最大的区别。所以我们其实可以由每个卡片自己决定自己的高度,也就是我们不需要在布局初始化的时候传入类似childAspectRatio这样影响卡片的参数。不过我们在实际的业务场景中,通常会对某些特殊位置的卡片进行特殊的高度设置,例如两列流中横条卡片上面的两个卡片,UED会有保证这两个卡片的底部位置一致的需求,不然就会造成卡片之间的裂隙,影响观感。所以我们需要一个定义了一个方法参数mainAxisExtentBuilder。

typedef double IndexedMainAxisExtentBuilder(int index);

这是一个返回值为double的方法参数,瀑布流在布局的时候会根据index尝试获取开发者在这个方法中的返回值,如果这个返回值为null,就用卡片自己内部的布局来决定卡片高度,反之就用这个返回值来决定卡片高度。计算过程如图所示:

横条卡片

横条卡片在高度的确定流程上是和普通卡片一致的,只是横条卡片的宽度总是和屏幕宽度一致,不受crossAxisCount限制。计算过程如图所示:

所以我们只需要在布局过程中能够区分这两种卡片,就可以用不同的策略对它们的尺寸进行计算。类似于mainAxisExtentBuilder,我们定义了一个IndexedFullSpanBuilder参数。

typedef bool IndexedFullSpanBuilder(int index);

这是一个返回值为bool的方法参数,瀑布流在布局的时候会根据index尝试获取开发者在这个方法中的返回值,如果这个返回值为null或者false,就使用普通卡片的宽度计算策略,反之就使用横条卡片的宽度计算策略。

所以我们就定义好了瀑布流布局初始化中确定布局的三个参数。

FlowView.count({
 @required int crossAxisCount,
 IndexedFullSpanBuilder fullSpanBuilder,
 IndexedMainAxisExtentBuilder mainAxisExtentBuilder,
})

这样我们就能够计算出布局中每一个卡片的尺寸了,接下来我们只需要再确定卡片左上角的坐标,这样就可以完成卡片的布局了。

位置计算过程

对于瀑布流来说,位置计算过程会比网格布局复杂得多,我们先来分析一下从上到下布局的过程。之前我们说过,在混排瀑布流布局中会有两种卡片,横条卡片和普通卡片。我们希望卡片的布局中尽量没有间隙。

所以对于普通卡片来说,卡片的纵坐标计算过程是这样的。我们需要在已经完成布局的卡片中进行查找,找到其中纵坐标+卡片高度(即卡片bottom纵坐标)值最小的卡片,我们把这张卡片叫做最低卡片。然后把下一张卡片布局在最低卡片的正下方,所以下一张卡片的纵坐标就是最低卡片的纵坐标+卡片高度。因为需要布局在最低卡片的正下方,所以横坐标就直接和最低卡片的横坐标保持一致即可。

对于横条卡片来说,因为他的宽度总是和屏幕宽度一致,所以我们只需要计算它的纵坐标。它的横坐标永远是0,他的纵坐标和普通卡片刚好相反,需要在已经完成布局的卡片中进行查找,找到其中纵坐标+卡片高度(即卡片bottom纵坐标)值最大的卡片,我们把这张卡片叫做最高卡片。然后把横条卡片布局在这张最高卡片下面,否则这张横条卡片会遮住其他卡片。在这里我们根据列数生成一个初始值都为0的纵坐标列表,每布局一个卡片就把该列的offset加上卡片的高度。

计算过程如图所示:

而从下到上的布局过程,瀑布流和GridView和ListView都不太一样,ListView,上一个卡片的位置可以由下一个卡片布局位置来确定,往上滚动的时候,我们只用把卡片布局在最上面的卡片上面就可以了,GridView直接根据index就可以完成计算了,瀑布流比较特殊,因为卡片的布局依赖于它上面的卡片的布局信息,无法通过后一个卡片的布局信息推断出前一个卡片的布局。在这里,一般有两种处理方式。

维护一个index和crossAxisIndex一一对应的Map关系表

目前RecyclerView和WaterFallFlow是采用这种方式的,在用户向下滑动时,正常布局,然后记录下每张卡片属于哪一列。然后在用户向上滑动时,对即将进行布局的卡片,先通过这个关系表得到它属于哪一列,然后将它布局在这一列最上面卡片的上方,这样就可以保证卡片的布局对于用户来说始终是一致的。但是这样的方式在混排瀑布流中,需要对横条卡片做特殊处理,因为横条卡片的上一张卡片不一定和横条卡片在布局上是紧贴着的,可能会有间隙。所以我们还需要记录横条卡片跟上一张卡片的间隙,布局的时候再加上这个间隙再布局,这样才能保证正确布局。

使用分页思想,始终从上到下进行布局。

FlutterStaggeredGridView采用的就是这种方式,而我们实现的混排瀑布流也使用了这样的思路。我们设定一个高度PageSize,按照这个高度给整个瀑布流布局进行分页,然后维护一个pageIndex和pageInfo的对应表,每一页里记录着自己的mainAxisOffsets,以及的firstChildIndex。

第一页的mainAxisOffsets很显然是一个长度为crossAxisCount,值为0的列表。然后从上到下布局时,不断更新这个mainAxisOffsets,例如第一页在第一列布局了第一个高度为100的普通卡片,则mainAxisOffsets更新为{100,0}。然后在第二列布局了第二个高度为150的普通卡片,则mainAxisOffsets更新为{100,150}。后续我们布局了一个高度为200的横条卡片,则mainAxisOffsets更新为{350,350}。然后横条卡片和第一张卡片之间会有一个50的间隙,这个mainAxisOffsets就是下一张卡片布局的起始点。然后当有mainAxisOffsets都超过PageSize时,我们就开始分下一页。下一页的initialOffsets就是上一页的mainAxisOffsets,然后再开始第二页的卡片布局。

这样当我们向上滚动时,当我们需要对上一个卡片进行布局时,我们就会从这个卡片所属的页面的第一个卡片开始布局,这样就瀑布流就始终是从上到下布局的。就能保证布局的正确性。

然后我们按照RenderSliverGrid的思路实现了一个RenderSliverFlow。整个布局的关键的源码如下:

// 卡片坐标计算

SliverFlowGeometry getGeometryForChildIndex(int index,List<double> startOffsets) {
 bool isFullSpan = _getIsFullSpan(index); //是否是横条卡片

 double maxOffset = startOffsets.reduce(math.max); //最高卡片底部纵坐标
 double minOffset = startOffsets.reduce(math.min); //最低卡片底部纵坐标

 var scrollOffset = minOffset;
 var crossAxisIndex = startOffsets.indexOf(minOffset); //属于哪一列
 int needCrossAxisCount = isFullSpan ? crossAxisCount : 1;

 if(isFullSpan){
  scrollOffset = maxOffset;
  crossAxisIndex = 0;
 }

 if (reverseCrossAxis) {
  crossAxisIndex = crossAxisCount - needCrossAxisCount - crossAxisIndex;
 }
 var crossAxisOffset = crossAxisIndex * crossAxisStride;
 var mainAxisExtent = _getChildMainAxisExtent(index);
 return SliverFlowGeometry(
  scrollOffset: scrollOffset, //纵坐标
  crossAxisOffset: crossAxisOffset, //横坐标
  mainAxisExtent: mainAxisExtent,
  crossAxisExtent: crossAxisStride * needCrossAxisCount - crossAxisSpacing,
  isFullSpan: isFullSpan,
  crossAxisIndex: crossAxisIndex,
 );
}

内存回收和性能优化

回收机制

前文中我们提到过,在端侧,因为一个滚动容器中的卡片数量可能会非常大,所以我们不可能一次性对所有的卡片都进行布局和绘制,内存和运算时间都是无法接受的。

我们总是希望只布局尽可能少的卡片,我们先来分析一下最晚可以从哪个卡片开始布局。从上文我们知道,我们将整个瀑布流进行了分页,每一页包含着多个卡片,我们记录着每一页的起始offsets,所以我们需要找可见区域最上方的卡片,把这个卡片的位置标记为firstIndex,然后从这个卡片所属的页面的第一个卡片开始布局。然后我们再分析一下布局在什么时候结束,因为我们前面的卡片无需依赖后面的卡片,所以我们布局到可视区域之外就可以停止布局了,然后把最后一张卡片的位置标记为lastIndex。每一次布局都会产生一个firstIndex和lastIndex。

当我们往下滑动的时候,我们会判断firstIndex属于哪一页,这就表明这一页此时在最上方,那对这一页之前的Page里的卡片我们就可以进行内存回收了。往上滑动的时候,我们把lastIndex之后的卡片全部进行回收就好了。

性能优化

这样的分页机制虽然是能够保证布局的正确性,但是其实很多情况下,我们都需要布局缓存区以外的卡片,举个极端情况的例子,可见区域的第一张卡片是属于某一个分页的最后一张卡片,这个时候我们就不得不把这个分页里的全部卡片都进行布局。这其实会对滑动性能造成一些影响,一开始的设计PageSize固定为一个屏幕的高度,每一屏分一页。后来进行了性能优化,我们会根据大部分瀑布流的卡片高度得到一个分页值,尽量保证每一次分页所包含的卡片尽可能就是一行的卡片数。这样可见区域的第一张卡片往往就是这个分页的第一张卡片,这样一来就可以减少不必要的布局。

然后我们对GridView和FlowView进行了性能测试,使用脚本对两个滚动容器分别往下滚动五次,再滚动五次。最后得出性能数据,然后我们主要关注两个数据,分别是最大丢帧数和最差帧耗时,这往往就是最影响体感的两个数据。通过根据平均卡片尺寸高度动态调整分页,最后的性能数据达到了尽可能和GridView一致。使用同一机型,性能测试数据如下:

效果与落地

这是目前使用FlowView完成的一个Demo工程,支持了Flutter滚动体系里的各种功能。scrollController(滚动到offset),reverse(逆序排列),scrollDirection(滚动方向垂直或水平滚动)等。

在闲鱼工程中,主要在首页、搜索结果页等进行落地。不过目前Flutter首页在线上只是进行了少量的灰度。

总结与展望

整个瀑布流目前结合PowerScrollView进行了初步落地,在整个布局的过程中,在功能上可扩展和优化的地方依然存在。

在可扩展的功能方面,未来希望可以在一个布局中完成不同列数的混排,例如一个Sliver中可以有一列、两列、三列、甚至六列的混排,类似于RecyclerView中的GridLayoutManager。

然后在性能方面,希望之后能够在布局逻辑中进行优化,尽可能减少不必要的计算和布局。能够在滑动中提供更好的体感。

希望官方之后会对这样比较常用的布局进行支持,这样也可以给后面的布局优化带来思路。

到此这篇关于详解Flutter混排瀑布流解决方案的文章就介绍到这了,更多相关Flutter混排瀑布流内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Flutter持久化存储之数据库存储(sqflite)详解

    前言 数据库存储是我们常用的存储方式之一,对大批量数据有增.删.改.查操作需求时,我们就会想到使用数据库,Flutter中提供了一个sqflite插件供我们用于大量数据执行CRUD操作.本篇我们就来一起学习sqflite的使用. sqflite是一款轻量级的关系型数据库,类似SQLite. 在Flutter平台我们使用sqflite库来同时支持Android 和iOS. sqflite使用 引入插件 在pubspec.yaml文件中添加path_provider插件,最新版本为1.0.0,如下:

  • Flutter 超实用简单菜单弹出框 PopupMenuButton功能

    相信在实际开发过程当中,肯定少不了这样的功能: 点击 AppBar 右上角的按钮,弹出一个菜单供用户选择. 幸运的是,Flutter 提供给我们了一个 Widget,直接就能实现如上的效果. PopupMenuButton 还是老规矩,先看官方的说明: Displays a menu when pressed and calls onSelected [1] when the menu is dismissed because an item was selected. The value pa

  • 详解Flutter WebView与JS互相调用简易指南

    本文采用Flutter官方WebView插件:https://pub.dartlang.org/packages/webview_flutter WebView与JS互相调用是一个刚需,但是貌似现在大家写的文章讲的都不是很清楚,我这个简易指南简单粗暴地分为两部分:JS调用Flutter和Flutter调用JS,拒绝花里胡哨,保证一看就懂,一学就会. 开始之前先简单了解一下官方WebView所包含的API: onWebViewCreated:在WebView创建完成后调用,只会被调用一次: ini

  • flutter Container容器实现圆角边框

    本文实例为大家分享了flutter Container容器实现圆角边框的具体代码,供大家参考,具体内容如下 在这里使用 Container 容器来实现圆角矩形边框效果 1 圆角矩形边框 Container( margin: EdgeInsets.only(left: 40, top: 40), //设置 child 居中 alignment: Alignment(0, 0), height: 50, width: 300, //边框设置 decoration: new BoxDecoration

  • Flutter实现页面切换后保持原页面状态的3种方法

    前言: 在Flutter应用中,导航栏切换页面后默认情况下会丢失原页面状态,即每次进入页面时都会重新初始化状态,如果在initState中打印日志,会发现每次进入时都会输出,显然这样增加了额外的开销,并且带来了不好的用户体验. 在正文之前,先看一些常见的App导航,以喜马拉雅FM为例: 它拥有一个固定的底部导航以及首页的顶部导航,可以看到不管是点击底部导航切换页面还是在首页左右侧滑切换页面,之前的页面状态都是始终维持的,下面就具体介绍下如何在flutter中实现类似喜马拉雅的导航效果 第一步:实

  • Flutter进阶之实现动画效果(一)

    上一篇文章我们了解了Flutter的动画基础,这一篇文章我们就来实现一个图表的动画效果. 首先,我们需要创建一个新项目myapp,然后把main.dart的内容替换成下面的代码 import 'package:flutter/material.dart'; import 'dart:math'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { @override Widget build(Bui

  • Flutter中http请求抓包的完美解决方案

    前言 前阵子有同学反馈Flutter中的http请求无法通过fiddler抓包,作者喜欢使用Charles抓包工具,于是抽时间写了个小demo测试了一下,结论是在手机上设置代理,Charles确实抓不到请求数据包.于是对该问题进行了分析: 确定使用的是http发起的get请求,理论上http协议应该可以被Charles抓到包的,如果没有抓到包,那可能是没有走代理,于是乎通过将笔记本连接的wifi断开测试了一下手机上APP发起http请求,发现请求成功,证实确实没有走代理: 为什么http请求没有

  • 详解Flutter混排瀑布流解决方案

    背景 流式布局,这是一种当前无论是前端,还是Native都比较流行的一种页面布局.特别是对于商品这样的Feeds流,无论是淘宝,京东,美团,还是闲鱼.都基本上以多列瀑布流进行呈现,容器列数固定,然后每个卡片高度不一,形成参差不齐的多栏布局. 对于Native来说,无论是iOS还是Android,CollectionView和RecyclerView都能满足我们的绝大部分场景了.不过目前闲鱼很多业务场景都是在Flutter上进行实现的,当时Flutter官方只提供了ListView和GridVie

  • 详解Flutter点击空白隐藏键盘的全局做法

    开发原生页面的时候,在处理键盘事件上,通常的需求是,点击输入框外屏幕,要隐藏键盘,同样的,这样的需求也需要在 Flutter 上实现, Android 上的实现方式是在基类 Activity 里实现事件分发,判断触摸位置是否在输入框内. /** * 获取点击事件 */ @CallSuper @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.MotionEv

  • 详解Flutter和Dart取消Future的三种方法

    目录 使用异步包(推荐) 完整示例 使用 timeout() 方法 快速示例 将Future转换为流 快速示例 结论 使用异步包(推荐) async包由 Dart 编程语言的作者开发和发布.它提供了dart:async风格的实用程序来增强异步计算.可以帮助我们取消Future的是CancelableOperation类: var myCancelableFuture = CancelableOperation.fromFuture( Future<T> inner, { FutureOr on

  • 详解Flutter中数据传递的方式

    目录 1.构造方法传递 2.InheritedWidget 3.Notification 4.Stream & event_bus 在Flutter中,常见的数据传递一共有以下几种: 1.构造方法传递 Flutter的构造方法具备着dart语言的特点,参数具备可选状态,通过构造方法传递数据,可以很方便的将任意数据进行传递,平时开发中,A跳转B页面最常用的方法就是通过构造方法进行传递.比如我们最常见的Key就是通过构造一级一级向下传递的. 优点: 相邻页面之间传递数据非常方便,你不需要进行任何额外

  • 详解Flutter 响应式状态管理框架GetX

    目录 一.状态管理框架对比 Provider BLoC GetX 二.基本使用 2.1 安装与引用 2.2 使用GetX改造Counter App 2.3 GetX代码插件 三.其他功能 3.1 路由管理 3.2 依赖关系管理 3.3 工具 3.4 改变主题 3.5 GetConnect 3.6 GetPage中间件 Priority Redirect onPageCalled OnBindingsStart OnPageBuildStart 3.7 全局设置和手动配置 3.8 StateMix

  • 详解Java分布式IP限流和防止恶意IP攻击方案

    前言 限流是分布式系统设计中经常提到的概念,在某些要求不严格的场景下,使用Guava RateLimiter就可以满足.但是Guava RateLimiter只能应用于单进程,多进程间协同控制便无能为力.本文介绍一种简单的处理方式,用于分布式环境下接口调用频次管控. 如何防止恶意IP攻击某些暴露的接口呢(比如某些场景下短信验证码服务)?本文介绍一种本地缓存和分布式缓存集成方式判断远程IP是否为恶意调用接口的IP. 分布式IP限流 思路是使用redis incr命令,完成一段时间内接口请求次数的统

  • 详解JAVA 字节流和字符流

    1.InputStream 和 Reader InputStream 和 Reader 是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法. 在 InputStream 里包含如下三个方法. int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型). int read(byte[] b):从输入流中最多读取 b.length 个字节的数据,并将其存储在字节数组 b 中,返回实际读

  • 详解Flutter 调用 Android Native 的方法

    Flutter 调用 Android Native 的方法,是通过MethodChannel的方式来实现的: 在Android端: 创建一个Class,实现FlutterPlugin和MethodCallHandler接口 重写onAttachedToEngine(),onDetachedFromEngine(),onMethodCall() onAttachedToEngine()中,根据自定义的CHANNEL_NAME创建MethodChannel, onDetachedFromEngine

  • 详解Golang实现请求限流的几种办法

    简单的并发控制 利用 channel 的缓冲设定,我们就可以来实现并发的限制.我们只要在执行并发的同时,往一个带有缓冲的 channel 里写入点东西(随便写啥,内容不重要).让并发的 goroutine在执行完成后把这个 channel 里的东西给读走.这样整个并发的数量就讲控制在这个 channel的缓冲区大小上. 比如我们可以用一个 bool 类型的带缓冲 channel 作为并发限制的计数器. chLimit := make(chan bool, 1) 然后在并发执行的地方,每创建一个新

  • 详解Flutter的路由导航

    Flutter 的路由导航 路由管理或导航管理:从一个页面平滑地过渡到另一个页面,我们需要有一个统一的机制来管理页面之间的跳转.在原生的Android 开发,是通过startActivity或startActivityForResult 来完成页面的跳转的,在Flutter 中如何实现呢? 在 Flutter 中,页面之间的跳转是通过 Route 和 Navigator 来管理的: Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭: 而 Navig

随机推荐