Flutter 仿抖音視頻滑動播放

這兩天用Flutter實現(xiàn)了仿抖音視頻滑動播放的功能

代碼運行環(huán)境 flutter SDK 3.10.2
老規(guī)矩先上效果(使用LICEcap截gif圖總是有問題,來張靜態(tài)的吧)

image
video_player: ^2.4.1
一. video_player基本使用
/// 聲明控制器
late VideoPlayerController _controller;
/// 初始化控制器
_controller = VideoPlayerController.network(list[0]['video_url'])
      ///設(shè)置視頻循環(huán)播放
      ..setLooping(true)
      ///設(shè)置監(jiān)聽
      ..addListener(() {
        setState(() {
        });
      })
      ///初始化
      ..initialize().then((_) async {
        ///初始化完成更新狀態(tài),不然播放器不會播放
        setState(() {
          playOrPauseVideo();
        });
      }).catchError((err) {
        ///播放出錯
        print(err);
      });


/// 顯示視頻
SizedBox(
     height: 240,
     width: MediaQuery.of(context).size.width,
     child: _controller.value.isInitialized
              ? AspectRatio(
                          aspectRatio:
                          _controller.value.aspectRatio,
                         child: VideoPlayer(_controller),
                             )
                             : const Text(
                                    "沒有要播放的視頻",
                                    style: TextStyle(color: Colors.red),
                                ),
                       ),

注意點:在播放器initialize 完后一定要更新播放器的狀態(tài),不然是widget拿不到狀態(tài)改變,是不會播放的

二. 視頻播放,暫停
/// 判斷播放和暫停
  void playOrPauseVideo() {
    setState(() {
      if (_controller.value.isPlaying) {
        _controller.pause();
      } else {
        // If the video is paused, play it.
        _controller.play();
      }
    });
  }
三. 視頻全屏

視頻全屏借助于auto_orientation來實現(xiàn)

auto_orientation: ^2.3.1

代碼實現(xiàn)

void _toggleFullScreen() {
    setState(() {
      if (_isFullScreen) {
        /// 如果是全屏就切換豎屏
        AutoOrientation.portraitAutoMode();

        ///顯示狀態(tài)欄苛骨,與底部虛擬操作按鈕
        SystemChrome.setEnabledSystemUIOverlays(
            [SystemUiOverlay.top, SystemUiOverlay.bottom]);

        _appbar = AppBar(
          //標題居中
          centerTitle: true,
          title: const Text(
            '仿抖音效果',
            style:
                TextStyle(overflow: TextOverflow.ellipsis, color: Colors.white),
          ),
          elevation: 0, //去掉Appbar底部陰影
          //背景顏色
          backgroundColor: Colors.blue,
        );
      } else {
        AutoOrientation.landscapeAutoMode();
        _appbar = null;

        ///關(guān)閉狀態(tài)欄檐束,與底部虛擬操作按鈕
        SystemChrome.setEnabledSystemUIOverlays([]);
      }
      _isFullScreen = !_isFullScreen;
    });
  }
四. 顯示加載進度

加載進度video_play已經(jīng)封裝好了VideoProgressIndicator,直接使用即可,將controller等其他參數(shù)設(shè)置好了就行.

Positioned(
                    bottom: MediaQuery.of(context).padding.bottom,
                    child: SizedBox(
                      width: MediaQuery.of(context).size.width,
                      height: 1,
                      child: VideoProgressIndicator(
                        _controller,
                        allowScrubbing: true,
                        padding: const EdgeInsets.all(0),
                        colors: const VideoProgressColors(
                          playedColor: Colors.white, // 已播放的顏色
                          bufferedColor:
                           Color.fromRGBO(255, 255, 255, .5), // 緩存中的顏色
                          backgroundColor:
                           Color.fromRGBO(255, 255, 255, .3), // 為緩存的顏色
                        ),
                      ),
                    ))
五. 循環(huán)播放視頻,在設(shè)置的控制器的時候使用級聯(lián)操作符設(shè)置下就可以了
 ..setLooping(true)
六. 實現(xiàn)抖音滑動效果

核心原理就是使用PageView來實現(xiàn)的,需要注意的是每次滑動的時候需要將上一個_controller釋放掉以后再重新創(chuàng)建一個,不然上個視頻還是會播放的.具體代碼如下:

PageView.builder(
          physics: const QuickerScrollPhysics(),
          controller: _pageController,
          scrollDirection: Axis.vertical,
          itemCount: list.length,
          onPageChanged: (index) {
            _controller.dispose();
            _controller =
                VideoPlayerController.network(list[index]['video_url'])
                  ..setLooping(true)
                  ..addListener(() {
                    setState(() {
                    });
                  })
                  ..initialize().then((_) async {
                    setState(() {
                      playOrPauseVideo();
                    });
                  }).catchError((err) {
                    print(err);
                  });

            if (index == list.length - 1) {
              Future.delayed(
                  const Duration(milliseconds: 200)).then((lwh) {
                _pageController.jumpToPage(0);
              });
      
            }
          },
          itemBuilder: (context, i) {
            return Stack(
              children: [
                /// 播放器view
                Container(
                  color: Colors.black,
                  child: Center(
                    child: Stack(
                      children: [
                        AppNetImage(
                          fit: BoxFit.fitWidth,
                          imageUrl: list[i]['image_url'],
                          height: 240,
                          width: MediaQuery.of(context).size.width,
                        ),
                        Positioned(
                            child: Stack(
                          children: [
                            InkWell(
                              child: SizedBox(
                                height: 240,
                                width: MediaQuery.of(context).size.width,
                                child: _controller.value.isInitialized
                                    ? AspectRatio(
                                        aspectRatio:
                                            _controller.value.aspectRatio,
                                        child: VideoPlayer(_controller),
                                      )
                                    : const Text(
                                        "沒有要播放的視頻",
                                        style: TextStyle(color: Colors.red),
                                      ),
                              ),
                              onTap: () {
                                playOrPauseVideo();
                              },
                            ),
                          ],
                        )),
                        Positioned(
                          left: MediaQuery.of(context).size.width / 2 - 30,
                          top: 90,
                          child: _controller.value.isPlaying
                              ? const SizedBox()
                              : const Icon(
                                  Icons.play_arrow,
                                  color: Colors.white,
                                  size: 60,
                                ),
                        ),
                      ],
                    ),
                  ),
                ),

                /// 顯示全屏按鈕
                Positioned(
                    bottom: MediaQuery.of(context).padding.bottom + 100,
                    right: 8,
                    child: InkWell(
                      child: const Icon(
                        Icons.aspect_ratio,
                        color: Colors.white,
                        size: 30,
                      ),
                      onTap: () {
                        _toggleFullScreen();
                      },
                    )),

                /// 顯示進度條
                Positioned(
                    bottom: MediaQuery.of(context).padding.bottom,
                    child: SizedBox(
                      width: MediaQuery.of(context).size.width,
                      height: 1,
                      child: VideoProgressIndicator(
                        _controller,
                        allowScrubbing: true,
                        padding: const EdgeInsets.all(0),
                        colors: const VideoProgressColors(
                          playedColor: Colors.white, // 已播放的顏色
                          bufferedColor:
                           Color.fromRGBO(255, 255, 255, .5), // 緩存中的顏色
                          backgroundColor:
                           Color.fromRGBO(255, 255, 255, .3), // 為緩存的顏色
                        ),
                      ),
                    ))
              ],
            );
          },
        )

實現(xiàn)循環(huán)的效果就是當PageView滑動到最后一個時然后jumpToPage(0)來實現(xiàn)循環(huán)的.

if (index == list.length - 1) {
              Future.delayed(
                  const Duration(milliseconds: 200)).then((lwh) {
                _pageController.jumpToPage(0);
              });
      
            }
七. 遺留問題待解決

當頁面滑動到中間位置時視頻播放會有問題,可以通過重寫B(tài)ouncingScrollPhysics的方法來解決.

八. 思考探索

之前用iOS來實現(xiàn)這個功能的時候采用的是使用ScrollerView,然后創(chuàng)建三個子View,依次循環(huán)顯示視頻播放器View來實現(xiàn)這種效果,這樣做的原因就是為了性能來考慮,不需要根據(jù)數(shù)據(jù)源去創(chuàng)建更多的View,flutter實現(xiàn)的話代碼如下.

PageView(
    scrollDirection: Axis.horizontal,
    children: [
      Container(
        height: 300,
        color: Colors.pink,
        child: const Center(
          child: Text("This is a page1"),
        ),
      ),
      Container(
        color: Colors.teal,
        child: const Center(
          child: Text("This is a page2"),
        ),
      ),
      Container(
        color: Colors.amber,
        child: const Center(
          child: Text("This is a page3"),
        ),
      ),
    ],
  );

但是在flutter中PageView 提供了 builder方法,builder方法是懶加載的并不是一開始就創(chuàng)建大量的view,思路和iOS的tableView大同小異.但是這兩種方式哪種的效率更高,還有待考證.今天先寫到這里,有時間再試下第二種思路,檢測下哪種更好吧.

九. 結(jié)束

demo地址請移步: 項目地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猴蹂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狭吼,更是在濱河造成了極大的恐慌氧苍,老刑警劉巖交掏,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件樊零,死亡現(xiàn)場離奇詭異朦拖,居然都是意外死亡圃阳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門璧帝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捍岳,“玉大人,你說我怎么就攤上這事裸弦。” “怎么了作喘?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵理疙,是天一觀的道長。 經(jīng)常有香客問我泞坦,道長窖贤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任贰锁,我火速辦了婚禮赃梧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘豌熄。我一直安慰自己授嘀,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布锣险。 她就那樣靜靜地躺著蹄皱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芯肤。 梳的紋絲不亂的頭發(fā)上巷折,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音崖咨,去河邊找鬼锻拘。 笑死,一個胖子當著我的面吹牛击蹲,可吹牛的內(nèi)容都是我干的署拟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼歌豺,長吁一口氣:“原來是場噩夢啊……” “哼芯丧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起世曾,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤缨恒,失蹤者是張志新(化名)和其女友劉穎谴咸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骗露,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡岭佳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了萧锉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片珊随。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖柿隙,靈堂內(nèi)的尸體忽然破棺而出叶洞,到底是詐尸還是另有隱情,我是刑警寧澤禀崖,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布衩辟,位于F島的核電站,受9級特大地震影響波附,放射性物質(zhì)發(fā)生泄漏艺晴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一掸屡、第九天 我趴在偏房一處隱蔽的房頂上張望封寞。 院中可真熱鬧,春花似錦仅财、人聲如沸狈究。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谦炒。三九已至,卻和暖如春风喇,著一層夾襖步出監(jiān)牢的瞬間宁改,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工魂莫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留还蹲,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓耙考,卻偏偏與公主長得像谜喊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倦始,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

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