Flutter——實(shí)現(xiàn)網(wǎng)易云音樂的漸進(jìn)式卡片切換

介紹

預(yù)覽圖

image

我們可以看到頁面下方切換的卡片效果

分析

首先動畫以 x軸分為兩部分 : 左側(cè)文字 和 右側(cè)圖片

右側(cè)圖片以 z軸 分為 : 上踊淳、下

仔細(xì)觀察假瞬,可以看到它的動畫流程大致如下:

上層顯示的是當(dāng)前圖片,下層顯示的時下一張
1迂尝、左側(cè)文字淡入淡出切換
2脱茉、右側(cè)圖片的上層,與左側(cè)文字同時淡出
3垄开、之后下層圖片上移到 上層圖片的位置
4琴许、移動完成后,淡入一張下層圖片溉躲,
5榜田、于此同時新的文字淡入

實(shí)現(xiàn)

首先我們定義一個類

class MusicCalendar extends WidgetState with SingleTickerProviderStateMixin{}
因?yàn)檫@個動畫比較復(fù)雜寸认,實(shí)際開發(fā)時用了provider,
代碼中可能會看到musicCalendarVM串慰, 它主要是用來持有和控制狀態(tài)及一些數(shù)據(jù)
我盡量把里面的代碼移出來

MusicCalendar

當(dāng)頁面初始完成后偏塞,我們會執(zhí)行init()這個方法:

 ///這個方法后面還會見到,它的執(zhí)行會使動畫開始邦鲫,即淡出-移動-淡入
 ///在這里講灸叼,是讓你對流程大概有了解,方便理解后面的代碼
  init(){
    if(streamSubscription.isPaused){
      streamSubscription.resume();
    }
  }

首先創(chuàng)建一些變量

//淡出/淡入動畫
  AnimationController fadeController;
  Animation fadeAnim;
  //圖片外層是 stack庆捺,所以下面兩個變量用于定位
  //我們根據(jù)動畫的進(jìn)度配合下面的兩個變量古今,就可以達(dá)到移動圖片的效果
  //具體可以看下面的實(shí)現(xiàn)
  double aboveRightMax;
  double aboveBottomMax;
  

我們再看一下布局,代碼較多,我把說明寫在注釋里

去掉一些不必要的代碼

//root layout

Container(
      child: Stack(
        children: <Widget>[
          ///date 這個不用管滔以,跟咱們做的沒關(guān)系
          Positioned(
            top: getWidthPx(10),
            child: Text('后天',style: TextStyle(fontSize: getSp(28),color: Colors.black,fontWeight: FontWeight.bold),),
          ),
          
          ///這里是左側(cè) 文字部分捉腥,它要做的動畫 就是淡入和淡出
          Positioned(
            top: getWidthPx(60),
            child: FadeTransition( //使用flutter 提供的fade組件
              opacity: musicCalendarVM.fadeAnim,//這里與我們的 animation 綁定
              child: Container(
                width: getWidthPx(430),
                child: Text('${creatives[musicCalendarVM.currentIndex].uiElement.mainTitle.title}',
                  style: TextStyle(color: Colors.grey,fontSize: getSp(32)),maxLines: 2,),
              ),
            ),
          ),
            ///這里是右側(cè)圖片區(qū)域
          Positioned(
            top: getWidthPx(30),
            right: 0,
            child: imageSwitcher(),
          ),

        ],
      ),
    );


我們來看一下 這個方法:imageSwitcher()

Widget imageSwitcher(){
    return Container(
    //這里我們限定一下右側(cè)圖片區(qū)域的整體大小 
    //注意,圖片要小于這個值
      width: getWidthPx(150),height: getWidthPx(150),
      child: Stack(
        children: <Widget>[
          ///below
          ///這是下面那張圖片你画,初始為右下角
          Positioned(
            right: 0,
            bottom: 0,
            //Opacity用于控制圖片的淡入淡出
            child: Opacity(
              opacity: musicCalendarVM.opacity,
              child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.currentIndex<=creatives.length-2
                  ?musicCalendarVM.currentIndex+1 : 0].uiElement.image.imageUrl
                  , getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
            ),
          ),
          ///fake
          ///這里我額外放置了一張假的圖片抵碟,下面細(xì)說
          Positioned(
            right: musicCalendarVM.right,
            bottom: musicCalendarVM.bottom,
            child: Visibility(
              visible:musicCalendarVM.showFake ,
              child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.fakeIndex].uiElement.image.imageUrl
                  , getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
            ),
          ),
          ///above
          ///上面那張圖片初始為左上角
          Positioned(
            left: 0,
            top: 0,
//            right: 0,
//            bottom: 0,
            ///這里用FadeTransition 控制淡入/淡出
            child: FadeTransition(
              opacity: musicCalendarVM.fadeAnim,//與 animation綁定,與之前的 文字動畫一樣
              child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.currentIndex].uiElement.image.imageUrl
                  , getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
            ),
          ),



        ],
      ),
    );
  }

首先我們可以看到坏匪,這個stack中有3個widget:

下方圖片    代號below
上方圖片    代號above
假圖片     代號fake

below & above widget

我們先來看一下 below 和 above,他們雖然都是做淡入和淡出效果拟逮,但是用的組件不一樣。

below 使用的是Opacity
above 使用的是FadeTransition 與文字一樣

這里之所以不同适滓,是因?yàn)閮烧邎?zhí)行的實(shí)際和動畫方向不同敦迄,所以未共用一個動畫。above和文字一樣凭迹,由animation控制罚屋,我們不用管它,來看一下below吧嗅绸。

它的 opacity屬性與musicCalendarVM.opacity綁定脾猛,而這個opacity屬性的刷新主要涉及兩個方法

回顧一下它要做的效果,淡入(實(shí)際移動由fake來做朽砰,我們后面講)
  void showBelow(){
  ///這里我們每20毫秒更新一下不透明度
    Timer timer = Timer.periodic(Duration(milliseconds: 20), (timer){
      if(opacity >= 1.0){
        ///當(dāng)不透明度>=1.0時尖滚,我們結(jié)束timer
        timer.cancel();
        //這個時候也就意味著,below淡入完成瞧柔,
        //我們可以將上層圖片也淡入(這個淡入的是 above)
        ///漸顯above和title
        fadeController.reverse().whenComplete((){
          ///當(dāng)above淡入后漆弄,隱藏fake
          showFake = false; notifyListeners();
          ///實(shí)際上在below淡入前,fake做了移動造锅,移動到左上角了(冒充 above)
          ///因此撼唾,above完成淡入后,我們要重置fake位置,顯示fake(冒充 below)
          /// 嘿嘿
          right = 0; bottom = 0;
          ///當(dāng)然 fake顯示的圖片也應(yīng)該是上層圖片的下一張
          fakeIndex = currentIndex <= creatives.length-2 ? currentIndex+1:0;
          showFake = true;
          ///真正的 below又變成全透明了
          opacity = 0;
          notifyListeners();
        });
        return;
      }
      //每20秒 不透明度+0.1
      opacity =(opacity+0.1).clamp(0.0, 1.0);
      notifyListeners();
    });

  }

我們來看一下哪里調(diào)用了showBelow()


如果你不太熟悉Provider或者bedrock框架的話哥蔚,這里簡單來講倒谷,
就是頁面初試完后蛛蒙,開始執(zhí)行clock監(jiān)聽并執(zhí)行相應(yīng)的動畫操作。

///開頭的那個方法
  init(){
    if(streamSubscription.isPaused){
      streamSubscription.resume();
    }
  }
    
    final Duration interval = Duration(seconds: 5);///每5秒切換一次卡片

  MusicCalendarVM(this.block3, this.creatives,){
    clock = Stream.periodic(interval,(index){

    });
    ///咱們只看這里的方法
    streamSubscription = clock.listen((i) async{
      if(destroy)return;
      if(fadeController.status == AnimationStatus.completed|| fadeController.status == AnimationStatus.dismissed){
        ///當(dāng)上層動畫淡出完成后
        ///title和 above 漸隱渤愁,同時fake上移
        fadeController.forward().whenComplete((){
            //這里的right和bottom與fake綁定牵祟,我們稍后介紹
            //實(shí)際上這里的right和bottom理論上已經(jīng)等于右邊的值
            //但實(shí)際上還是會出現(xiàn)偏差,這里進(jìn)行校準(zhǔn)
          right = aboveRightMax;
          bottom = aboveBottomMax;
          notifyListeners();
          ///更新index =》 dataList的index
          incrementIndex();
          ///插入新的below
          ///調(diào)用了我們上的方法
          showBelow();
          //fadeController.reverse();
        });
      }


    });
    streamSubscription.pause();

  }

fake widget

至此 above和below就介紹完了抖格,我們來說一下 fake诺苹,

        ///fake
          Positioned(
            right: musicCalendarVM.right,
            bottom: musicCalendarVM.bottom,
            child: Visibility(
              visible:musicCalendarVM.showFake ,
              child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.fakeIndex].uiElement.image.imageUrl
                  , getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
            ),
          ),

fake的工作流程介紹:

當(dāng)頁面初始時:
    above圖片為第一張,fake和below為第二張
    above在右上角雹拄,below和fake在右下角收奔,且fake遮擋below(實(shí)際below為不可見)

當(dāng)動畫開始時:

    above淡出,于此同時fake滑動到左上角滓玖。
    
當(dāng)?shù)鰟赢嫿Y(jié)束后坪哄,我們將 above、below顯示的圖片index+1势篡, 這里below還是不可見的翩肌。
然后,我們將below淡入(它始終在右下角)殊霞,同時above也是淡入
(因?yàn)樗俣葮O快摧阅,且與fake重合,你是看不出它的淡入的)

操作完成后:
    我們將fake(在左上)隱藏绷蹲,并調(diào)整它的right和bottom為0,這樣又到了右下顾孽,
    隨后再顯示它祝钢,這樣又遮擋了below,
    至此一個輪回就結(jié)束了

我們大致看一下fake的主要屬性:

//控制它的位置
double right = 0;   double bottom = 0;

showFake//控制fake的隱藏若厚,在上面的方法中有出現(xiàn)過

更新這兩個屬性的位置在:

  void updatePosition(){
    right = aboveRightMax * (1-fadeAnim.value);
    bottom = aboveBottomMax * (1-fadeAnim.value);
    notifyListeners();
  }

updatePosition()方法則在 animationListener中調(diào)用

    
  musicCalendarVM.fadeController.addListener(musicCalendarVM.animationListener);
    
  animationListener(){
    if(fadeController.status == AnimationStatus.forward){
      if(!showFake) showFake = true;
      updatePosition();
    }

  }

到了這里拦英,整個動畫效果就實(shí)現(xiàn)了,如果有點(diǎn)亂测秸,可以在demo中對照源碼和真機(jī)效果來理解疤估。

謝謝大家的閱讀,歡迎指出不足支出 :)

Demo

內(nèi)部搜索即可

仿網(wǎng)易云音樂

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霎冯,一起剝皮案震驚了整個濱河市铃拇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沈撞,老刑警劉巖慷荔,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缠俺,居然都是意外死亡显晶,警方通過查閱死者的電腦和手機(jī)贷岸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來磷雇,“玉大人偿警,你說我怎么就攤上這事∥希” “怎么了螟蒸?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長睁本。 經(jīng)常有香客問我尿庐,道長,這世上最難降的妖魔是什么呢堰? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任抄瑟,我火速辦了婚禮,結(jié)果婚禮上枉疼,老公的妹妹穿的比我還像新娘皮假。我一直安慰自己,他們只是感情好骂维,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布惹资。 她就那樣靜靜地躺著,像睡著了一般航闺。 火紅的嫁衣襯著肌膚如雪褪测。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天潦刃,我揣著相機(jī)與錄音侮措,去河邊找鬼。 笑死乖杠,一個胖子當(dāng)著我的面吹牛分扎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播胧洒,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼畏吓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卫漫?” 一聲冷哼從身側(cè)響起菲饼,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎汛兜,沒想到半個月后巴粪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年肛根,在試婚紗的時候發(fā)現(xiàn)自己被綠了辫塌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡派哲,死狀恐怖臼氨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芭届,我是刑警寧澤储矩,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站褂乍,受9級特大地震影響持隧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逃片,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一屡拨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧褥实,春花似錦呀狼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至僻澎,卻和暖如春貌踏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窟勃。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工哩俭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拳恋。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像砸捏,于是被迫代替她去往敵國和親谬运。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345