flutter开发仿抖音首页面上下滑动切换播放视频效果

题记
—— 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天。

重要消息


本小节讲述:
1 VideoPlayer 视频播放组件使用
2 VideoPlayerController 的使用分析
3 FutureBuilder 的使用分析
4 PageView构建上下滑动的整屏切换页面
5 TabBar 与 TabBarView 构建左右滑动切换的页面

在这里插入图片描述
1 首先我们来实现页面的主体部分

通过 TabBar 与 TabBarView 实现左右切换的页面


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class MainFind3Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MainFindPage3State();
  }
}

class MainFindPage3State extends State with SingleTickerProviderStateMixin {
  
  List<String> tabTextList = ["关注", "推荐"];
  List<Tab> tabWidgetList = [];
  TabController tabController;

  @override
  void initState() {
    super.initState();

    for (var value in tabTextList) {
      tabWidgetList.add(Tab(
        text: "$value",
      ));
    }
    tabController = new TabController(length: tabTextList.length, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return buildRootBody();
  }

  Widget buildRootBody() {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Positioned(
            left: 0,
            right: 0,
            top: 0,
            bottom: 0,
            child: Container(
              color: Colors.black,
            ),
          ),
          Positioned(
            left: 0,
            right: 0,
            top: 0,
            bottom: 0,
            child: buildTableViewWidget(),
          ),
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            top: 54,
            child: buildTabBarWidget(),
          ),
        ],
      ),
    );
  }
  ///构建 TabBarView
  buildTableViewWidget() {
    return TabBarView(
      controller: tabController,
      children: tabTextList
          .map((value) => Container(
                alignment: Alignment.center,
                child: Text("$value",style: TextStyle(color: Colors.white),),
              ))
          .toList(),
    );
  }
  ///构建顶部标签部分
  buildTabBarWidget() {
    return Container(
      ///对齐在顶部中间
      alignment: Alignment.topCenter,
      child: TabBar(
        controller: tabController,
        tabs: tabWidgetList,
        ///指示器的颜色
        indicatorColor: Colors.white,
        ///指示器的高度
        indicatorWeight: 2.0,
        isScrollable: true,

        ///指示器的宽度与文字对齐
        indicatorSize: TabBarIndicatorSize.label,
      ),
    );
  }
}

在这里是通过 帧布局 将 TabBar 与 TabBarView 叠在一起的。
效果如下


在这里插入图片描述
2 通过 PageView 来实现上下整屏切换效果

文章《flutter跨平台开发一点一滴分析系列文章》中 1.3.3 有记录 PageView 的使用案例

我们将上述 【构建 TabBarView】 处代码替换,使用 PageView 来构建 上下整屏页面切换效果


  ///构建 TabBarView
  buildTableViewWidget() {
    return TabBarView(
      controller: tabController,
      children: tabTextList
          .map((value) => buildTableViewItemWidget(value))
          .toList(),
    );
  }
   /// 用来创建上下滑动的页面
  Widget buildTableViewItemWidget(String value) {

    List<VideoModel> list =[];
    if(value == "推荐"){
      list= videoList;
    }else{
      list = videoList2;
    }
    return PageView.builder(
      /// pageview中 子条目的个数
      itemCount:list.length ,
      /// 上下滑动
        scrollDirection: Axis.vertical,
        itemBuilder: (BuildContext context,int index){
          VideoModel videoModel = list[index];
      return buildPageViewItemWidget(value,videoModel);
    });
  }

这里面用到了 videoList 与 videoList2,保存的数据模型,是在 initState函数中初始化的


  ///推荐模拟数据
  List <VideoModel> videoList =[];
  ///关注模拟数据
  List <VideoModel> videoList2 =[];

  @override
  void initState() {
    super.initState();

   ...

    ///创建模拟数据

    for (int i = 0; i < 10; i++) {
      VideoModel videoModel = new VideoModel();
      videoModel.videoName = "推荐测试数据$i";
      videoModel.pariseCount = i * 22;
      if (i % 3 == 0) {
        videoModel.isAttention = true;
        videoModel.isLike = true;
      } else {
        videoModel.isAttention = false;
        videoModel.isLike = false;
      }
      videoModel.videoImag ="";
      videoModel.videoUrl ="";
      videoList.add(videoModel);
    }

    for (int i = 0; i < 3; i++) {
      VideoModel videoModel = new VideoModel();
      videoModel.videoName = "关注测试数据$i";
      videoModel.pariseCount = i * 22;
      videoModel.isAttention = true;
      if (i % 3 == 0) {
        videoModel.isLike = true;
      } else {
        videoModel.isLike = false;
      }
      videoModel.videoImag ="";
      videoModel.videoUrl ="";
      videoList2.add(videoModel);
    }
  }

对于 VideoModel 来讲,就是我们保存视频信息的数据模型了


class VideoModel {
  ///视频名称
  String videoName ='';
  ///视频链接
  String videoUrl ='';
  ///视频截图
  String videoImag ='';
  ///是否关注
  bool isAttention =false;
  ///关注的个数
  num attentCount =0;
  ///是否喜欢
  bool isLike = false;
  ///点赞的个数
  num pariseCount = 0;
  ///分享的次数
  num shareCount=0;
}

在上述代码中我们也使用到了 buildPageViewItemWidget 函数,如下

  buildPageViewItemWidget(String value, VideoModel videoModel) {
    return FindVideoItemPage(value,videoModel);
  }

在这里直接构建 的 FindVideoItemPage ,看如下 FindVideoItemPage 的定义


///播放视频的页面
class FindVideoItemPage extends StatefulWidget {
  String tabValue;
  VideoModel videoModel;
  FindVideoItemPage(this.tabValue, this.videoModel);

  @override
  State<StatefulWidget> createState() {
    return FindVideoItemPageState();
  }
}

class FindVideoItemPageState extends State<FindVideoItemPage> {
  ///创建视频播放控制 器
  VideoPlayerController videoPlayerController;
  ///控制更新视频加载初始化完成状态更新
  Future videoPlayFuture;

  @override
  void initState() {
    super.initState();

    videoPlayerController =
        VideoPlayerController.network(widget.videoModel.videoUrl);

    videoPlayFuture = videoPlayerController.initialize().then((_) {
      ///视频初始完成后
      ///调用播放
      videoPlayerController.play();
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ///播放视频
        buildVideoWidget(),

        ///控制播放视频按钮
        buildControllWidget(),

        ///底部区域的视频介绍
        buildBottmFlagWidget(),

        ///右侧的用户信息按钮区域
        buildRightUserWidget(),
      ],
    );
  }

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

}

其实 FindVideoItemPage 就是我们 PageView 中构建的子视图了,我们可以看到 在 初始化函数 initState 中 创建了 VideoPlayerController,顾名思义 VideoPlayerController 是用来控制当前页面视频的播放的,在 dispose 中销毁 VideoPlayerController,这个也好理解,就是当前页面都释放掉了,播放的视频当然要停止播放了。

在这里这创建了一个 videoPlayFuture ,是用来监听 VideoPlayerController 初始化状态的,结合 FutureBuilder 来实时更新页面 State,如在方法 buildVideoWidget() 中


 ///播放视频
  buildVideoWidget() {
    return FutureBuilder(
      future: videoPlayFuture,
      builder: (BuildContext contex, value) {
        if (value.connectionState == ConnectionState.done) {
          ///点击事件
          return InkWell(
            onTap: () {
              if (videoPlayerController.value.initialized) {
                /// 视频已初始化
                if (videoPlayerController.value.isPlaying) {
                  /// 正播放 --- 暂停
                  videoPlayerController.pause();
                } else {
                  ///暂停 ----播放
                  videoPlayerController.play();
                }

                setState(() {});
              } else {
                ///未初始化
                videoPlayerController.initialize().then((_) {
                  videoPlayerController.play();
                  setState(() {});
                });
              }
            },

            ///居中
            child: Center(
              /// AspectRatio 组件用来设定子组件宽高比
              child: AspectRatio(
                ///设置视频的大小 宽高比。长宽比表示为宽高比。例如,16:9宽高比的值为16.0/9.0
                aspectRatio: videoPlayerController.value.aspectRatio,
                ///播放视频的组件
                child: VideoPlayer(videoPlayerController),
              ),
            ),
          );
        } else {
          return Container(
            alignment: Alignment.center,
            ///圆形加载进度
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }

FutureBuilder会依赖一个Future,对于FutureBuilder来讲

FutureBuilder({
  this.future,
  this.initialData,
  @required this.builder,
})

future ,FutureBuilder 中依赖的 Future ,通常是一个异步耗时任务,如这里的 videoPlayFuture 是指向 videoPlayerController 的初始化函数initialize(),这是一个异步的耗时操作,
builder ,Widget构建器,该构建器会在Future执行的不同阶段被多次调用,构建格式如下

Function (BuildContext context, AsyncSnapshot snapshot)
  /**
   * snapshot会包含当前异步任务的状态信息及结果信息 ,
   * 比如我们可以通过snapshot.connectionState获取异步任务的状态信息、
   * 通过snapshot.hasError判断异步任务是否有错误等等
   */

而通过 snapshot.connectionState 获取的 ConnectionState 状态有以下值:

enum ConnectionState {
  /// 当前没有异步任务,比如[FutureBuilder]的[future]为null时
  none,
  /// 异步任务处于等待状态
  waiting,
  /// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。
  active,
  /// 异步任务已经终止.
  done,
}

3 通过 VideoPlayer 播放视频

使用 VideoPlayer,我们首先需要添加依赖

  video_player: ^0.6.4

对于 VideoPlayer 来讲,它只接收一个 VideoPlayerController,我们可以通过 VideoPlayerController 来绑定要播放的视频地址

    ///网络链接
    videoPlayerController = VideoPlayerController.network(widget.videoModel.videoUrl);
    ///本地链接
    VideoPlayerController videoPlayerController2 = VideoPlayerController.asset(widget.videoModel.videoUrl);
    ///File形式的视频
    VideoPlayerController videoPlayerController3 = VideoPlayerController.file(File(widget.videoModel.videoUrl));

当 绑定了播放的地址后,可以VideoPlayerController来预加载初始化播放器

videoPlayerController.initialize().then((_) {
      ///视频初始完成后
      ///调用播放
      videoPlayerController.play();
      setState(() {});
    });

对于 initialize() 方法来讲,这是一个异步的耗时操作.

VideoPlayerValue 记录了当前视频播放的一些状态信息

VideoPlayerValue videoPlayerValue = videoPlayerController.value;


///是否初始化完成
bool initialized = videoPlayerValue.initialized;
///是否正在播放
bool isPlaying = videoPlayerValue.isPlaying;
///当前播放的视频的宽高比例
double aspectRatio = videoPlayerValue.aspectRatio;
///当前视频是否缓存 
bool  isBuffer = videoPlayerValue.isBuffering;
///当前视频是否循环
bool isLoop = videoPlayerValue.isLooping;
///当前播放视频的总时长
Duration totalDuration = videoPlayerValue.duration;
///当前播放视频的位置 
Duration currentDuration = videoPlayerValue.position;
              
              

在这里,我们通过 totalDuration 与 currentDuration 就可实现播放进度的进度条绘制。


完毕

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

推荐阅读更多精彩内容