Flutter動(dòng)畫原理

  • 概述

    作為前端開發(fā)技術(shù)弱匪,動(dòng)畫是一門前端語言所必須的缩赛,在Flutter中的動(dòng)畫是如何使用的呢堡纬?它的設(shè)計(jì)原理又是什么呢?本文就從源碼的角度來分析一下Flutter動(dòng)畫饥悴。

  • 從使用開始

    當(dāng)然坦喘,我們還是從使用API的入口開始盲再,看一下動(dòng)畫機(jī)制是怎么驅(qū)動(dòng)的。下面是一個(gè)demo:

    //需要繼承TickerProvider瓣铣,如果有多個(gè)AnimationController答朋,則應(yīng)該使用TickerProviderStateMixin。
    class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      initState() {
        super.initState();
        //step1.
        controller = AnimationController(
          duration: const Duration(seconds: 2),
          vsync: this,
        );
    
        //勻速
        //圖片寬高從0變到300
        //step2.
        final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        //Tween可以持有AnimationController也可以持有CurvedAnimation坯沪,他們都是Animation類型
        //animation = Tween(begin: 0.0, end: 300.0).animate(controller)
        animation = Tween(begin: 0.0, end: 300.0).animate(curve)
          ..addListener(() {
            setState(() => {});
          });
    
        //啟動(dòng)動(dòng)畫(正向執(zhí)行)
        //step3.
        controller.forward();
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Image.asset(
            "imgs/avatar.png",
            //Image的寬高被動(dòng)畫的更新值驅(qū)動(dòng)
            width: animation.value,
            height: animation.value,
          ),
        );
      }
    
      dispose() {
        //路由銷毀時(shí)需要釋放動(dòng)畫資源
        controller.dispose();
        super.dispose();
      }
    }
    

    首先我們需要一個(gè)AnimationController绿映,不管是什么類型的動(dòng)畫一定得有這個(gè),它的作用就是控制動(dòng)畫的開始停止等(原理是被vsync屏幕刷新信號(hào)驅(qū)動(dòng)來控制動(dòng)畫值的更新腐晾,后面會(huì)分析到)叉弦。

    這里給出了兩個(gè)必須的參數(shù),一個(gè)是duration藻糖,表示動(dòng)畫時(shí)長淹冰,這個(gè)值如果不傳會(huì)在執(zhí)行的時(shí)候報(bào)錯(cuò),另一個(gè)參數(shù)vsync是TickerProvider巨柒,它被required修飾樱拴,所以必傳,這里傳入的是this洋满,這個(gè)this指向的并不是State而是SingleTickerProviderStateMixin晶乔,SingleTickerProviderStateMixin繼承自TickerProvider。

    step2部分的Tween其實(shí)你可以選擇去掉牺勾,但是需要在AnimationController構(gòu)造時(shí)指定lowerBound和upperBound來確定范圍正罢,同時(shí),你要用AnimationController調(diào)用addListener方法驻民,并在回調(diào)中調(diào)用setState方法翻具,否則動(dòng)畫不會(huì)引起圖片大小的變化。

    Curve是用來描述速度變化快慢的回还,他最終想要驅(qū)動(dòng)動(dòng)畫值變化還是要調(diào)用AnimationController裆泳,它只不過是AnimationController的進(jìn)一步封裝,相當(dāng)于包裝模式柠硕;同理工禾,在這個(gè)demo 中,Tween也是完成了對(duì)于Curve的封裝調(diào)用仅叫。

    最后執(zhí)行forward方法來開啟動(dòng)畫帜篇。

    總之,在上面的這個(gè)demo中诫咱,AnimationController和其必須的構(gòu)造參數(shù)對(duì)象是必須要有的笙隙,中間的Tween就是指定圖片大小的上下限取值,可以用AnimationController全權(quán)受理坎缭。

    下面我們針對(duì)上面用到的這些類來逐個(gè)研究竟痰。

  • AnimationController

    上面我們說到签钩,AnimationController是用來管理動(dòng)畫的啟動(dòng)停止等動(dòng)作的,并且是通過vsync信號(hào)驅(qū)動(dòng)的坏快,我們從源碼角度來驗(yàn)證它铅檩。

    它的構(gòu)造方法中有這樣一段代碼:

    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
    

    _internalSetValue方法邏輯比較簡單,我們先看 _internalSetValue方法:

    void _internalSetValue(double newValue) {
      _value = newValue.clamp(lowerBound, upperBound);
      if (_value == lowerBound) {
        _status = AnimationStatus.dismissed;
      } else if (_value == upperBound) {
        _status = AnimationStatus.completed;
      } else {
        _status = (_direction == _AnimationDirection.forward) ?
          AnimationStatus.forward :
          AnimationStatus.reverse;
      }
    }
    

    _value 是動(dòng)畫即時(shí)值莽鸿,未指定時(shí)是null昧旨,如果它小于lowerBound,clamp方法會(huì)把它置為lowerBound祥得,同樣兔沃,大于upperBound會(huì)把它置為upperBound,這里會(huì)根據(jù)是它是等于lowerBound還是等于upperBound而給 _status賦值為AnimationStatus.dismissed或AnimationStatus.completed级及,前者表示在起點(diǎn)乒疏,后者表示在終點(diǎn);如果是在規(guī)定區(qū)間內(nèi)的值則會(huì)根據(jù) _direction是否為 _AnimationDirection.forward來決定 _status是AnimationStatus.forward還是AnimationStatus.reverse饮焦。

    總之怕吴,_internalSetValue的作用就是初始化 _value和 _status。

    回過頭來县踢,我們看到_ticker在這里是調(diào)用的vsync的createTicker方法創(chuàng)建的转绷,假如我們State依賴的mixin是SingleTickerProviderStateMixin,我們?nèi)タ纯此倪@個(gè)方法:

    @override
    Ticker createTicker(TickerCallback onTick) {
      _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null);
      return _ticker!;
    }
    

    我只接截取了關(guān)鍵代碼硼啤,assert等容錯(cuò)邏輯沒截取暇咆。

    可以看到,在這里創(chuàng)建了一個(gè)Ticker對(duì)象丙曙,他持有了一個(gè)函數(shù)對(duì)象onTick,這個(gè)onTick就是AnimationController構(gòu)造方法里傳入的 _tick函數(shù):

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

    可以看到這里會(huì)通過 _simulation產(chǎn)生新的 _value值其骄,所以猜測亏镰,這里可能是屏幕刷新后會(huì)回調(diào)的地方,但是怎么驗(yàn)證呢拯爽?我們?nèi)フ艺宜窃趺春推聊凰⑿聶C(jī)制關(guān)聯(lián)上的索抓。

    _simulation是什么呢?我們發(fā)現(xiàn)他沒有初始化值毯炮,又在什么時(shí)候賦值的呢逼肯?因?yàn)槠聊凰⑿乱恢痹谶M(jìn)行,動(dòng)畫的回調(diào)想要被它影響一定會(huì)在某個(gè)時(shí)間點(diǎn)建立關(guān)聯(lián)桃煎,那在什么時(shí)候呢篮幢?開啟動(dòng)畫的時(shí)候就是最恰當(dāng)?shù)臅r(shí)候,所以我們直接從動(dòng)畫開啟方法之一的forward方法找起:

    TickerFuture forward({ double? from }) {
      _direction = _AnimationDirection.forward;
      if (from != null)
        value = from;
      return _animateToInternal(upperBound);
    }
    

    可見它內(nèi)部調(diào)用了_animateToInternal方法为迈,這個(gè)方法就是開啟動(dòng)畫的方法三椿,很明顯它是一個(gè)受保護(hù)的內(nèi)部方法缺菌,調(diào)用它的有四處,也是供外部調(diào)用的四個(gè)開啟方法:forward搜锰、reverse伴郁、animateTo、animateBack蛋叼。

    _animateToInternal方法內(nèi)部主要是做一些對(duì)于當(dāng)前動(dòng)畫的一些停止和對(duì)即將開始的新動(dòng)畫的初始化工作焊傅,關(guān)鍵是它最后調(diào)用了 _startSimulation方法:

    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
    
    TickerFuture _startSimulation(Simulation simulation) {
      _simulation = simulation;
      _lastElapsedDuration = Duration.zero;
      _value = simulation.x(0.0).clamp(lowerBound, upperBound);
      final TickerFuture result = _ticker!.start();
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.forward :
        AnimationStatus.reverse;
      _checkStatusChanged();
      return result;
    }
    

    可以發(fā)現(xiàn),_startSimulation的代碼和 _tick中的代碼好相似啊狈涮,仔細(xì)看會(huì)發(fā)現(xiàn)狐胎, _startSimulation方法中simulation的x方法的傳參固定是0.0,因?yàn)檫@是動(dòng)畫開啟薯嗤;又發(fā)現(xiàn)這里沒有調(diào)用stop方法顽爹,因?yàn)閯?dòng)畫剛開啟不需要結(jié)束,結(jié)束的判斷就是要交給上面的 _tick方法骆姐。

    重要的是我們?cè)谶@里找到了 _ticker的開啟方法:

    TickerFuture start() {
      _future = TickerFuture._();
      if (shouldScheduleTick) {
        scheduleTick();
      }
      if (SchedulerBinding.instance!.schedulerPhase.index > SchedulerPhase.idle.index &&
          SchedulerBinding.instance!.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
        _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
      return _future!;
    }
    

    在這里會(huì)創(chuàng)建TickerFuture镜粤,isActive方法中會(huì)根據(jù) _future來判斷動(dòng)畫是否正在運(yùn)行。然后調(diào)用scheduleTick方法:

    @protected
    void scheduleTick({ bool rescheduling = false }) {
      assert(!scheduled);
      assert(shouldScheduleTick);
      _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling);
    }
    

    看到這里我們需要先明白一個(gè)原理玻褪,就是<font color=red>Flutter 應(yīng)用在啟動(dòng)時(shí)都會(huì)綁定一個(gè)SchedulerBinding肉渴,通過SchedulerBinding可以給每一次屏幕刷新添加回調(diào),而Ticker就是通過SchedulerBinding來添加屏幕刷新回調(diào)的带射,這樣一來同规,每次屏幕刷新都會(huì)調(diào)用TickerCallback。使用Ticker(而不是Timer)來驅(qū)動(dòng)動(dòng)畫會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí)窟社,如鎖屏?xí)r)消耗不必要的資源券勺,因?yàn)镕lutter中屏幕刷新時(shí)會(huì)通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動(dòng)的灿里,由于鎖屏后屏幕會(huì)停止刷新关炼,所以Ticker就不會(huì)再觸發(fā)。</font>

    所以這里調(diào)用了SchedulerBinding.instance的scheduleFrameCallback方法來和 _tick回調(diào)函數(shù)建立關(guān)聯(lián):

    int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
      scheduleFrame();
      _nextFrameCallbackId += 1;
      _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
      return _nextFrameCallbackId;
    }
    

    我們?cè)趆andleBeginFrame方法中找到了使用 _transientCallbacks的地方:

    void handleBeginFrame(Duration? rawTimeStamp) {
          ...
        callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
          if (!_removedIds.contains(id))
            _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
        });
          ...
    }
    

    這個(gè)方法會(huì)確保注冊(cè)在window上:

    @protected
    void ensureFrameCallbacksRegistered() {
      window.onBeginFrame ??= _handleBeginFrame;
      window.onDrawFrame ??= _handleDrawFrame;
    }
    

    到這里匣吊,我們就找出了動(dòng)畫回調(diào)函數(shù)和屏幕刷新回調(diào)之間的關(guān)聯(lián)邏輯儒拂。

    而在Ticker的stop方法中我們會(huì)找到回調(diào)移除的邏輯:

    void stop({ bool canceled = false }) {
      if (!isActive)
        return;
    
      // We take the _future into a local variable so that isTicking is false
      // when we actually complete the future (isTicking uses _future to
      // determine its state).
      final TickerFuture localFuture = _future!;
      _future = null;
      _startTime = null;
      assert(!isActive);
      //移除動(dòng)畫回調(diào)
      unscheduleTick();
      if (canceled) {
        localFuture._cancel(this);
      } else {
        localFuture._complete();
      }
    }
    
  • Simulation

    在上面的分析中有一個(gè)Simulation類,它有三個(gè)方法:

    /// The position of the object in the simulation at the given time.
    double x(double time);
    
    /// The velocity of the object in the simulation at the given time.
    double dx(double time);
    
    /// Whether the simulation is "done" at the given time.
    bool isDone(double time);
    

    根據(jù)注釋可知色鸳,可以通過x方法社痛、dx方法、isDone方法分別可以得出當(dāng)前動(dòng)畫的進(jìn)度命雀、速度和是否已完成的標(biāo)志蒜哀。我們上面?zhèn)魅氲氖撬膶?shí)現(xiàn)類 _InterpolationSimulation,它內(nèi)部持有的屬性有開始值咏雌、終點(diǎn)值和執(zhí)行時(shí)長凡怎,執(zhí)行時(shí)長保存在 _durationInSeconds屬性:

    _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
    

    可以看到校焦,這里會(huì)將設(shè)置的市場按照scale進(jìn)行壓縮,并最終取微秒單位统倒,因?yàn)閯?dòng)畫要求肉眼看不出卡頓寨典,所以這里使用最細(xì)致的微秒單位。

    我們先看看它的x方法:

    @override
    double x(double timeInSeconds) {
      final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
      if (t == 0.0)
        return _begin;
      else if (t == 1.0)
        return _end;
      else
        return _begin + (_end - _begin) * _curve.transform(t);
    }
    

    可見房匆,這里會(huì)在初始值的基礎(chǔ)上加上需要前進(jìn)的值耸成,正常的t是自上次屏幕刷新后消逝的時(shí)間,這里調(diào)用 _curve的transform方法也正是Curve的原理所在浴鸿,它對(duì)進(jìn)度作了進(jìn)一步的處理井氢。

    @override
    double dx(double timeInSeconds) {
      final double epsilon = tolerance.time;
      return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
    }
    

    同樣,dx方法中是關(guān)于速度的算法岳链。

    isDone最簡單花竞,只是通過是否超出_durationInSeconds來判斷動(dòng)畫是否結(jié)束:

    @override
    bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
    

    <font color=red>記住這個(gè)Simulation所做的事情,在下面對(duì)于Curve和Tween的原理分析中掸哑,你會(huì)看到似曾相識(shí)的邏輯</font>约急。

  • Curve

    在上面對(duì)于Simulation的分析中我們提到,在x方法獲取最新進(jìn)度值的時(shí)候會(huì)通過Curve的transform方法處理一下苗分,而這一點(diǎn)也正是在動(dòng)畫機(jī)制中Curve發(fā)揮作用的關(guān)鍵厌蔽。

    transform方法最終在其父類ParametricCurve中會(huì)調(diào)用transformInternal方法,這個(gè)方法理應(yīng)由子類實(shí)現(xiàn)摔癣,Curve的子類有很多奴饮,代表著很多中變化曲線算法,這里就不展開講了择浊,Curve的原理在上文中其實(shí)已經(jīng)講完了戴卜。

  • Tween

    Tween是用來設(shè)置屬性范圍的,可能有人會(huì)講琢岩,屬性范圍我完全可以通過AnimationController的lowerBound和upperBound來指定叉瘩,為什么還需要這個(gè)多余的類呢?

    把屬性的類型不設(shè)限于double粘捎,你就能體會(huì)到他應(yīng)該有的作用了。

    class Tween<T extends Object?> extends Animatable<T> {
      Tween({
        this.begin,
        this.end,
      });
      ...
    }
    

    可見危彩,Tween持有了一個(gè)泛型攒磨,表示可以設(shè)置任何屬性,通過Tween汤徽,我們動(dòng)畫最終產(chǎn)生的值就不再只局限于double了娩缰,我可以轉(zhuǎn)成任何直接使用的屬性值。

    上文我們知道谒府,AnimationController會(huì)在vsync的驅(qū)動(dòng)下執(zhí)行動(dòng)畫回調(diào)獲取動(dòng)畫最新值拼坎,那么在使用那部分我們看到浮毯,我們引用的直接是Tween的animate方法返回的Animation對(duì)象的value值,那么這個(gè)value值和AnimationController即時(shí)獲取的最新值是怎么關(guān)聯(lián)的呢泰鸡?

    我們先看一下Tween的animate方法返回的是什么:

    Animation<T> animate(Animation<double> parent) {
      return _AnimatedEvaluation<T>(parent, this);
    }
    

    可以看到债蓝,是一個(gè)_AnimatedEvaluation實(shí)例,它的parent指定為AnimationController盛龄,我們來看一下它的value:

    @override
    T get value => _evaluatable.evaluate(parent);
    

    value通過 _evaluatable的evaluate方法獲取饰迹, _evaluatable就是前面?zhèn)魅氲膖his,也就是Tween本身:

    T evaluate(Animation<double> animation) => transform(animation.value);
    

    可見evaluate又調(diào)用了transform方法:

    @override
    T transform(double t) {
      if (t == 0.0)
        return begin as T;
      if (t == 1.0)
        return end as T;
      return lerp(t);
    }
    

    transform方法中如果是中間值的話又會(huì)調(diào)用lerp方法:

    @protected
    T lerp(double t) {
      ...
      return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
    }
    

    怎么樣余舶,熟悉吧啊鸭,是不是和_InterpolationSimulation中的x方法如出一轍,這就是默認(rèn)的進(jìn)度處理邏輯匿值,不同的是赠制,這里的begin、end和返回值都是泛型指定的挟憔,而不是固定的double钟些,t為double是因?yàn)閠是刷新的微秒進(jìn)度值,一定是double曲楚。

    你可以定義自己的Tween類厘唾,繼承并重寫lerp方法即可完成自定義的屬性值轉(zhuǎn)換,比如ColorTween:

    @override
    Color? lerp(double t) => Color.lerp(begin, end, t);
    

    Color的lerp方法如下:

    static Color? lerp(Color? a, Color? b, double t) {
      assert(t != null);
      if (b == null) {
        if (a == null) {
          return null;
        } else {
          return _scaleAlpha(a, 1.0 - t);
        }
      } else {
        if (a == null) {
          return _scaleAlpha(b, t);
        } else {
          return Color.fromARGB(
            _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
            _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
          );
        }
      }
    }
    

    這就是Tween的原理龙誊,可以知道抚垃,Tween只不過是內(nèi)部持有了AnimationController,然后通過AnimationController拿到進(jìn)度值然后做自己的處理之后返回給value使用趟大。

    Tween的parent換做持有CurvedAnimation也是一樣的原理:

    @override
    double get value {
      final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
      final double t = parent.value;
      if (activeCurve == null)
        return t;
      if (t == 0.0 || t == 1.0) {
        return t;
      }
      return activeCurve.transform(t);
    }
    

    可見鹤树,只不過CurvedAnimation持有的是Curve,然后通過Curve去調(diào)用AnimationController而已逊朽。

  • TickerProviderStateMixin

    前面講了SingleTickerProviderStateMixin罕伯,其實(shí)TickerProvider還有一個(gè)子類就是TickerProviderStateMixin:

    @override
    Ticker createTicker(TickerCallback onTick) {
      _tickers ??= <_WidgetTicker>{};
      final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: 'created by $this');
      _tickers!.add(result);
      return result;
    }
    
    void _removeTicker(_WidgetTicker ticker) {
      assert(_tickers != null);
      assert(_tickers!.contains(ticker));
      _tickers!.remove(ticker);
    }
    

    可見,這個(gè)mixin是用來出來State中有多個(gè)AnimationController的情況叽讳,主要是用于Ticker的釋放清理工作追他。

  • 總結(jié)

    Flutter的動(dòng)畫原理的核心主要在于AnimationController和Scheduler關(guān)聯(lián),通過給Scheduler添加回調(diào)函數(shù)的方式和系統(tǒng)的vsync信號(hào)建立關(guān)聯(lián)岛蚤,從而由屏幕的刷新信號(hào)驅(qū)動(dòng)到動(dòng)畫回調(diào)函數(shù)邑狸,在回調(diào)函數(shù)中調(diào)用setState方法更新界面,而界面控件中又引用了動(dòng)畫類的value涤妒,value是通過直接或者間接的方式從AnimationController中獲取的最新當(dāng)前進(jìn)度值单雾,這就完成了整個(gè)動(dòng)畫的流程。

    可以通過CurvedAnimation、Tween等包裝類(當(dāng)然也可以自定義)使用包裝的設(shè)計(jì)模式去包裝AnimationController硅堆,使得在使用獲取的進(jìn)度值之前可以對(duì)其有更靈活的處理屿储。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市渐逃,隨后出現(xiàn)的幾起案子够掠,更是在濱河造成了極大的恐慌,老刑警劉巖朴乖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祖屏,死亡現(xiàn)場離奇詭異,居然都是意外死亡买羞,警方通過查閱死者的電腦和手機(jī)袁勺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門户辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腌紧,“玉大人,你說我怎么就攤上這事敌完〕蕴簦” “怎么了钝荡?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舶衬。 經(jīng)常有香客問我埠通,道長,這世上最難降的妖魔是什么逛犹? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任端辱,我火速辦了婚禮,結(jié)果婚禮上虽画,老公的妹妹穿的比我還像新娘舞蔽。我一直安慰自己,他們只是感情好码撰,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布渗柿。 她就那樣靜靜地躺著,像睡著了一般脖岛。 火紅的嫁衣襯著肌膚如雪朵栖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天柴梆,我揣著相機(jī)與錄音混槐,去河邊找鬼。 笑死轩性,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播揣苏,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼悯嗓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卸察?” 一聲冷哼從身側(cè)響起脯厨,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坑质,沒想到半個(gè)月后合武,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涡扼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年稼跳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃沪。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汤善,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出票彪,到底是詐尸還是另有隱情红淡,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布降铸,位于F島的核電站在旱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏推掸。R本人自食惡果不足惜桶蝎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望终佛。 院中可真熱鬧俊嗽,春花似錦、人聲如沸铃彰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牙捉。三九已至竹揍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邪铲,已是汗流浹背芬位。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留带到,地道東北人昧碉。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親被饿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子四康,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Flutter的動(dòng)畫體系是怎么運(yùn)作的,各組件之間的關(guān)聯(lián)關(guān)系及原理什么狭握,隱式動(dòng)畫闪金、顯式動(dòng)畫怎么區(qū)分,本文將會(huì)進(jìn)行詳細(xì)...
    whqfor閱讀 2,024評(píng)論 0 6
  • 概述 動(dòng)畫API認(rèn)識(shí) 動(dòng)畫案例練習(xí) 其它動(dòng)畫補(bǔ)充 一论颅、動(dòng)畫API認(rèn)識(shí) 動(dòng)畫實(shí)際上是我們通過某些方式(某種對(duì)象哎垦,An...
    IIronMan閱讀 364評(píng)論 1 3
  • 對(duì)于一個(gè)前端的App來說,添加適當(dāng)?shù)膭?dòng)畫恃疯,可以給用戶更好的體驗(yàn)和視覺效果漏设。所以無論是原生的iOS或Android,...
    5e4c664cb3ba閱讀 1,527評(píng)論 0 7
  • 動(dòng)畫實(shí)現(xiàn)的方式 Flutter中澡谭,我們可以簡單的把調(diào)用this.setState()理解為渲染一幀愿题。那么只要我們不...
    shawn_yy閱讀 1,846評(píng)論 0 6
  • 在任何系統(tǒng)的UI框架中,動(dòng)畫實(shí)現(xiàn)的原理都是相同的:在一段時(shí)間內(nèi)蛙奖,快速地多次改變UI外觀潘酗;由于人眼會(huì)產(chǎn)生視覺暫留,所...
    zombie閱讀 153評(píng)論 0 0