Flutter - Framework

线程模型

Flutter是运行Engine上来实现跨平台的,Dart支持通过isolate实现异步处理的逻辑。但是线程的管理和创建并不是有Engine负责,而是交由Embedder去实现,除了线程之外Embedder还负责了事件循环、平台插件等相关逻辑,Embedder就是Flutter和运行平台之间的中间层架构,他们的关系如下图:

架构图

Flutter Engine中需要运行四个任务,分别是:UI Task RunnerPlatform Task RunnerGPU Task RunnerI/O Task Runner,这四个任务运行在不同的线程之上。


UI Task Runner

UI Task Runner其实就是Flutter的UI运行任务,也就是我们所说的UI线程,因为UI Task Runner会执行的Dart的root isolate去齐总运行App main方法,并且为其绑定了UI渲染提交的回调等。这也是为什么称Flutter是单线程应用的原因,这里指的就是UI Task Runner。UI Task Runner除了处理UI之外还处理了microTasks、插件消息的响应处理等相关逻辑。

UI Task Runner

谷歌官方的UI渲染绘制流程:


image-20210318151533200.png


GPU Task Runner

当UI Task Runner生成Layer Tree之后,接下来的处理就会给GPU Task Runner处理,GPU Task Runner会把Layer Tree转化为Skia所需要的绘制指令,通过延迟调度和Buffer来保证绘制任务的流程运行,下图就是GPU的整个流程

image-20210318152725197.png


I/O Task Runner

I/O Task Runner就是顾名思义具备读写能力线程,比如图片数据的获取、解析成渲染数据等耗时操作,这些都属于耗时操作,交给其他三个Task Runner处理明显不合适,所以对于需要耗时的操作,一般都是通过对I/O Task Runner处理。


Platform Task Runner

Platform Task RunnerFlutter Engine的主线程,在Flutter 中所有和Engine调用都会通过Platform Task Runner,之所以这样子做是为了保证线程安全。但是也带来了一个弊端,某个任务的处理造成了严重堵塞的时候,可能会引发应用的ANR奔溃。

那么对与一些耗时的操作,我们应该放到哪里去处理呢?答案就是isolate


isolate

isolate运行我们开辟一个线程,由于isolate的特性(数据不能互通),所以也不要锁。在实际开发中比如json等一些耗时操作我们就可以使用isolate去处理。

isolate对比传统的线程最大区别是:

  • 数据不共享
  • 只能通过port进行通信
  • 每个isolate都有自己的内存和任务管理


单线程运行

之说以说Flutter是单线程应用,是因为Dart数据单线程运行机制,而这个机制主要是通过消息循环机制任务调度处理,其中有两个任务队列:microTask queueEvent queue

其中microTask优先级高于Event queue,从下面的运行流程图就可以很明显的看出。

实际开发中也要注意由于microTask的特性(优先级高),如果频繁把任务插入到microTask中去执行,就可能会造成UI卡顿和掉帧的现象。

[图片上传失败...(image-ff934f-1616915674307)]


async/await、Future

通常我们使用async/awaitFuture来实现异步的操作,但实际上这并不是真正的异步,而是在单线程上的任务调度,也称之为协程协程是不具备线程一样并发执行的能力。简单的说就是当程序执行被标注了async方法,运行到await的时候,表示这个方法需要等待结果,此时就会跳过这个片段代码继续执行其他逻辑,然后在程序在下一次轮询的时候,在判断是否有了返回结果,如果就执行,没有就继续等待下一个轮询。

比如我们要在StatefulWidget中的initState方法中通过context获取一些数据,如果我们直接使用,就会抛出异常,但是我们使用Future.delay(duration: Duration(seconds: 0)).then(() {...})方法包装起来之后,则不会出现该问题。这是因为获取context的逻辑会被放到下一个轮询中 被执行


动画

我们知道,动画其实是由帧构成的,每一帧都是一张图片,多张图片连续执行,就形成了动画。


普通动画

之前我们说过Widget是不可变的,那么Widget是如何产生动画的呢?

我们用一个示例来看看,一个简单的放大动画的实现。

示例代码
class AnimationControllerPage extends StatefulWidget {
  @override
  _AnimationControllerPageState createState() =>
      _AnimationControllerPageState();
}

class _AnimationControllerPageState extends State<AnimationControllerPage>
    with SingleTickerProviderStateMixin {
  AnimationController _animationController;

  double size = 100;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      lowerBound: 100,
      upperBound: 200,
      duration: Duration(milliseconds: 500),
    );
    _animationController.addListener(() {
      setState(() {
        size = _animationController.value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("AnimationController"),
      ),
      body: Center(
        child: InkWell(
          onTap: () {
            if (_animationController.value == 200) {
              _animationController.reverse();
            } else {
              _animationController.forward();
            }
          },
          child: Container(
            width: size,
            height: size,
            color: Colors.yellow,
            child: Center(
              child: Text(_animationController.value == 200 ? "点我缩小" : "点我放大"),
            ),
          ),
        ),
      ),
    );
  }

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


实现分析

当我们要实现一个从100→200的矩形变大动画的时候,最重要的就是每一帧都让这个矩形变大一点,每一帧变大多少,就通过动画总时长和矩形大小的比(不考虑Curve),然后得到每一帧的宽高值,在把值设置给矩形。


所以我们需要三个条件:
  • 可以更新
  • 并且是每一帧都会有回调
  • 能帮我们计算没帧的宽高值的对象


分析上面的代码

  • 首先这个是StatefulWidget

    这里使用它的关键地方就是他可以使用setState进行跟新UI操作

  • 其次我们混入了SingleTickerProviderStateMixin

    提供了每一帧的绘制回调Tick

  • 然后我们使用了AnimationController

    通过TickerProviderStateMixin每一帧的回调,在使用AnimationController内部的simulation计算单位时间内value的值,最后通过setState进行更新

内部_tick的源码示例:

  void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    assert(elapsedInSeconds >= 0.0);
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation!.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }

最后我们发现上面使用的是SingleTickerProviderStateMixin如果我们要在一个页面使用多个动画的时候,只需要把SingleTickerProviderStateMixin换成TickerProviderStateMixin

至于具体的动画案例,后续出对应的文章。


路由动画

路由动画指的就是我们在页面进行切换的时候两个页面展示的动画。系统默认给我们实现了一套,代码在theme的pageTransitionTheme设置。我们可以在这里进行自定义,同时也可以在push的时候自定义PageRouterBuilder,在设置transitionsBuilder设置动画效果。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Animation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        pageTransitionsTheme: PageTransitionsTheme(
            builders: <TargetPlatform, PageTransitionsBuilder>{
              TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
              TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
            }),
      ),
    );
  }
}


/// 自定义PageRouterBuilder
goNextPage(BuildContext context) {
  var router = PageRouteBuilder(
      pageBuilder: (BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation) {
        return Page();
      },
      transitionsBuilder: (BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation, Widget child) {
        return SlideTransition(position: Tween<Offset>(
          begin: Offset(0.0, 0.0),
          end: Offset(0.0, 0.0),
        ).animate(animation), child: child,);
      });

  return Navigator.push(context, router);
}


Hero动画

使用Hero动画的时候我们需要注意两个Hero Widget分别位于两个页面当中,但是两个Hero的tag必须一致。

如以下示例代码:

const String _heroTag = '_heroTag';
const String _imgUrl =
    "https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1915138504,138225023&fm=111&gp=0.jpg";

class HeroAnimatedPage extends StatefulWidget {
  @override
  _HeroAnimatedPageState createState() => _HeroAnimatedPageState();
}

class _HeroAnimatedPageState extends State<HeroAnimatedPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hero"),
      ),
      body: Center(
        child: InkWell(
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute(builder: (context) {
              return HeroPage();
            }));
          },
          child: SizedBox(
            width: 100,
            height: 100,
            child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
          ),
        ),
      ),
    );
  }
}

class HeroPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("内页")),
      body: Center(
        child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
      ),
    );
  }
}

可以看到使用Hero动画的过程很简单,那么系统层如何帮助我们做到的?

这是因为我们在使用MaterialApp的时候,其内部的_MaterialAppState已经帮我们初始化了HeroController的示例对象,并且HeroControllerNavigatorObserver的子类,可以订阅路由的跳转事件,HeroController监听到跳转之后就负责生成Hero的动画和执行


static HeroController createMaterialHeroController() {
  return HeroController(
    createRectTween: (Rect begin, Rect end) {
      /// 可以看到这里传入了开始和结束的Rect
      return MaterialRectArcTween(begin: begin, end: end);
    },
  );
}

class _MaterialAppState extends State<MaterialApp> {
  HeroController _heroController;

  @override
  void initState() {
    super.initState();
    /// 这里初始化了 HeroController
    _heroController = MaterialApp.createMaterialHeroController();
  }
  
    @override
  Widget build(BuildContext context) {
    Widget result = _buildWidgetApp(context);

    assert(() {
      if (widget.debugShowMaterialGrid) {
        result = GridPaper(
          color: const Color(0xE0F9BBE0),
          interval: 8.0,
          divisions: 2,
          subdivisions: 1,
          child: result,
        );
      }
      return true;
    }());

    return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      /// 可以看到这里使用HeroControllerScope包装起来了,其内部是继承InheritedWidget
      /// 当下个页面也有Hero Widget的时候,且tag一样,那么就可以开启Hero动画
      child: HeroControllerScope(
        controller: _heroController,
        child: result,
      )
    );
  }
}


/// 以下的代码片段截取自_HeroFlight,可以点击Hero查看源码得到
Positioned(
 top: offsets.top,
 right: offsets.right,
 bottom: offsets.bottom,
 left: offsets.left,
 child: IgnorePointer(
   child: RepaintBoundary(
     child: Opacity(
       opacity: _heroOpacity.value,
       child: child,
     ),
   ),
 ),
);

通过对源码的查看,我们知道Hero动画就是页面开始跳转的时候,通过计算原Rect和结束时目标的Rect之后,通过Positioned改变自身的位置,最后实现动画的展示


Rive 动画

这个更多的是设计通过工具导出各种绚丽的动画给到我们,然后我们在给展示到屏幕中。

比如下面的这个动画,如果自己去使用绘制效果去处理,那么绝对是一件头疼的事情,但是引入Rive动画就可以很好的解决这个问题,并且如果你仔细的观察下面的动画,你会发现动画是可以分别控制的(雨刮器和车子)。

当然除了这个Lottie也有对应的Flutter package支持。

rive.gif

这里把示例代码展示一下

需要注意的是资源文件你需要自行去这里下载,然后倒入rivepackage,之后就可以实现上图的动画效果了。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

class RiveDemoPage extends StatefulWidget {
  @override
  _RiveDemoPageState createState() => _RiveDemoPageState();
}

class _RiveDemoPageState extends State<RiveDemoPage> with SingleTickerProviderStateMixin{
  void _togglePlay() {
    setState(() => _controller.isActive = !_controller.isActive);
  }

  /// Tracks if the animation is playing by whether controller is running.
  bool get isPlaying => _controller?.isActive ?? false;

  Artboard _artboard;
  RiveAnimationController _controller;
  WiperAnimation _wipersController;
  // flag to turn on and off the wipers
  bool _wipers = false;
  @override
  void initState() {
    _loadRiveFile();
    super.initState();
  }

  /// Loads a Rive file
  void _loadRiveFile() async {
    final bytes = await rootBundle.load('assets/off_road_car.riv');
    final file = RiveFile();
    if (file.import(bytes)) {
      setState(() => _artboard = file.mainArtboard
      /// idle 控制车辆
        ..addController(_controller = SimpleAnimation('idle')));
    }
  }

  void _wipersChange(bool wipersOn) {
    if (_wipersController == null) {
      _artboard.addController(
        /// 控制雨刮器
        _wipersController = WiperAnimation('windshield_wipers'),
      );
    }
    setState(() {
      if (_wipers) {
        _wipersController.stop();
      } else {
        _wipersController.start();
      }
      _wipers = !_wipers;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("自定义动画"),),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(child: _artboard == null
                ? const SizedBox()
                : Rive(artboard: _artboard),),
            SizedBox(
              height: 50,
              width: 200,
              child: SwitchListTile(
                title: const Text('雨刮器'),
                value: _wipers,
                onChanged: _wipersChange,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _togglePlay,
        tooltip: isPlaying ? 'Pause' : 'Play',
        child: Icon(
          isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
}


class WiperAnimation extends SimpleAnimation {
  WiperAnimation(String animationName) : super(animationName);

  start() {
    instance.animation.loop = Loop.loop;
    isActive = true;
  }

  stop() => instance.animation.loop = Loop.oneShot;
}


手势与触摸

当我们对屏幕区域中的任意一点进行点击的时候,这个事件是如何准确找到是谁处理或者抛弃该事件的呢?由于Flutter的跨平台特性,所以一个事件的传递需要经过原生层、中间层、Dart层。对于原生层和中间层会有各自的处理逻辑,从Dart层开始就进入了Flutter的领域了。

手势和触摸


事件流程

通过上图可以看到,在Dart层中事件都是从_dispatchPointerDataPacket开始,之后会通过Zone判断环境进行回调,然后返回到GestureBinding类中的_handlePointEvent方法,下面就是截取的_handlePointerEvent方法的内部代码

 void _handlePointerEvent(PointerEvent event) {
    assert(!locked);
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      /// 检测并添加合法控件成员列表
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $hitTestResult');
        return true;
      }());
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
        // 抬起,取消事件,不用hitTest,移除
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down) {
      // Because events that occur with the pointer down (like
      // PointerMoveEvents) should be dispatched to the same place that their
      // initial PointerDownEvent was, we want to re-use the path we found when
      // the pointer went down, rather than do hit detection each time we get
      // such an event.
      hitTestResult = _hitTests[event.pointer];
    }
    assert(() {
      if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
        debugPrint('$event');
      return true;
    }());
    if (hitTestResult != null ||
        event is PointerHoverEvent ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      /// 满足上面条件之后,开始分发事件
      dispatchEvent(event, hitTestResult);
    }
  }


hitTest

hitTest方法主要是为了获取到一个HitTestResult,这个hitTestResult内用一个List<HisTestEntry>用于保存竞技对象,而每个HitTestEntry.target都会存储每个空间对应的RenderObject对象(因为RenderObject实现了HitTestTarget接口)


dispatchEvent

dispatchEvent主要是对事件进行分发。通过上面的hitTest方法之后,我们得到了所有的竞技对象列表,然后就挨个的进行遍历HitTestEntry对象,调用他们的handleEvent方法:

下面代码删除了部分error的处理

 @override // from HitTestDispatcher
  void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
    assert(!locked);
    if (hitTestResult == null) {
      assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
      }
      return;
    }
    /// 遍历HitTestEntry对象列表
    for (final HitTestEntry entry in hitTestResult.path) {
      try {
        /// 分别调用renderObject的handleEvent方法
        entry.target.handleEvent(event.transformed(entry.transform), entry);
      } catch (exception, stack) {
      }
    }
  }

这里可能会出现一种情况:如果一个区域的内的多个控件都实现了HandleEvent方法,此时最后的处理权交给那个控件呢?这就设计了事件竞争


事件竞争

Flutter设计事件竞争的时候,定义了一个概念:通过一个竞技场,各个控件参与竞争,但只有符合下面两个条件中的任意一个的时候,才可以获得事件的处理权。

  • 最后得到直接胜利的控件(理解为这个区域就只有他)
  • 活到最后的空间中排在第一为的控件(这个区域有多个,但是列表中排第一个的才享有处理权)

也就是说当Down事件时通过addPointer加入GestureRecognizer竞技场,如果区域内只有他一个响应者,那么会在Up的时候进行_checkUp,直接就完成竞争流程,如果这个区域内有多个响应,那么Down的时候就决定不了谁是最终响应者,则会在Up的时候直接把列表中的第一个作为响应者。


滑动事件

滑动事件的处理,也要求控件在Down流程中通过addPointer加入竞技场,然后在Move流程中,通过PointerRouter.route执行DragGestureRecoginzer.handleEvent处理事件。

image-20210322151936319.png

PointerMoveEvent事件里,DragGestureRecognizer.handleEvent会通过_hasSufficientPendingDragDeltaToAccept方法判断是否符合条件,如果符合条件直接执行resolve(GestureDisposition.accepted),之后流程回到竞技场执行acceptGesture,触发onStartonUpdate去通知上层处理事件。

onUpdate事件回去更新Offset,然后通过markNeedsLayout是的ViewPort重新布局,让界面看起来像滚动起来。


滑动Physic

我们都知道滑动结束后不会马上停止,而是有一个物理的减弱动画的实现,那么他是怎么实现的呢?

官方给我们定义了四种ScrollPhysics

  • BouncingScrollPhysics 允许滚动超出边界,松手之后内容会进行回弹效果
  • ClampingScrollPhysics 防止超出边界,夹住收尾区域
  • AlwaysScrollableScrollPhysics 始终响应用户的滚动,松手之后内容会进行回弹效果
  • NeverScrollableScrollPhysics 不响应用户的滚动

部分情况下我们不会特意去设置physics,细心的你可能会发现在iOSAndroid上面ListViewCustomScrollView等控件,拖拽效果会有不同,为啥子呢?这是因为在ScrollConfiguration内部进行特别的处理。


ScrollConfiguration

实现滑动溢出的效果关键的两个雷就是ScrollConfigurationScrollBehavior。在Scrollable_updatePosition源码中,里面有一个关键的判断,如果widget.physics==null的时候,就会使用ScrollConfiguration.of(context)getScrollPhysics(context)方法获取_physicsScrollConfiguration.of(context)返回是一个InheritedWidget对象ScrollBehavior

  void _updatePosition() {
    _configuration = ScrollConfiguration.of(context);
    _physics = _configuration.getScrollPhysics(context);
    /// 这里如果不为空才使用自己的,否则就使用从ScrollConfiguration中获取的physics
    if (widget.physics != null)
      _physics = widget.physics.applyTo(_physics);
    
    final ScrollController controller = widget.controller;
    final ScrollPosition oldPosition = position;
    if (oldPosition != null) {
      controller?.detach(oldPosition);
      scheduleMicrotask(oldPosition.dispose);
    }

    _position = controller?.createScrollPosition(_physics, this, oldPosition)
      ?? ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
    assert(position != null);
    controller?.attach(position);
  }

所以在我们不填写physics属性的情况下,我们获取的physics就要看getScrollPhysics(context)这个方法的内部实现

    static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
  static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());

  ScrollPhysics getScrollPhysics(BuildContext context) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        return _bouncingPhysics;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        return _clampingPhysics;
    }
    return null;
  }

上面的源码就可以很好的展示了默认情况下的为什么列表在iOSAndroid平台中会有不同滑动效果的原因。


然后我们去查看ScrollConfiguration.of方法

  static ScrollBehavior of(BuildContext context) {
    final ScrollConfiguration configuration = context.dependOnInheritedWidgetOfExactType<ScrollConfiguration>();
    return configuration?.behavior ?? const ScrollBehavior();
  }

发现这个类是InheritedWidget,我们知道InheritedWidget的特性(数据向下传递),那么他是在哪里被初始化的呢?最终在MaterialApp的实现中找到了,代码截图如下:

    /// 此为代码片段,删除了部分无关代码
    @override
  Widget build(BuildContext context) {
    Widget result = _buildWidgetApp(context);
    
    /// 发现这里是ScrollConfiguration包装起来的对象
    return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      child: HeroControllerScope(
        controller: _heroController,
        child: result,
      )
    );
  }


ScrollPhysics

通过查看ScrollConfiguration的相关实现,我们知道了为什么不同的平台会有不同的拖曳处理,但是怎么去实现这个效果则主要是通过ScrollPhysics的子类去实现,ScrollPhysics本身只是定义了一些属性和方法,其中几个比较重要的方法如下:

class ScrollPhysics {
  // 将用户拖曳距离Offset转化为需要移动的pixels
  // 如果没有父类就直接返回offset,否则就调用父类的applyPhysicsToUserOffset方法
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    if (parent == null)
      return offset;
    return parent.applyPhysicsToUserOffset(position, offset);
  }
  
  // 返回边界条件,如果是0,overscroll则一直就是0
  // 如果没有父类,直接返回0,否则就调用父类的同名方法
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    if (parent == null)
      return 0.0;
    return parent.applyBoundaryConditions(position, value);
  }
  
  /// 创建一个滚动的模拟器,这个处理器就是处理阻尼、滑动、回弹效果的具体实现
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    if (parent == null)
      return null;
    return parent.createBallisticSimulation(position, velocity);
  }
  
  /// 最小的滑动速率 
  /// const double kMinFlingVelocity = 50.0 默认是50
  double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
  
  
  /// 传输动量,返回重复滚动的速度
  /// 如果父类为空就直接返回0 否则调用父类同名方法
  double carriedMomentum(double existingVelocity) {
    if (parent == null)
      return 0.0;
    return parent.carriedMomentum(existingVelocity);
  }
  
    /// 最小的开始拖曳距离
  /// 如果返回空,则不执行最低阈值。
  double get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
  
  /// 滚动模拟的公差
  /// 指定距离、持续时间和速度差应视为平等的差异的结构
  Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
  
  /// 这个就是默认实现
  static final Tolerance _kDefaultTolerance = Tolerance(
    // TODO(ianh): Handle the case of the device pixel ratio changing.
    // TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
    velocity: 1.0 / (0.050 * WidgetsBinding.instance.window.devicePixelRatio), // logical pixels per second
    distance: 1.0 / WidgetsBinding.instance.window.devicePixelRatio, // logical pixels
  );
}

所以ScrollPhysics的具体实现就依赖Scrollable的触摸响应流程中,主要的逻辑关键地方就是下面的是三个方法:

  • applyPhysicsToUserOffset: 通过physics将用户拖曳距离offset转化为setPixels(滚动)的增量
  • applyBoundaryConditions:通过physics计算当前滚动的边界条件
  • createBallisticSimulation:创建自动滑动的模拟器


这三个触发的时机分别在于:_handleDragUpdate_handleDragCancel_handleDragEnd也就是拖曳的过程和结束的时机

  • applyPhysicsToUserOffsetapplyBoundaryConditions是在_handleDragUpdate时触发计算
  • createBallisticSimulation是在_handleDragCancel_handleDragEnd时触发
image-20210323112605356.png


图片加载

Flutter加载图片一般有两种方式:ImageDecorationImage,其中DecorationImage用于DecoratedBox中。

两者不同之处是Image是一个StatefulWidget,其内部通过RawImage实现图片的绘制;DecorationImage并不是控件,只是单纯图片绘制,内部通过DecorationImagePainter直接绘制图片。正因如此,DecorationImage对比Image少了一些可定制化的配置。

两者相同的地方则是都需要ImageProvider实现图片的加载和数据转换。只是Image控件提供了Image.asset方法对AssetImage的封装,其实他们最终都会通过decoration_image.dart下的paintImage方法实现图片的绘制逻辑。


下表是Image的参数列表

参数 描述
width/height 图片区域的宽高,需要注意不是图片的宽高
fit 填充模式
BoxFit.fill 铺满设置的区域,可能会拉伸
BoxFit.fitHeight 填充高度,可能会裁剪、拉伸
BoxFit.fitWidth 填充宽度、可能会裁剪、拉伸
BoxFit.contain 居中显示,显示原比例,可能不会填满
BoxFit.cover 原比例进行裁剪并填满容器
BoxFit.scaleDown 和contain类似,但是只会缩小不会放大
color 前景色,会覆盖图片颜色,多数情况和colorBlendMode配合使用
colorBlendMode 与color参数结合使用,设置color的回合模式
alignment 对齐方式
repeat 是否重复(当图片填充不满设置的width/height)
centerSlice 设置图片的拉伸区域,需要注意只有在大于width/height的情况下才可以使用这个属性
matchTextDirection 需要配合Directionality进行使用,一般配合文本对齐
gaplessPlayback 图片更新之后,是否将原图片进行保留之后在显示
filterQuality 图片显示的过滤质量
loadingBuilder 图片加载中的显示 返回Widget
frameBuilder 对需要显示的图片进行定制处理


Image Demo

class ImageDemoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Image Demo")),
      body: Center(
        child: Image(
          width: 100,
          height: 100,
          fit: BoxFit.cover,
          image: NetworkImage("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1421194919,3695584663&fm=26&gp=0.jpg"),
          frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
            return Container(
              decoration: BoxDecoration(
                boxShadow: [BoxShadow(color: Colors.grey, blurRadius: 1, spreadRadius: 1)],
              ),
              child: child,
            );
          },
          loadingBuilder: (context, child, loadingProgress) {
            if (loadingProgress == null)
              return child;
            return Center(
              child: CircularProgressIndicator(
                value: loadingProgress.expectedTotalBytes != null
                    ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes
                    : null,
              ),
            );
          },
        ),
      ),
    );
  }
}

效果图如下:

图片处理效果


图片加载流程

图片加载流程


图片缓存

Flutter中对图片是进行了缓存的(但是没有做本地缓存),缓存的方法实现在ImageCache中的putIfAbsent方法。

ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? onError }) {
    
    TimelineTask? timelineTask;
    TimelineTask? listenerTask;
    if (!kReleaseMode) {
      timelineTask = TimelineTask()..start(
        'ImageCache.putIfAbsent',
        arguments: <String, dynamic>{
          'key': key.toString(),
        },
      );
    }
    ImageStreamCompleter? result = _pendingImages[key]?.completer;
    // 还在加载,直接返回
    if (result != null) {
      if (!kReleaseMode) {
        timelineTask!.finish(arguments: <String, dynamic>{'result': 'pending'});
      }
      return result;
    }
    
      // 已经缓存过了,直接返回
    final _CachedImage? image = _cache.remove(key);
    if (image != null) {
      if (!kReleaseMode) {
        timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
      }
      _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
      _cache[key] = image;
      return image.completer;
    }

  
    final _CachedImage? liveImage = _liveImages[key];
    if (liveImage != null) {
      _touch(key, liveImage, timelineTask);
      if (!kReleaseMode) {
        timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
      }
      return liveImage.completer;
    }

    try {
      result = loader();
      _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
    } catch (error, stackTrace) {
      if (!kReleaseMode) {
        timelineTask!.finish(arguments: <String, dynamic>{
          'result': 'error',
          'error': error.toString(),
          'stackTrace': stackTrace.toString(),
        });
      }
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }

    if (!kReleaseMode) {
      listenerTask = TimelineTask(parent: timelineTask)..start('listener');
    }
    // If we're doing tracing, we need to make sure that we don't try to finish
    // the trace entry multiple times if we get re-entrant calls from a multi-
    // frame provider here.
    bool listenedOnce = false;

    // We shouldn't use the _pendingImages map if the cache is disabled, but we
    // will have to listen to the image at least once so we don't leak it in
    // the live image tracking.
    // If the cache is disabled, this variable will be set.
    _PendingImage? untrackedPendingImage;
    void listener(ImageInfo? info, bool syncCall) {
      // Images that fail to load don't contribute to cache size.
      final int imageSize = info == null || info.image == null ? 0 : info.image.height * info.image.width * 4;

      final _CachedImage image = _CachedImage(result!, imageSize);

      _trackLiveImage(
        key,
        _LiveImage(
          result,
          imageSize,
          () => _liveImages.remove(key),
        ),
      );

      final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }
      // Only touch if the cache was enabled when resolve was initially called.
      if (untrackedPendingImage == null) {
        _touch(key, image, listenerTask);
      }

      if (!kReleaseMode && !listenedOnce) {
        listenerTask!.finish(arguments: <String, dynamic>{
          'syncCall': syncCall,
          'sizeInBytes': imageSize,
        });
        timelineTask!.finish(arguments: <String, dynamic>{
          'currentSizeBytes': currentSizeBytes,
          'currentSize': currentSize,
        });
      }
      listenedOnce = true;
    }

    final ImageStreamListener streamListener = ImageStreamListener(listener);
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = _PendingImage(result, streamListener);
    } else {
      untrackedPendingImage = _PendingImage(result, streamListener);
    }
    // Listener is removed in [_PendingImage.removeListener].
    result.addListener(streamListener);

    return result;
  } 

由于图片的缓存都是异步的方式,他并不知道图片会消耗多少内存(在未解码之前),所以当一个页面加载了大量图片,此时就有可能会造成内存不够的情况,在iOS上面的表现就是会被系统给杀掉。

所以如果对内存缓存大小和数量有要求的话,可以通过PaintingBinding.instacne.imageCache.maximunSize进行设置(默认是100m),同时最好在页面不可见的时候暂停图片的I/O和下载。


网络请求

Flutter中的网络请求是不经过原生端的,所以我们使用的是Dart层的网络请求服务。

HttpClient

/// 创建Client
HttpClient httpClient = HttpClient();
/// 地址
Uri uri = Uri(scheme: "https", host: "example.com");
/// 得到request
HttpClientRequest request = await httpClient.getUrl(uri);
/// 添加请求头
request.header.add("token": "xxxxx");
/// 等待请求结果
HttpClientResponse response = await request.close();
/// 解析数据
String responseBody = await response.transform(utf8.decoder).join();
/// 关闭client
httpClient.close();

一般情况下,我们很少使用到HttpClient来进行网络请求,在Flutter大部分还是使用第三方封装好的SDK,比如Dio


Dio

Flutter中出名的网络请求框架,除了基本的网络请求之外还设置了拦截器、代理等相当使用的功能。

具体的实现可以去GitHub或者pub.dev查看

import 'package:dio/dio.dart';
void getHttp() async {
  try {
    Response response = await Dio().get("http://www.google.com");
    print(response);
  } catch (e) {
    print(e);
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容