0%

​ 今天找《code》封面时看到一个个人博客,推荐了作者自己读过的最好的几本技术书籍,其中包括了《code》,还有大名鼎鼎的《The C Programming Language》,在此收藏一下,日后有时间去拜读一遍,也许是never,:-)

​ 看完书籍推荐,顺便看了下作者的about,法国Lyon(里昂?)出生,Romans-Sur-Isere长大,2004在Grenoble University获得CS Master学位,在巴黎做了一年程序员后,为了逐梦和学英语,使用Work Holiday Visa去了多伦多,计划呆五个月,最终呆了9年。在多伦多的第一份工作是busboy

image

不久后回归程序员,在Rogers和QuickPlay供职,然后成为一家创业公司Memset Software的老板。

2012年获得加拿大国籍,拥有加拿大和法国双国籍。

2014年入职谷歌,在加州落脚。

出过两本书,Game Engine Black Book: Wolfenstein 3DGame Engine Black Book: DOOM

完。

​ 以CRT(cathode-ray tube)阴极射线管显示器为例,把计算机数据信号转换并传给CRT显示的部件叫显示适配器(video display adapter),适配器所在的线路板就是大家所熟知的显卡(video board)

image

image

image

二维图像显示看起来有点复杂,简单来说图像是由一束光(beam)快速连续扫过整个屏幕生成的,从屏幕左上角开始,水平扫到右边,到头另起一行重新开始,直到最右下角结束。每一条水平的线叫做扫描线,扫完整个屏幕生成一张图像,每秒生成六十张图像,这个速度足够快,不会出现一闪一闪体验不好,也就是通常所说的卡顿。为什么选择60Hz而不是其他,可以看这里,大意是CRT因为构造原理关系对磁场非常敏感,为了降低交流电磁场的影响,选择和北美交流电一样的频率。我国交流电受前苏联影响是50Hz。

image

早期的显示器(电视机)是黑白的,因为原理和成本相比彩色会比较简单便宜,0.5v代表黑色,2v代表白色,这两个电压之间表示灰色

image

接下来让我们把目光从电视机转向计算机,从计算机的角度来看,生成一张图像,最方便的办法是把图像分解为一个网格(rectangular grid),每一个格子叫做一个像素pixel (picture element)

image

由于早期电视显示带宽4.2MHz的限制,显示分辨率最高为320*200,横向320像素,纵向200像素,每一个像素在黑白之间切换,一共可以显示64000个像素。

现在我们想在这些格子里显示一些文本,一次性能显示多少内容呢?这个取决于每一个字符用多少个像素来实现。下图是一个8*8像素的实现

image

上图中显示的是常用的一些ASCII码,在内存中,占用的大小是7bit,如果要显示,对应占用的大小就是64bit,这64bit所代表的数值也可以看作是字符的一种码。按照这种定义,我们可以在320*200分辨率的屏幕上显示每行40字,一共25行的文本内容。

image

为了动态显示,显示适配器配备一个RAM来存要显示的内容,CPU可以访问更新数据,也包含一个字符串生成器,里面记录着ASCII码对应的像素排列格式,只读形式,字符的像素排列一旦定下里,不能随意改动(ROM)。

image

在八十年代,硬件成本非常昂贵,7bit字符对应64bit显示,会显得有点大,为了节约成本,进化出了另一种对应的显示方式

image

左边每一行是10bit,右边是8bit,每行对应,左边前7位依然是ASCII码,后三位代表第几行,从上往下数一共8行,最终的效果是

image

image

在文本输入过程中,会有一个游标紧跟其后,来提醒用户下一个字符应该出现在哪里,行和列的坐标数据存放在显示适配器上一个8bit的寄存器(register)里,CPU进行实时计算更新

image

image

在IBM1981年发售第一款PC时搭配的显示器(上图)每行显示80个字符,一共25行,每行显示80个字符是因为IBM早期的计算机使用打孔卡片,上面一次性最多显示80个

image

到了1987年,IBM和Apple计算机图形显示分辨率都提升为640*480 (4:3,爱迪生电影公司制定的标准),随着后面像素的提升,800 * 600,1024 * 768,1280 * 960,都是按照这个比例。

后面随着技术的发展和需求的提升,出现了图形化界面,显卡的功能从text only进化到多媒体

image

在早期的显卡工作过程中,cpu把数据写入显存的同时,还在画图,包括富文本的大小和字体,因此比仅显示文本的显卡拥有更多的内存,上面举例的显示分辨率有64000个像素,每个像素为1bit,显示黑与白,对应的内存就为64000bit大小,8000字节。在实际使用过程中,需要显示的图像不仅仅是非黑即白,也需要显示黑白中间的颜色,也就是灰色,每个像素就需要扩大为1字节(8bit),00h代表黑色,FFh代表白色,中间的值代表不同颜色的灰,内存扩大为64000字节(byte)。

image

如果需要显示彩色,我们知道任何颜色都是由三原色组成,Red,Green,Blue,RGB

image

image

每个像素三个bit,可以呈现出八种颜色,最终呈现的效果是

image

在现实生活中,色彩的种类更加丰富,把每个像素点上升到3字节,每个字节代表一种原色,每一种原色都有256种程度表示,组合起来一共可以显示1677,7261种颜色,这被称作full color,真彩色

image

image

在显示器成像方面,有两种成像的方式,vector和raster

image

显示发展到这一步,交互呼之欲出

鼠标是怎么移动的?在第一台GUI Alto计算机上,屏幕的分辨率是606*808,一共489648像素,每一个像素对应一个bit,颜色为黑白,需要64KB显存。在显存写入数据,经过计算呈现在屏幕上,移动鼠标时,底部的滚轮通过传感器输入坐标,显存根据坐标在不同的位置计算01的变化,呈现鼠标移动的效果,如果鼠标点击了按钮,对应位置的比特通过计算来响应按钮的点击事件。这种新颖的交互方式同时也催生了世界上第一款面向对象的语言–Small Talk

image

image

说到这里,顺便提一下bitmap,屏幕上像素显示的图像如果想存起来,最简单的方法就是对应的矩阵像素点在内存中对应存起来,这种数据类型存起来以后就是bitmap

image

存起来以后发现文件很大,并且很多数据是重复的,比如图中有一大片蓝天和白云

image

我们可以存储1000这个数字和一个蓝点来表示蓝色像素有一千个,更加节省bit,这就是压缩算法的基本思想,为了节省空间。有的压缩算法会导致数据有损,但损失可接受,不影响图片效果,比如最常见的JPEG(Joint Photography Experts Group)

OCR

前面讲过,ASCII码转为像素形式显示,显卡内部的ROM有一一对应关系,如果逆向过来,已知一个像素排列阵型(pixel pattern),如何确定对应的ASCII码呢?通常来说有些困难,因为通常得到的bitmap不一定和ROM预先存储的一样,需要一些软件算法来尽量还原,这就是OCR(optical character recognition)。简单阐述大致原理是根据每一种特定的排列通过特定的算法生成一列哈希值,然后去和已知图像的哈希值对比,最接近的便是首选答案。

image

衍生问题:显示卡顿优化

一秒播放六十张图片,低于这个频率视觉上会有卡顿,每16.7毫秒需要生成一张图片,图片生成需要CPU计算生成bitmap,交给GPU渲染,然后放到frame缓冲区等待显示到屏幕上

CPU方面:减少计算量

  • 视图轻量级(减少点)
  • 视图布局简单(减少点)
  • 减少修改属性
  • 合理使用线程,避免线程卡顿
  • 富文本处理合理
  • 图片解码合理

GPU方面:减少渲染量

  • 视图层级简单(减少点)

  • 渲染图片少(减少点)

  • 渲染图片小(减少点)

  • 合理使用透明度

  • 避免离屏渲染(阴影,圆角,遮罩)(减少点)

完。

PS:如果文中某些图片加载失败,请参考这里设置host文件

参考链接:

https://en.wikipedia.org/wiki/Code:_The_Hidden_Language_of_Computer_Hardware_and_Software

https://blog.infolink.com.tw/2021/rediscover-pixel-dpi-ppi-and-pixel-density/

​ 今天看耗子哥新文如何做一个有质量的技术分享,文末做了一个分享示例

For example, if you want to sharing a topic about Docker. the following outlines would be good one:

  • What’s the major problems need to solve. (Provision, Environment, Isolation etc.)
  • The Alternative solutions. (Puppet/Chef/Ansible, VM, LXC etc.)
  • The Best Solution – Docker. Why?
  • Docker’s key techniques – image, cgroup, union fs, namespace…
  • Docker’s Pros/Cons
  • Further reading list.

感觉吸收新知识也是同理,建立一个问题,引起好奇心,然后顺理成章往下推演,看现在的最佳解决方案(知识)是怎么经过筛选得来的。

下面是自己收集的一些tips供自己参考,持续更新:

  • teach someone else
  • learn in short bursts of time
  • take notes by hand
  • take a study nap
  • the human brain processes images 60,000 times faster than text, and 90 percent of information transmitted to the brain is visual

参考:learn anything faster

Humans Process Visual Data Better

​ 一开始想添加一个博客评论系统,方便交流,参考的是kid的博客,选择最简单的方案 utteranc.es,hexo已经有插件集成,使用的是npm安装。安装完成后在博客底部成功出现评论框

comment-box.jpg

点击github登录时出现了访问失败,找不到资源

image

经过测试,确定原因是自己域名没有https的原因,链接添加参数后默认以https访问出现请求失败情况。首先想到的是去阿里万网看自带的服务,价格有些小贵image

为了秉持互联网免费原则,确定了Netlify方案,根据教程配置完成后大概等了半天时间,https域名就可以正常使用了。

没有尝试的方案:

最后,感谢蓝伟洪小蘑菇配合测试评论系统。

​ 在纯flutter应用里打开网页,目前流行两个插件:

  • flutter_webview_plugin (以下简称三方web)

    • 特点

      1.实测中滑动相对流畅

      2.bug较少

      3.不在flutter的widget tree中,视图在app最顶层,通过channel通知来实现显示和隐藏,全局是一个单例

      4.非官方

  • webview_flutter(以下简称官方web)

    • 特点

      1.镶嵌在widget tree中,方便在flutter中控制

      2.键盘弹出有部分bug

      3.官方支持

由于历史原因和开发成本考虑,继续沿用三方web作为浏览器,遇到一个需求,就是需要打开网页的时候秒开,实现类似今日头条的效果,经过调研和结合flutter特性,最终方案定型为下图

image

image

​ 在实现iOS过程中比较顺利,在appdelegate启动的时候加载WebViewController放在后台待命,MainViewController新增一个导航在合适的时候来push WebViewController

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
    lazy var webViewEngine:FlutterEngine = {
let engine = FlutterEngine(name: "webViewEngineName")
engine.run(withEntrypoint: "webViewMain")
return engine
}()

lazy var webViewFlutterVC:FlutterViewController = {
let webFlutterVC = FlutterViewController(engine: self.webViewEngine, nibName: nil, bundle: nil)
webFlutterVC.view.backgroundColor = UIColor.white
return webFlutterVC
}()

lazy var registerWebFlutterVC: Void = {
CopiedPluginRegistrant.register(with: self.webViewFlutterVC)
}()


override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
//启动WebViewController
_ = self.registerWebFlutterVC

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

同样的原理框架在实现安卓的时候因为实现原理不同,做了改动:

  1. 两个页面均由activity实现,在安卓app启动的时候需要指定一个activity作为单例launcher,把web activity作为launcher先启动,等内部webview预热完成,再启动main activity

manifest文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<activity
android:name=".FlutterWebViewActivity"
android:launchMode="singleInstance"
android:theme="@style/activityTheme">

<!--启动先预热第二引擎页面,oncreate里面再切换回app主页-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!--启动的时候加背景图防止黑屏-->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />


</activity>

  1. 在预热web activity过程中,如果不注意两个activity engine注册插件先后顺序和冲突,会出现flutter和原生交互找不到method方法报错,需要按照下面的顺序来

    • 在Application onCreate方法里预热web引擎,automaticallyRegisterPlugins一定要传false

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class MyApp extends FlutterApplication {
      @Override
      public void onCreate() {
      super.onCreate();

      //启动预加载一个引擎for 单例 webview 不自动注册插件
      FlutterEngine webEngine = new FlutterEngine(this,null,false);
      FlutterEngineCache.getInstance().put(WebEngine.ENGINE_ID,webEngine);

      }
      }
    • 继承FlutterActivity类,覆写获取engine方法,拿到已经预热好的engine

      1
      2
      3
      4
      5
      //加载预热的引擎,在my_app类中创建
      override fun provideFlutterEngine(context: Context): FlutterEngine? {
      //return super.provideFlutterEngine(context)
      return FlutterEngineCache.getInstance().get(WebEngine.ENGINE_ID)
      }
    • 执行指定的entry point

      1
      2
      3
      4
      override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
      runEntrypoint(flutterEngine)
      super.configureFlutterEngine(flutterEngine)
      }

这样,两端就都可以实现H5“秒开”体验。

完。

症状:

除了电源按钮,键盘和触摸板全部失灵,触摸板可以按下去,鼠标无法滑动,外接usb键盘鼠标可以使用

处理办法:

重置NVRAM

步骤:首先按住 option+commamd+P+R键,再按住电源键开机,等待听duang的开机声,听到四声后,松开按键,按电源键开机,恢复正常

参考:https://www.jianshu.com/p/51d6142b564a

完。

报错原文:

1
2
Cloning into '/var/folders/nd/n2mbp09n1lqbyyf586wntcww0000gn/T/d20210428-5764-pwxnoe'...
fatal: unable to access 'https://github.com/SDWebImage/SDWebImage.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

原因分析:

本地git使用代理连接github,代理出了问题

问题解决:

让代理恢复正常或者不使用代理

1
2
git config --global --unset http.https://github.com.proxy
git config --global --unset https.https://github.com.proxy

去除了http 和 https的代理

完。

参考:https://www.jianshu.com/p/388c2914f22a

​ WK is run in a separate process so that it can draw on native Safari JavaScript optimizations,this means WK loads web pages faster than UIWeb,UIWeb uses traditional JS engines which are basic interpreters,Apple’s Nitro engine uses a 4 tier compiling strategy which can execute JS 40 times faster than a traditional JS interpreter.

Nitro means Nitromethane, is a kind of fuel for car engine.

source:

早上开车等红绿灯时想到几个月前写的一个算法,应该可以更简单,今天重新写了一遍

  • 寻找下一个数字,例如输入345,输出354

    思路:从最右边位和上一位对比,如果大,就往前移,移动完,尾数排序,从小到大,保证次大

  • 代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//字符串分成数组
NSMutableArray *convertStringToArray(NSString *str){
NSMutableArray *marr = [NSMutableArray array];
for (NSInteger i = 0; i < str.length; i++) {
[marr addObject:[str substringWithRange:NSMakeRange(i, 1)]];
}
return marr;
}

//数组拼成字符串
NSString *convertArrayToString(NSArray *arr){
NSMutableString *mstr = [NSMutableString string];
for (NSString *str in arr) {
[mstr appendString:str];
}
return mstr;
}

//冒泡排序 从小到大
NSString *bubbleSort(NSString *str){
NSArray *sortedArray = convertStringToArray(str);
NSMutableArray *marr = [NSMutableArray arrayWithArray:sortedArray];
//最后一位的索引
unsigned long j = marr.count-1;
for (;j>0;j--) {

BOOL didSort = NO;
//第一轮结束
for (int i=0; i<j; i++) {
NSString *pre = marr[i];
NSString *next = marr[i+1];
if (pre.integerValue > next.integerValue) {
[marr exchangeObjectAtIndex:i+1 withObjectAtIndex:i];
didSort = YES;
}
}

if (!didSort) {
break;
}
}

NSMutableString *mstr = [NSMutableString string];
for (NSString *str in marr) {
[mstr appendString:str];
}
return mstr;

}


NSString *findNextLargerNum(NSString *oriNum){
NSString *resultStr = oriNum;
NSMutableArray *oriNumMArr =[NSMutableArray arrayWithArray:convertStringToArray(oriNum)];

for (int j = oriNumMArr.count-1; j>0; j--) {

BOOL getResult = NO;
NSString *last = oriNumMArr[j];
for (int i = j-1; i>=0; i--) {
NSString *pre = oriNumMArr[i];
if (last.integerValue > pre.integerValue) {
[oriNumMArr exchangeObjectAtIndex:i withObjectAtIndex:j];
NSRange range = NSMakeRange(i+1, oriNumMArr.count - 1 - i);
NSString *surfixStr = convertArrayToString([oriNumMArr subarrayWithRange:range]);
NSString *surResultStr = bubbleSort(surfixStr);
NSString *preStr = convertArrayToString([oriNumMArr subarrayWithRange:NSMakeRange(0,i+1)]);
resultStr = [NSString stringWithFormat:@"%@%@",preStr,surResultStr];
getResult = YES;
break;
}
}


if (getResult) {
break;
}

}
return resultStr;
}
使用:
1
2
3
4
NSString *str = findNextLargerNum(@"8976");
NSLog(@"======%@",str);
//打印结果
======9678

  • 抽取widget,尽量成为一个stateless widget

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MainWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Expanded(
    child: Container(
    color: Colors.grey[700],
    child: Center(
    child: Text(
    'Hello Flutter',
    style: Theme.of(context).textTheme.display1,
    ),
    ),
    ),
    );
    }
    }
  • 更新属性值,用传值的方式

    • ChangeNotifier

      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
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      //------ ChangeNotifier class ----//
      class MyColorNotifier extends ChangeNotifier {
      Color myColor = Colors.grey;
      Random _random = new Random();

      void changeColor() {
      int randomNumber = _random.nextInt(30);
      myColor = Colors.primaries[randomNumber % Colors.primaries.length];
      notifyListeners();
      }
      }
      //------ State class ----//

      class _MyHomePageState extends State<MyHomePage> {
      final _colorNotifier = MyColorNotifier();

      void _onPressed() {
      _colorNotifier.changeColor();
      }

      @override
      void dispose() {
      _colorNotifier.dispose();
      super.dispose();
      }

      @override
      Widget build(BuildContext context) {
      print('building `MyHomePage`');
      return Scaffold(
      floatingActionButton: FloatingActionButton(
      onPressed: _onPressed,
      child: Icon(Icons.colorize),
      ),
      body: Stack(
      children: [
      Positioned.fill(
      child: BackgroundWidget(),
      ),
      Center(
      child: AnimatedBuilder(
      animation: _colorNotifier,
      builder: (_, __) => Container(
      height: 150,
      width: 150,
      color: _colorNotifier.myColor,
      ),
      ),
      ),
      ],
      ),
      );
      }
      }
  • ValueNotifier

    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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    class _MyHomePageState extends State<MyHomePage> {
    final _colorNotifier = ValueNotifier<Color>(Colors.grey);
    Random _random = new Random();

    void _onPressed() {
    int randomNumber = _random.nextInt(30);
    _colorNotifier.value =
    Colors.primaries[randomNumber % Colors.primaries.length];
    }

    @override
    void dispose() {
    _colorNotifier.dispose();
    super.dispose();
    }

    @override
    Widget build(BuildContext context) {
    print('building `MyHomePage`');
    return Scaffold(
    floatingActionButton: FloatingActionButton(
    onPressed: _onPressed,
    child: Icon(Icons.colorize),
    ),
    body: Stack(
    children: [
    Positioned.fill(
    child: BackgroundWidget(),
    ),
    Center(
    child: ValueListenableBuilder(
    valueListenable: _colorNotifier,
    builder: (_, value, __) => Container(
    height: 150,
    width: 150,
    color: value,
    ),
    ),
    ),
    ],
    ),
    );
    }
    }
  • 使用const修饰stateless widget,set state不会重复刷新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class BackgroundWidget extends StatelessWidget {
    const BackgroundWidget();

    @override
    Widget build(BuildContext context) {
    print('building `BackgroundWidget`');
    return Image.network(
    'https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569_960_720.jpg',
    fit: BoxFit.cover,
    );
    }
    }
  • 生成列表的时候使用itemExtent属性告诉系统item高度,更高效的滑动

    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
    28
    29
    30
    31
    32
    33
    34
    35
    class MyHomePage extends StatelessWidget {
    final widgets = List.generate(
    10000,
    (index) => Container(
    color: Colors.primaries[index % Colors.primaries.length],
    child: ListTile(
    title: Text('Index: $index'),
    ),
    ),
    );

    final _scrollController = ScrollController();

    void _onPressed() async {
    _scrollController.jumpTo(
    _scrollController.position.maxScrollExtent,
    );
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    floatingActionButton: FloatingActionButton(
    onPressed: _onPressed,
    splashColor: Colors.red,
    child: Icon(Icons.slow_motion_video),
    ),
    body: ListView(
    controller: _scrollController,
    children: widgets,
    itemExtent: 200,
    ),
    );
    }
    }
  • 需要动画的子控件设置给AnimatedBuilder的child,避免重复绘制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    floatingActionButton: FloatingActionButton(
    onPressed: _onPressed,
    splashColor: Colors.red,
    child: Icon(Icons.slow_motion_video),
    ),
    body: AnimatedBuilder(
    animation: _controller,
    child: CounterWidget(
    counter: counter,
    ),
    builder: (_, child) => Transform(
    alignment: Alignment.center,
    transform: Matrix4.identity()
    ..setEntry(3, 2, 0.001)
    ..rotateY(360 * _controller.value * (pi / 180.0)),
    child: child,
    ),
    ),
    );
    }
  • 合理使用opacity属性,减少调用setlayer方法,触发离屏渲染

    • 使用fadeTransition

      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
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      class _MyHomePageState extends State<MyHomePage>
      with SingleTickerProviderStateMixin {
      AnimationController _controller;
      int counter = 0;

      void _onPressed() {
      setState(() {
      counter++;
      });
      _controller.forward(from: 0.0);
      }

      @override
      void initState() {
      _controller = AnimationController(
      vsync: this, duration: const Duration(milliseconds: 600));
      _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
      _controller.reverse();
      }
      });
      super.initState();
      }

      @override
      void dispose() {
      _controller.dispose();
      super.dispose();
      }

      @override
      Widget build(BuildContext context) {
      return Scaffold(
      floatingActionButton: FloatingActionButton(
      onPressed: _onPressed,
      splashColor: Colors.red,
      child: Icon(Icons.slow_motion_video),
      ),
      body: FadeTransition(
      opacity: Tween(begin: 1.0, end: 0.0).animate(_controller),
      child: CounterWidget(
      counter: counter,
      ),
      ),
      );
      }
      }
  • 使用animatedOpacity

    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
    28
    29
    30
    31
    32
    33
    34
    35
    const duration = const Duration(milliseconds: 600);

    class _MyHomePageState extends State<MyHomePage> {
    int counter = 0;
    double opacity = 1.0;

    void _onPressed() async {
    counter++;
    setState(() {
    opacity = 0.0;
    });
    await Future.delayed(duration);
    setState(() {
    opacity = 1.0;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    floatingActionButton: FloatingActionButton(
    onPressed: _onPressed,
    splashColor: Colors.red,
    child: Icon(Icons.slow_motion_video),
    ),
    body: AnimatedOpacity(
    opacity: opacity,
    duration: duration,
    child: CounterWidget(
    counter: counter,
    ),
    ),
    );
    }
    }

完。

sources: