You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter VideoPlayer实现类Twitter视频播放器效果需求

问题描述

我有一个用于展示视频的Flutter VideoPlayer Widget,希望实现类似Twitter应用的视频播放器效果:

  • 当视频为9:16的竖屏视频时,非全屏状态下需居中缩放以填充播放器的全部可用宽度(允许裁剪)
  • 进入Fullscreen模式后,视频需完整填充屏幕且不被裁剪
    当前代码已能正确显示横屏等其他比例的视频。我正在使用FlutterFlow,也可编写原生Flutter代码,以下是我的自定义Widget代码:
import "package:flutter/material.dart";
import "package:video_player/video_player.dart";

class Videoplayer extends StatefulWidget {
  const Videoplayer({
    Key? key,
    this.width,
    this.height,
    this.videopath,
  }) : super(key: key);

  final double? width;
  final double? height;
  final String? videopath;

  @override
  _VideoplayerState createState() => _VideoplayerState();
}

class _VideoplayerState extends State<Videoplayer> {
  late VideoPlayerController _controller;
  bool _isFullScreen = false;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset(widget.videopath!)
      ..initialize().then((_) {
        setState(() {
          // Ensure that the video starts playing when it's initialized
          _controller.play();
        });
      });

    // Enable video controls
    _controller.setVolume(1.0); // Set volume to maximum
    _controller.setLooping(true); // Enable looping
    _controller.setPlaybackSpeed(1.0); // Set playback speed to normal
  }

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

  void _toggleFullScreen() {
    setState(() {
      _isFullScreen = !_isFullScreen;
    });

    if (_isFullScreen) {
      _controller.pause(); // Pause video when entering fullscreen
    } else {
      _controller.play(); // Resume video when exiting fullscreen
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleFullScreen,
      child: _isFullScreen
          ? AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            )
          : _buildVideoPreview(),
    );
  }

  Widget _buildVideoPreview() {
    return Center(
      child: AspectRatio(
        aspectRatio: _controller.value.aspectRatio,
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            VideoPlayer(_controller),
            VideoPlayerControls(controller: _controller),
          ],
        ),
      ),
    );
  }
}

class VideoPlayerControls extends StatefulWidget {
  final VideoPlayerController controller;

  const VideoPlayerControls({Key? key, required this.controller})
      : super(key: key);

  @override
  _VideoPlayerControlsState createState() => _VideoPlayerControlsState();
}

class _VideoPlayerControlsState extends State<VideoPlayerControls> {
  late bool _isPlaying;

  @override
  void initState() {
    super.initState();
    _isPlaying = widget.controller.value.isPlaying;
    widget.controller.addListener(_listener);
  }

  void _listener() {
    if (!mounted) return;
    setState(() {
      _isPlaying = widget.controller.value.isPlaying;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedSwitcher(
          duration: Duration(milliseconds: 300),
          reverseDuration: Duration(milliseconds: 300),
          child: _isPlaying
              ? SizedBox.shrink()
              : Container(
                  color: Colors.black.withOpacity(0.6),
                  child: Center(
                    child: IconButton(
                      icon: Icon(Icons.play_arrow, color: Colors.white),
                      onPressed: () {
                        widget.controller.play();
                      },
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            setState(() {
              if (_isPlaying) {
                widget.controller.pause();
              } else {
                widget.controller.play();
              }
            });
          },
        ),
      ],
    );
  }

  @override
  void dispose() {
    widget.controller.removeListener(_listener);
    super.dispose();
  }
}

Twitter视频播放器效果示例


解决方案

要实现需求,核心是根据视频比例和全屏状态动态调整视频的适配逻辑,以下是修改后的完整代码:

import "package:flutter/material.dart";
import "package:video_player/video_player.dart";

class Videoplayer extends StatefulWidget {
  const Videoplayer({
    Key? key,
    this.width,
    this.height,
    this.videopath,
  }) : super(key: key);

  final double? width;
  final double? height;
  final String? videopath;

  @override
  _VideoplayerState createState() => _VideoplayerState();
}

class _VideoplayerState extends State<Videoplayer> {
  late VideoPlayerController _controller;
  bool _isFullScreen = false;
  // 定义9:16竖屏比例的判断阈值
  static const double _nineSixteenRatio = 9 / 16;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset(widget.videopath!)
      ..initialize().then((_) {
        setState(() {
          _controller.play();
        });
      });

    _controller.setVolume(1.0);
    _controller.setLooping(true);
    _controller.setPlaybackSpeed(1.0);
  }

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

  void _toggleFullScreen() {
    setState(() {
      _isFullScreen = !_isFullScreen;
    });
    // 全屏切换时保持播放(可根据需求调整为暂停/继续)
    _isFullScreen ? _controller.play() : _controller.play();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleFullScreen,
      child: _isFullScreen ? _buildFullScreenVideo() : _buildVideoPreview(),
    );
  }

  Widget _buildVideoPreview() {
    final videoRatio = _controller.value.aspectRatio;
    final containerWidth = widget.width ?? MediaQuery.of(context).size.width;

    return Container(
      width: containerWidth,
      height: widget.height,
      child: Center(
        child: videoRatio == _nineSixteenRatio
            ? // 9:16竖屏视频:填充宽度,居中裁剪
              FittedBox(
                  fit: BoxFit.cover,
                  alignment: Alignment.center,
                  child: SizedBox(
                    width: containerWidth,
                    height: containerWidth / videoRatio,
                    child: Stack(
                      alignment: Alignment.center,
                      children: [
                        VideoPlayer(_controller),
                        VideoPlayerControls(controller: _controller),
                      ],
                    ),
                  ),
                )
            : // 其他比例视频:保持原比例显示
              AspectRatio(
                  aspectRatio: videoRatio,
                  child: Stack(
                    alignment: Alignment.center,
                    children: [
                      VideoPlayer(_controller),
                      VideoPlayerControls(controller: _controller),
                    ],
                  ),
                ),
      ),
    );
  }

  Widget _buildFullScreenVideo() {
    return Scaffold(
      backgroundColor: Colors.black,
      body: OrientationBuilder(
        builder: (context, orientation) {
          return Center(
            child: FittedBox(
              fit: BoxFit.contain,
              alignment: Alignment.center,
              child: SizedBox(
                width: orientation == Orientation.portrait
                    ? MediaQuery.of(context).size.width
                    : MediaQuery.of(context).size.height * _controller.value.aspectRatio,
                height: orientation == Orientation.portrait
                    ? MediaQuery.of(context).size.width / _controller.value.aspectRatio
                    : MediaQuery.of(context).size.height,
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    VideoPlayer(_controller),
                    VideoPlayerControls(controller: _controller),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class VideoPlayerControls extends StatefulWidget {
  final VideoPlayerController controller;

  const VideoPlayerControls({Key? key, required this.controller})
      : super(key: key);

  @override
  _VideoPlayerControlsState createState() => _VideoPlayerControlsState();
}

class _VideoPlayerControlsState extends State<VideoPlayerControls> {
  late bool _isPlaying;

  @override
  void initState() {
    super.initState();
    _isPlaying = widget.controller.value.isPlaying;
    widget.controller.addListener(_listener);
  }

  void _listener() {
    if (!mounted) return;
    setState(() {
      _isPlaying = widget.controller.value.isPlaying;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 300),
          reverseDuration: const Duration(milliseconds: 300),
          child: _isPlaying
              ? const SizedBox.shrink()
              : Container(
                  color: Colors.black.withOpacity(0.6),
                  child: Center(
                    child: IconButton(
                      icon: const Icon(Icons.play_arrow, color: Colors.white),
                      onPressed: () {
                        widget.controller.play();
                      },
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            setState(() {
              _isPlaying ? widget.controller.pause() : widget.controller.play();
            });
          },
        ),
      ],
    );
  }

  @override
  void dispose() {
    widget.controller.removeListener(_listener);
    super.dispose();
  }
}

关键修改说明

  • 非全屏竖屏适配:通过判断视频比例是否为9:16,使用FittedBox的BoxFit.cover模式让视频填充容器宽度,超出部分居中裁剪,匹配Twitter的展示效果。
  • 全屏模式处理:用Scaffold作为全屏容器,结合OrientationBuilder监听屏幕方向,通过BoxFit.contain确保视频完整显示,无裁剪。
  • 容器约束优化:非全屏时明确指定容器宽度,保证FittedBox能准确计算填充范围,避免布局异常。

内容的提问来源于stack exchange,提问作者Mark

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 15:59:54