美文网首页
Flutter——实现网易云音乐的渐进式卡片切换

Flutter——实现网易云音乐的渐进式卡片切换

作者: 吉哈达 | 来源:发表于2020-10-15 11:29 被阅读0次

    介绍

    预览图

    image

    我们可以看到页面下方切换的卡片效果

    分析

    首先动画以 x轴分为两部分 : 左侧文字 和 右侧图片

    右侧图片以 z轴 分为 : 上、下

    仔细观察,可以看到它的动画流程大致如下:

    上层显示的是当前图片,下层显示的时下一张
    1、左侧文字淡入淡出切换
    2、右侧图片的上层,与左侧文字同时淡出
    3、之后下层图片上移到 上层图片的位置
    4、移动完成后,淡入一张下层图片,
    5、于此同时新的文字淡入
    

    实现

    首先我们定义一个类

    class MusicCalendar extends WidgetState with SingleTickerProviderStateMixin{}
    
    因为这个动画比较复杂,实际开发时用了provider,
    代码中可能会看到musicCalendarVM, 它主要是用来持有和控制状态及一些数据
    我尽量把里面的代码移出来
    

    MusicCalendar

    当页面初始完成后,我们会执行init()这个方法:

     ///这个方法后面还会见到,它的执行会使动画开始,即淡出-移动-淡入
     ///在这里讲,是让你对流程大概有了解,方便理解后面的代码
      init(){
        if(streamSubscription.isPaused){
          streamSubscription.resume();
        }
      }
    

    首先创建一些变量

    //淡出/淡入动画
      AnimationController fadeController;
      Animation fadeAnim;
      //图片外层是 stack,所以下面两个变量用于定位
      //我们根据动画的进度配合下面的两个变量,就可以达到移动图片的效果
      //具体可以看下面的实现
      double aboveRightMax;
      double aboveBottomMax;
      
    

    我们再看一下布局,代码较多,我把说明写在注释里

    去掉一些不必要的代码
    
    //root layout
    
    Container(
          child: Stack(
            children: <Widget>[
              ///date 这个不用管,跟咱们做的没关系
              Positioned(
                top: getWidthPx(10),
                child: Text('后天',style: TextStyle(fontSize: getSp(28),color: Colors.black,fontWeight: FontWeight.bold),),
              ),
              
              ///这里是左侧 文字部分,它要做的动画 就是淡入和淡出
              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,),
                  ),
                ),
              ),
                ///这里是右侧图片区域
              Positioned(
                top: getWidthPx(30),
                right: 0,
                child: imageSwitcher(),
              ),
    
            ],
          ),
        );
    
    
    

    我们来看一下 这个方法:imageSwitcher()

    Widget imageSwitcher(){
        return Container(
        //这里我们限定一下右侧图片区域的整体大小 
        //注意,图片要小于这个值
          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
              ///这里我额外放置了一张假的图片,下面细说
              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 与文字一样
    

    这里之所以不同,是因为两者执行的实际和动画方向不同,所以未共用一个动画。above和文字一样,由animation控制,我们不用管它,来看一下below吧。

    它的 opacity属性与musicCalendarVM.opacity绑定,而这个opacity属性的刷新主要涉及两个方法

    回顾一下它要做的效果,淡入(实际移动由fake来做,我们后面讲)
    
      void showBelow(){
      ///这里我们每20毫秒更新一下不透明度
        Timer timer = Timer.periodic(Duration(milliseconds: 20), (timer){
          if(opacity >= 1.0){
            ///当不透明度>=1.0时,我们结束timer
            timer.cancel();
            //这个时候也就意味着,below淡入完成,
            //我们可以将上层图片也淡入(这个淡入的是 above)
            ///渐显above和title
            fadeController.reverse().whenComplete((){
              ///当above淡入后,隐藏fake
              showFake = false; notifyListeners();
              ///实际上在below淡入前,fake做了移动,移动到左上角了(冒充 above)
              ///因此,above完成淡入后,我们要重置fake位置,显示fake(冒充 below)
              /// 嘿嘿
              right = 0; bottom = 0;
              ///当然 fake显示的图片也应该是上层图片的下一张
              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();
        });
    
      }
    

    我们来看一下哪里调用了showBelow()

    
    如果你不太熟悉Provider或者bedrock框架的话,这里简单来讲,
    就是页面初试完后,开始执行clock监听并执行相应的动画操作。
    
    ///开头的那个方法
      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){
            ///当上层动画淡出完成后
            ///title和 above 渐隐,同时fake上移
            fadeController.forward().whenComplete((){
                //这里的right和bottom与fake绑定,我们稍后介绍
                //实际上这里的right和bottom理论上已经等于右边的值
                //但实际上还是会出现偏差,这里进行校准
              right = aboveRightMax;
              bottom = aboveBottomMax;
              notifyListeners();
              ///更新index =》 dataList的index
              incrementIndex();
              ///插入新的below
              ///调用了我们上的方法
              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的工作流程介绍:

    当页面初始时:
        above图片为第一张,fake和below为第二张
        above在右上角,below和fake在右下角,且fake遮挡below(实际below为不可见)
    
    当动画开始时:
    
        above淡出,于此同时fake滑动到左上角。
        
    当淡出动画结束后,我们将 above、below显示的图片index+1, 这里below还是不可见的。
    然后,我们将below淡入(它始终在右下角),同时above也是淡入
    (因为速度极快,且与fake重合,你是看不出它的淡入的)
    
    操作完成后:
        我们将fake(在左上)隐藏,并调整它的right和bottom为0,这样又到了右下,
        随后再显示它,这样又遮挡了below,
        至此一个轮回就结束了
    

    我们大致看一下fake的主要属性:

    //控制它的位置
    double right = 0;   double bottom = 0;
    
    showFake//控制fake的隐藏,在上面的方法中有出现过
    

    更新这两个属性的位置在:

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

    updatePosition()方法则在 animationListener中调用

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

    到了这里,整个动画效果就实现了,如果有点乱,可以在demo中对照源码和真机效果来理解。

    谢谢大家的阅读,欢迎指出不足支出 :)

    Demo

    内部搜索即可

    仿网易云音乐

    相关文章

      网友评论

          本文标题:Flutter——实现网易云音乐的渐进式卡片切换

          本文链接:https://www.haomeiwen.com/subject/twmnpktx.html