Flutter - Framework

線程模型

Flutter是運(yùn)行Engine上來(lái)實(shí)現(xiàn)跨平臺(tái)的,Dart支持通過(guò)isolate實(shí)現(xiàn)異步處理的邏輯。但是線程的管理和創(chuàng)建并不是有Engine負(fù)責(zé),而是交由Embedder去實(shí)現(xiàn),除了線程之外Embedder還負(fù)責(zé)了事件循環(huán)、平臺(tái)插件等相關(guān)邏輯,Embedder就是Flutter和運(yùn)行平臺(tái)之間的中間層架構(gòu),他們的關(guān)系如下圖:

架構(gòu)圖

Flutter Engine中需要運(yùn)行四個(gè)任務(wù),分別是:UI Task RunnerPlatform Task Runner、GPU Task RunnerI/O Task Runner,這四個(gè)任務(wù)運(yùn)行在不同的線程之上。


UI Task Runner

UI Task Runner其實(shí)就是Flutter的UI運(yùn)行任務(wù),也就是我們所說(shuō)的UI線程,因?yàn)閁I Task Runner會(huì)執(zhí)行的Dart的root isolate去齊總運(yùn)行App main方法,并且為其綁定了UI渲染提交的回調(diào)等。這也是為什么稱(chēng)Flutter是單線程應(yīng)用的原因,這里指的就是UI Task Runner。UI Task Runner除了處理UI之外還處理了microTasks、插件消息的響應(yīng)處理等相關(guān)邏輯。

UI Task Runner

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


image-20210318151533200.png


GPU Task Runner

當(dāng)UI Task Runner生成Layer Tree之后,接下來(lái)的處理就會(huì)給GPU Task Runner處理,GPU Task Runner會(huì)把Layer Tree轉(zhuǎn)化為Skia所需要的繪制指令,通過(guò)延遲調(diào)度和Buffer來(lái)保證繪制任務(wù)的流程運(yùn)行,下圖就是GPU的整個(gè)流程

image-20210318152725197.png


I/O Task Runner

I/O Task Runner就是顧名思義具備讀寫(xiě)能力線程,比如圖片數(shù)據(jù)的獲取、解析成渲染數(shù)據(jù)等耗時(shí)操作,這些都屬于耗時(shí)操作,交給其他三個(gè)Task Runner處理明顯不合適,所以對(duì)于需要耗時(shí)的操作,一般都是通過(guò)對(duì)I/O Task Runner處理。


Platform Task Runner

Platform Task RunnerFlutter Engine的主線程,在Flutter 中所有和Engine調(diào)用都會(huì)通過(guò)Platform Task Runner,之所以這樣子做是為了保證線程安全。但是也帶來(lái)了一個(gè)弊端,某個(gè)任務(wù)的處理造成了嚴(yán)重堵塞的時(shí)候,可能會(huì)引發(fā)應(yīng)用的ANR奔潰。

那么對(duì)與一些耗時(shí)的操作,我們應(yīng)該放到哪里去處理呢?答案就是isolate


isolate

isolate運(yùn)行我們開(kāi)辟一個(gè)線程,由于isolate的特性(數(shù)據(jù)不能互通),所以也不要鎖。在實(shí)際開(kāi)發(fā)中比如json等一些耗時(shí)操作我們就可以使用isolate去處理。

isolate對(duì)比傳統(tǒng)的線程最大區(qū)別是:

  • 數(shù)據(jù)不共享
  • 只能通過(guò)port進(jìn)行通信
  • 每個(gè)isolate都有自己的內(nèi)存和任務(wù)管理


單線程運(yùn)行

之說(shuō)以說(shuō)Flutter是單線程應(yīng)用,是因?yàn)?code>Dart數(shù)據(jù)單線程運(yùn)行機(jī)制,而這個(gè)機(jī)制主要是通過(guò)消息循環(huán)機(jī)制任務(wù)調(diào)度處理,其中有兩個(gè)任務(wù)隊(duì)列:microTask queueEvent queue。

其中microTask優(yōu)先級(jí)高于Event queue,從下面的運(yùn)行流程圖就可以很明顯的看出。

實(shí)際開(kāi)發(fā)中也要注意由于microTask的特性(優(yōu)先級(jí)高),如果頻繁把任務(wù)插入到microTask中去執(zhí)行,就可能會(huì)造成UI卡頓和掉幀的現(xiàn)象。

[圖片上傳失敗...(image-ff934f-1616915674307)]


async/await、Future

通常我們使用async/awaitFuture來(lái)實(shí)現(xiàn)異步的操作,但實(shí)際上這并不是真正的異步,而是在單線程上的任務(wù)調(diào)度,也稱(chēng)之為協(xié)程。協(xié)程是不具備線程一樣并發(fā)執(zhí)行的能力。簡(jiǎn)單的說(shuō)就是當(dāng)程序執(zhí)行被標(biāo)注了async方法,運(yùn)行到await的時(shí)候,表示這個(gè)方法需要等待結(jié)果,此時(shí)就會(huì)跳過(guò)這個(gè)片段代碼繼續(xù)執(zhí)行其他邏輯,然后在程序在下一次輪詢的時(shí)候,在判斷是否有了返回結(jié)果,如果就執(zhí)行,沒(méi)有就繼續(xù)等待下一個(gè)輪詢。

比如我們要在StatefulWidget中的initState方法中通過(guò)context獲取一些數(shù)據(jù),如果我們直接使用,就會(huì)拋出異常,但是我們使用Future.delay(duration: Duration(seconds: 0)).then(() {...})方法包裝起來(lái)之后,則不會(huì)出現(xiàn)該問(wèn)題。這是因?yàn)?strong>獲取context的邏輯會(huì)被放到下一個(gè)輪詢中 被執(zhí)行。


動(dòng)畫(huà)

我們知道,動(dòng)畫(huà)其實(shí)是由幀構(gòu)成的,每一幀都是一張圖片,多張圖片連續(xù)執(zhí)行,就形成了動(dòng)畫(huà)。


普通動(dòng)畫(huà)

之前我們說(shuō)過(guò)Widget是不可變的,那么Widget是如何產(chǎn)生動(dòng)畫(huà)的呢?

我們用一個(gè)示例來(lái)看看,一個(gè)簡(jiǎn)單的放大動(dòng)畫(huà)的實(shí)現(xiàn)。

示例代碼
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 ? "點(diǎn)我縮小" : "點(diǎn)我放大"),
            ),
          ),
        ),
      ),
    );
  }

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


實(shí)現(xiàn)分析

當(dāng)我們要實(shí)現(xiàn)一個(gè)從100→200的矩形變大動(dòng)畫(huà)的時(shí)候,最重要的就是每一幀都讓這個(gè)矩形變大一點(diǎn),每一幀變大多少,就通過(guò)動(dòng)畫(huà)總時(shí)長(zhǎng)和矩形大小的比(不考慮Curve),然后得到每一幀的寬高值,在把值設(shè)置給矩形。


所以我們需要三個(gè)條件:
  • 可以更新
  • 并且是每一幀都會(huì)有回調(diào)
  • 能幫我們計(jì)算沒(méi)幀的寬高值的對(duì)象


分析上面的代碼

  • 首先這個(gè)是StatefulWidget

    這里使用它的關(guān)鍵地方就是他可以使用setState進(jìn)行跟新UI操作

  • 其次我們混入了SingleTickerProviderStateMixin

    提供了每一幀的繪制回調(diào)Tick

  • 然后我們使用了AnimationController

    通過(guò)TickerProviderStateMixin每一幀的回調(diào),在使用AnimationController內(nèi)部的simulation計(jì)算單位時(shí)間內(nèi)value的值,最后通過(guò)setState進(jìn)行更新

內(nèi)部_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();
  }

最后我們發(fā)現(xiàn)上面使用的是SingleTickerProviderStateMixin,如果我們要在一個(gè)頁(yè)面使用多個(gè)動(dòng)畫(huà)的時(shí)候,只需要把SingleTickerProviderStateMixin換成TickerProviderStateMixin。

至于具體的動(dòng)畫(huà)案例,后續(xù)出對(duì)應(yīng)的文章。


路由動(dòng)畫(huà)

路由動(dòng)畫(huà)指的就是我們?cè)陧?yè)面進(jìn)行切換的時(shí)候兩個(gè)頁(yè)面展示的動(dòng)畫(huà)。系統(tǒng)默認(rèn)給我們實(shí)現(xiàn)了一套,代碼在theme的pageTransitionTheme設(shè)置。我們可以在這里進(jìn)行自定義,同時(shí)也可以在push的時(shí)候自定義PageRouterBuilder,在設(shè)置transitionsBuilder設(shè)置動(dòng)畫(huà)效果。

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動(dòng)畫(huà)

使用Hero動(dòng)畫(huà)的時(shí)候我們需要注意兩個(gè)Hero Widget分別位于兩個(gè)頁(yè)面當(dāng)中,但是兩個(gè)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("內(nèi)頁(yè)")),
      body: Center(
        child: Hero(tag: _heroTag, child: Image.network(_imgUrl)),
      ),
    );
  }
}

可以看到使用Hero動(dòng)畫(huà)的過(guò)程很簡(jiǎn)單,那么系統(tǒng)層如何幫助我們做到的?

這是因?yàn)槲覀冊(cè)谑褂?code>MaterialApp的時(shí)候,其內(nèi)部的_MaterialAppState已經(jīng)幫我們初始化了HeroController的示例對(duì)象,并且HeroControllerNavigatorObserver的子類(lèi),可以訂閱路由的跳轉(zhuǎn)事件,HeroController監(jiān)聽(tīng)到跳轉(zhuǎn)之后就負(fù)責(zé)生成Hero的動(dòng)畫(huà)和執(zhí)行


static HeroController createMaterialHeroController() {
  return HeroController(
    createRectTween: (Rect begin, Rect end) {
      /// 可以看到這里傳入了開(kāi)始和結(jié)束的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包裝起來(lái)了,其內(nèi)部是繼承InheritedWidget
      /// 當(dāng)下個(gè)頁(yè)面也有Hero Widget的時(shí)候,且tag一樣,那么就可以開(kāi)啟Hero動(dòng)畫(huà)
      child: HeroControllerScope(
        controller: _heroController,
        child: result,
      )
    );
  }
}


/// 以下的代碼片段截取自_HeroFlight,可以點(diǎn)擊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,
     ),
   ),
 ),
);

通過(guò)對(duì)源碼的查看,我們知道Hero動(dòng)畫(huà)就是頁(yè)面開(kāi)始跳轉(zhuǎn)的時(shí)候,通過(guò)計(jì)算原Rect和結(jié)束時(shí)目標(biāo)的Rect之后,通過(guò)Positioned改變自身的位置,最后實(shí)現(xiàn)動(dòng)畫(huà)的展示


Rive 動(dòng)畫(huà)

這個(gè)更多的是設(shè)計(jì)通過(guò)工具導(dǎo)出各種絢麗的動(dòng)畫(huà)給到我們,然后我們?cè)诮o展示到屏幕中。

比如下面的這個(gè)動(dòng)畫(huà),如果自己去使用繪制效果去處理,那么絕對(duì)是一件頭疼的事情,但是引入Rive動(dòng)畫(huà)就可以很好的解決這個(gè)問(wèn)題,并且如果你仔細(xì)的觀察下面的動(dòng)畫(huà),你會(huì)發(fā)現(xiàn)動(dòng)畫(huà)是可以分別控制的(雨刮器和車(chē)子)。

當(dāng)然除了這個(gè)Lottie也有對(duì)應(yīng)的Flutter package支持。

rive.gif

這里把示例代碼展示一下

需要注意的是資源文件你需要自行去這里下載,然后倒入rivepackage,之后就可以實(shí)現(xiàn)上圖的動(dòng)畫(huà)效果了。

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 控制車(chē)輛
        ..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("自定義動(dòng)畫(huà)"),),
      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;
}


手勢(shì)與觸摸

當(dāng)我們對(duì)屏幕區(qū)域中的任意一點(diǎn)進(jìn)行點(diǎn)擊的時(shí)候,這個(gè)事件是如何準(zhǔn)確找到是誰(shuí)處理或者拋棄該事件的呢?由于Flutter的跨平臺(tái)特性,所以一個(gè)事件的傳遞需要經(jīng)過(guò)原生層、中間層、Dart層。對(duì)于原生層和中間層會(huì)有各自的處理邏輯,從Dart層開(kāi)始就進(jìn)入了Flutter的領(lǐng)域了。

手勢(shì)和觸摸


事件流程

通過(guò)上圖可以看到,在Dart層中事件都是從_dispatchPointerDataPacket開(kāi)始,之后會(huì)通過(guò)Zone判斷環(huán)境進(jìn)行回調(diào),然后返回到GestureBinding類(lèi)中的_handlePointEvent方法,下面就是截取的_handlePointerEvent方法的內(nèi)部代碼

 void _handlePointerEvent(PointerEvent event) {
    assert(!locked);
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      /// 檢測(cè)并添加合法控件成員列表
      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);
      /// 滿足上面條件之后,開(kāi)始分發(fā)事件
      dispatchEvent(event, hitTestResult);
    }
  }


hitTest

hitTest方法主要是為了獲取到一個(gè)HitTestResult,這個(gè)hitTestResult內(nèi)用一個(gè)List<HisTestEntry>用于保存競(jìng)技對(duì)象,而每個(gè)HitTestEntry.target都會(huì)存儲(chǔ)每個(gè)空間對(duì)應(yīng)的RenderObject對(duì)象(因?yàn)?code>RenderObject實(shí)現(xiàn)了HitTestTarget接口)


dispatchEvent

dispatchEvent主要是對(duì)事件進(jìn)行分發(fā)。通過(guò)上面的hitTest方法之后,我們得到了所有的競(jìng)技對(duì)象列表,然后就挨個(gè)的進(jìn)行遍歷HitTestEntry對(duì)象,調(diào)用他們的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對(duì)象列表
    for (final HitTestEntry entry in hitTestResult.path) {
      try {
        /// 分別調(diào)用renderObject的handleEvent方法
        entry.target.handleEvent(event.transformed(entry.transform), entry);
      } catch (exception, stack) {
      }
    }
  }

這里可能會(huì)出現(xiàn)一種情況:如果一個(gè)區(qū)域的內(nèi)的多個(gè)控件都實(shí)現(xiàn)了HandleEvent方法,此時(shí)最后的處理權(quán)交給那個(gè)控件呢?這就設(shè)計(jì)了事件競(jìng)爭(zhēng)。


事件競(jìng)爭(zhēng)

Flutter設(shè)計(jì)事件競(jìng)爭(zhēng)的時(shí)候,定義了一個(gè)概念:通過(guò)一個(gè)競(jìng)技場(chǎng),各個(gè)控件參與競(jìng)爭(zhēng),但只有符合下面兩個(gè)條件中的任意一個(gè)的時(shí)候,才可以獲得事件的處理權(quán)。

  • 最后得到直接勝利的控件(理解為這個(gè)區(qū)域就只有他)
  • 活到最后的空間中排在第一為的控件(這個(gè)區(qū)域有多個(gè),但是列表中排第一個(gè)的才享有處理權(quán))

也就是說(shuō)當(dāng)Down事件時(shí)通過(guò)addPointer加入GestureRecognizer競(jìng)技場(chǎng),如果區(qū)域內(nèi)只有他一個(gè)響應(yīng)者,那么會(huì)在Up的時(shí)候進(jìn)行_checkUp,直接就完成競(jìng)爭(zhēng)流程,如果這個(gè)區(qū)域內(nèi)有多個(gè)響應(yīng),那么Down的時(shí)候就決定不了誰(shuí)是最終響應(yīng)者,則會(huì)在Up的時(shí)候直接把列表中的第一個(gè)作為響應(yīng)者。


滑動(dòng)事件

滑動(dòng)事件的處理,也要求控件在Down流程中通過(guò)addPointer加入競(jìng)技場(chǎng),然后在Move流程中,通過(guò)PointerRouter.route執(zhí)行DragGestureRecoginzer.handleEvent處理事件。

image-20210322151936319.png

PointerMoveEvent事件里,DragGestureRecognizer.handleEvent會(huì)通過(guò)_hasSufficientPendingDragDeltaToAccept方法判斷是否符合條件,如果符合條件直接執(zhí)行resolve(GestureDisposition.accepted),之后流程回到競(jìng)技場(chǎng)執(zhí)行acceptGesture,觸發(fā)onStartonUpdate去通知上層處理事件。

onUpdate事件回去更新Offset,然后通過(guò)markNeedsLayout是的ViewPort重新布局,讓界面看起來(lái)像滾動(dòng)起來(lái)。


滑動(dòng)Physic

我們都知道滑動(dòng)結(jié)束后不會(huì)馬上停止,而是有一個(gè)物理的減弱動(dòng)畫(huà)的實(shí)現(xiàn),那么他是怎么實(shí)現(xiàn)的呢?

官方給我們定義了四種ScrollPhysics

  • BouncingScrollPhysics 允許滾動(dòng)超出邊界,松手之后內(nèi)容會(huì)進(jìn)行回彈效果
  • ClampingScrollPhysics 防止超出邊界,夾住收尾區(qū)域
  • AlwaysScrollableScrollPhysics 始終響應(yīng)用戶的滾動(dòng),松手之后內(nèi)容會(huì)進(jìn)行回彈效果
  • NeverScrollableScrollPhysics 不響應(yīng)用戶的滾動(dòng)

部分情況下我們不會(huì)特意去設(shè)置physics,細(xì)心的你可能會(huì)發(fā)現(xiàn)在iOSAndroid上面ListView、CustomScrollView等控件,拖拽效果會(huì)有不同,為啥子呢?這是因?yàn)樵?code>ScrollConfiguration內(nèi)部進(jìn)行特別的處理。


ScrollConfiguration

實(shí)現(xiàn)滑動(dòng)溢出的效果關(guān)鍵的兩個(gè)雷就是ScrollConfigurationScrollBehavior。在Scrollable_updatePosition源碼中,里面有一個(gè)關(guān)鍵的判斷,如果widget.physics==null的時(shí)候,就會(huì)使用ScrollConfiguration.of(context)getScrollPhysics(context)方法獲取_physicsScrollConfiguration.of(context)返回是一個(gè)InheritedWidget對(duì)象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);
  }

所以在我們不填寫(xiě)physics屬性的情況下,我們獲取的physics就要看getScrollPhysics(context)這個(gè)方法的內(nèi)部實(shí)現(xiàn)

    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;
  }

上面的源碼就可以很好的展示了默認(rèn)情況下的為什么列表在iOSAndroid平臺(tái)中會(huì)有不同滑動(dòng)效果的原因。


然后我們?nèi)ゲ榭?code>ScrollConfiguration.of方法

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

發(fā)現(xiàn)這個(gè)類(lèi)是InheritedWidget,我們知道InheritedWidget的特性(數(shù)據(jù)向下傳遞),那么他是在哪里被初始化的呢?最終在MaterialApp的實(shí)現(xiàn)中找到了,代碼截圖如下:

    /// 此為代碼片段,刪除了部分無(wú)關(guān)代碼
    @override
  Widget build(BuildContext context) {
    Widget result = _buildWidgetApp(context);
    
    /// 發(fā)現(xiàn)這里是ScrollConfiguration包裝起來(lái)的對(duì)象
    return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      child: HeroControllerScope(
        controller: _heroController,
        child: result,
      )
    );
  }


ScrollPhysics

通過(guò)查看ScrollConfiguration的相關(guān)實(shí)現(xiàn),我們知道了為什么不同的平臺(tái)會(huì)有不同的拖曳處理,但是怎么去實(shí)現(xiàn)這個(gè)效果則主要是通過(guò)ScrollPhysics的子類(lèi)去實(shí)現(xiàn),ScrollPhysics本身只是定義了一些屬性和方法,其中幾個(gè)比較重要的方法如下:

class ScrollPhysics {
  // 將用戶拖曳距離Offset轉(zhuǎn)化為需要移動(dòng)的pixels
  // 如果沒(méi)有父類(lèi)就直接返回offset,否則就調(diào)用父類(lèi)的applyPhysicsToUserOffset方法
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    if (parent == null)
      return offset;
    return parent.applyPhysicsToUserOffset(position, offset);
  }
  
  // 返回邊界條件,如果是0,overscroll則一直就是0
  // 如果沒(méi)有父類(lèi),直接返回0,否則就調(diào)用父類(lèi)的同名方法
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    if (parent == null)
      return 0.0;
    return parent.applyBoundaryConditions(position, value);
  }
  
  /// 創(chuàng)建一個(gè)滾動(dòng)的模擬器,這個(gè)處理器就是處理阻尼、滑動(dòng)、回彈效果的具體實(shí)現(xiàn)
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    if (parent == null)
      return null;
    return parent.createBallisticSimulation(position, velocity);
  }
  
  /// 最小的滑動(dòng)速率 
  /// const double kMinFlingVelocity = 50.0 默認(rèn)是50
  double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
  
  
  /// 傳輸動(dòng)量,返回重復(fù)滾動(dòng)的速度
  /// 如果父類(lèi)為空就直接返回0 否則調(diào)用父類(lèi)同名方法
  double carriedMomentum(double existingVelocity) {
    if (parent == null)
      return 0.0;
    return parent.carriedMomentum(existingVelocity);
  }
  
    /// 最小的開(kāi)始拖曳距離
  /// 如果返回空,則不執(zhí)行最低閾值。
  double get dragStartDistanceMotionThreshold => parent?.dragStartDistanceMotionThreshold;
  
  /// 滾動(dòng)模擬的公差
  /// 指定距離、持續(xù)時(shí)間和速度差應(yīng)視為平等的差異的結(jié)構(gòu)
  Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
  
  /// 這個(gè)就是默認(rèn)實(shí)現(xiàn)
  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的具體實(shí)現(xiàn)就依賴Scrollable的觸摸響應(yīng)流程中,主要的邏輯關(guān)鍵地方就是下面的是三個(gè)方法:

  • applyPhysicsToUserOffset: 通過(guò)physics將用戶拖曳距離offset轉(zhuǎn)化為setPixels(滾動(dòng))的增量
  • applyBoundaryConditions:通過(guò)physics計(jì)算當(dāng)前滾動(dòng)的邊界條件
  • createBallisticSimulation:創(chuàng)建自動(dòng)滑動(dòng)的模擬器


這三個(gè)觸發(fā)的時(shí)機(jī)分別在于:_handleDragUpdate、_handleDragCancel、_handleDragEnd也就是拖曳的過(guò)程和結(jié)束的時(shí)機(jī)

  • applyPhysicsToUserOffsetapplyBoundaryConditions是在_handleDragUpdate時(shí)觸發(fā)計(jì)算
  • createBallisticSimulation是在_handleDragCancel_handleDragEnd時(shí)觸發(fā)
image-20210323112605356.png


圖片加載

Flutter加載圖片一般有兩種方式:ImageDecorationImage,其中DecorationImage用于DecoratedBox中。

兩者不同之處是Image是一個(gè)StatefulWidget,其內(nèi)部通過(guò)RawImage實(shí)現(xiàn)圖片的繪制;DecorationImage并不是控件,只是單純圖片繪制,內(nèi)部通過(guò)DecorationImagePainter直接繪制圖片。正因如此,DecorationImage對(duì)比Image少了一些可定制化的配置。

兩者相同的地方則是都需要ImageProvider實(shí)現(xiàn)圖片的加載和數(shù)據(jù)轉(zhuǎn)換。只是Image控件提供了Image.asset方法對(duì)AssetImage的封裝,其實(shí)他們最終都會(huì)通過(guò)decoration_image.dart下的paintImage方法實(shí)現(xiàn)圖片的繪制邏輯。


下表是Image的參數(shù)列表

參數(shù) 描述
width/height 圖片區(qū)域的寬高,需要注意不是圖片的寬高
fit 填充模式
BoxFit.fill 鋪滿設(shè)置的區(qū)域,可能會(huì)拉伸
BoxFit.fitHeight 填充高度,可能會(huì)裁剪、拉伸
BoxFit.fitWidth 填充寬度、可能會(huì)裁剪、拉伸
BoxFit.contain 居中顯示,顯示原比例,可能不會(huì)填滿
BoxFit.cover 原比例進(jìn)行裁剪并填滿容器
BoxFit.scaleDown 和contain類(lèi)似,但是只會(huì)縮小不會(huì)放大
color 前景色,會(huì)覆蓋圖片顏色,多數(shù)情況和colorBlendMode配合使用
colorBlendMode 與color參數(shù)結(jié)合使用,設(shè)置color的回合模式
alignment 對(duì)齊方式
repeat 是否重復(fù)(當(dāng)圖片填充不滿設(shè)置的width/height)
centerSlice 設(shè)置圖片的拉伸區(qū)域,需要注意只有在大于width/height的情況下才可以使用這個(gè)屬性
matchTextDirection 需要配合Directionality進(jìn)行使用,一般配合文本對(duì)齊
gaplessPlayback 圖片更新之后,是否將原圖片進(jìn)行保留之后在顯示
filterQuality 圖片顯示的過(guò)濾質(zhì)量
loadingBuilder 圖片加載中的顯示 返回Widget
frameBuilder 對(duì)需要顯示的圖片進(jìn)行定制處理


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中對(duì)圖片是進(jìn)行了緩存的(但是沒(méi)有做本地緩存),緩存的方法實(shí)現(xiàn)在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;
    }
    
      // 已經(jīng)緩存過(guò)了,直接返回
    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;
  } 

由于圖片的緩存都是異步的方式,他并不知道圖片會(huì)消耗多少內(nèi)存(在未解碼之前),所以當(dāng)一個(gè)頁(yè)面加載了大量圖片,此時(shí)就有可能會(huì)造成內(nèi)存不夠的情況,在iOS上面的表現(xiàn)就是會(huì)被系統(tǒng)給殺掉。

所以如果對(duì)內(nèi)存緩存大小和數(shù)量有要求的話,可以通過(guò)PaintingBinding.instacne.imageCache.maximunSize進(jìn)行設(shè)置(默認(rèn)是100m),同時(shí)最好在頁(yè)面不可見(jiàn)的時(shí)候暫停圖片的I/O和下載。


網(wǎng)絡(luò)請(qǐng)求

Flutter中的網(wǎng)絡(luò)請(qǐng)求是不經(jīng)過(guò)原生端的,所以我們使用的是Dart層的網(wǎng)絡(luò)請(qǐng)求服務(wù)。

HttpClient

/// 創(chuàng)建Client
HttpClient httpClient = HttpClient();
/// 地址
Uri uri = Uri(scheme: "https", host: "example.com");
/// 得到request
HttpClientRequest request = await httpClient.getUrl(uri);
/// 添加請(qǐng)求頭
request.header.add("token": "xxxxx");
/// 等待請(qǐng)求結(jié)果
HttpClientResponse response = await request.close();
/// 解析數(shù)據(jù)
String responseBody = await response.transform(utf8.decoder).join();
/// 關(guān)閉client
httpClient.close();

一般情況下,我們很少使用到HttpClient來(lái)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,在Flutter大部分還是使用第三方封裝好的SDK,比如Dio


Dio

Flutter中出名的網(wǎng)絡(luò)請(qǐng)求框架,除了基本的網(wǎng)絡(luò)請(qǐng)求之外還設(shè)置了攔截器、代理等相當(dāng)使用的功能。

具體的實(shí)現(xiàn)可以去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);
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容