📹 小视频模块
版本 PanGrowth

📼 宫格与双Feed小视频

宫格与双Feed小视频提供轻量化的视频浏览入口,适用于首页导流、内容聚合等场景。插件同时支持 NativeView 组件API 控制 两种接入方式。

平台支持:

  • Android: 完整支持宫格与双Feed布局
  • ⚠️ iOS: 暂未实现,显示"当前平台暂不支持宫格小视频"

🚪 接入方式概览

方式推荐场景实现特点
GridVideoNativeView 组件(推荐)Flutter 页面内嵌宫格内容,追求最快集成自动管理创建/销毁,Flutter 布局友好
API 调用需要在原生容器中展示,或兼容历史流程主动控制生命周期

两种方式共用 GridVideoConfig 配置对象,事件回调也通过同一套监听体系。

✅ 方式一:使用 GridVideoNativeView(推荐)

GridVideoNativeView 通过 PlatformView 内嵌原生宫格/双Feed视图,Flutter 端仅需像普通 Widget 一样使用。

基础用法

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

class GridVideoPage extends StatelessWidget {
  const GridVideoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('宫格小视频')),
      body: GridVideoNativeView(
        layout: GridVideoLayout.grid,  // grid-宫格布局, doubleFeed-双Feed布局
        config: const GridVideoConfig(
          scene: 'home_grid',           // 场景标识,用于内容定制
          cardStyle: GridVideoCardStyle.normal,  // normal-普通, staggered-瀑布流
          enableRefresh: true,          // 支持下拉刷新
        ),
      ),
    );
  }
}

双Feed布局示例

GridVideoNativeView(
  layout: GridVideoLayout.doubleFeed,  // 使用双Feed布局
  config: const GridVideoConfig(
    scene: 'feed_channel',
    cardStyle: GridVideoCardStyle.staggered,  // 瀑布流样式
    enableRefresh: true,
  ),
)

使用控制器进行外部控制

通过 GridVideoController 可以在外部控制宫格视频的刷新、滚动等行为:

class GridVideoWithControllerPage extends StatefulWidget {
  const GridVideoWithControllerPage({super.key});

  @override
  State<GridVideoWithControllerPage> createState() => _GridVideoWithControllerPageState();
}

class _GridVideoWithControllerPageState extends State<GridVideoWithControllerPage> {
  late final GridVideoController _controller;

  @override
  void initState() {
    super.initState();
    _controller = GridVideoController();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('宫格小视频'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => _controller.refresh(),
          ),
          IconButton(
            icon: const Icon(Icons.arrow_upward),
            onPressed: () => _controller.scrollToTop(),
          ),
        ],
      ),
      body: GridVideoNativeView(
        layout: GridVideoLayout.grid,
        config: const GridVideoConfig(scene: 'home_grid'),
        controller: _controller,
      ),
    );
  }
}

GridVideoController 方法

方法说明使用场景
refresh()刷新宫格/双Feed数据用户下拉刷新或点击刷新按钮
scrollToTop()滚动到顶部Tab切换回来时快速回到顶部
backRefresh()返回场景时的挽留刷新从详情页返回列表时自动刷新
setUserVisible(bool)控制可见性Tab切换时暂停/恢复播放
canBackPress()校验返回键是否可直接关闭Android返回键拦截
dispose()销毁视频实例并清理状态Widget销毁时调用

监听宫格视频事件

可通过 GridVideoListener 获取组件创建、销毁及交互事件,便于埋点或自定义 UI:

GridVideoNativeView(
  layout: GridVideoLayout.grid,
  config: const GridVideoConfig(scene: 'home_grid'),
  listener: GridVideoListener(
    onGridReady: (gridId) {
      debugPrint('宫格视图就绪: $gridId');
    },
    onGridDisposed: () {
      debugPrint('宫格视图已释放');
    },
    onGridError: (error) {
      debugPrint('宫格视图加载失败: $error');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('宫格加载失败,请稍后重试')),
      );
    },
    onItemClick: (videoId) {
      debugPrint('点击视频: $videoId');
    },
    onItemShow: (videoId) {
      debugPrint('展示视频: $videoId');
    },
    onRefreshComplete: () {
      debugPrint('刷新完成');
    },
  ),
)

生命周期与限制

  • 单实例约束:插件内部限制同一时刻仅允许存在一个宫格视频实例,创建新的 GridVideoNativeView 前需确保旧实例已被销毁。
  • 平台视图限制:PlatformView 由原生渲染,避免在其上方叠加 Positioned/Stack 等复杂布局,必要时可在外层使用安全区域、背景容器。
  • 销毁策略:GridVideoNativeViewdispose 阶段会自动调用 PangrowthContent.destroyGridVideo,如果在外层缓存了 gridId,可在自定义流程中重复调用(幂等)。

🧭 方式二:使用 API 控制宫格视频

API 方式用于历史项目兼容或需要原生容器承载的场景。如无特殊需求,仍建议优先选择 NativeView 方案。

调用流程

  1. createGridVideo(config)createDoubleFeedVideo(config) 创建宫格/双Feed视频,获得 videoId;
  2. showGridVideo(videoId) 打开原生宫格视频页面(Android 启动 Activity,iOS 推入控制器);
  3. 页面结束时调用 destroyGridVideo(videoId) 释放资源。

宫格布局参考实现

class GridVideoLauncher {
  String? _videoId;

  Future<void> openGridVideo() async {
    final result = await PangrowthContent.createGridVideo(
      const GridVideoConfig(
        scene: 'home_grid',
        cardStyle: GridVideoCardStyle.normal,
        enableRefresh: true,
      ),
    );

    if (result['success'] != true) {
      debugPrint('创建宫格视频失败: $result');
      return;
    }

    _videoId = result['videoId'] as String;

    // Android: 可指定容器ID
    // iOS: 可指定展示样式
    await PangrowthContent.showGridVideo(
      _videoId!,
      containerId: null,  // Android专用,默认使用android.R.id.content
      presentationStyle: Platform.isIOS ? 'pageSheet' : null,  // iOS专用
    );
  }

  Future<void> dispose() async {
    if (_videoId != null) {
      await PangrowthContent.destroyGridVideo(_videoId!);
      _videoId = null;
    }
  }
}

双Feed布局参考实现

class DoubleFeedVideoLauncher {
  String? _videoId;

  Future<void> openDoubleFeedVideo() async {
    final result = await PangrowthContent.createDoubleFeedVideo(
      const GridVideoConfig(
        scene: 'feed_channel',
        cardStyle: GridVideoCardStyle.staggered,  // 瀑布流样式
        enableRefresh: true,
      ),
    );

    if (result['success'] != true) {
      debugPrint('创建双Feed视频失败: $result');
      return;
    }

    _videoId = result['videoId'] as String;
    await PangrowthContent.showDoubleFeedVideo(
      _videoId!,
      containerId: null,
      presentationStyle: Platform.isIOS ? 'fullScreen' : null,
    );
  }

  Future<void> dispose() async {
    if (_videoId != null) {
      await PangrowthContent.destroyDoubleFeedVideo(_videoId!);
      _videoId = null;
    }
  }
}

destroyGridVideodestroyDoubleFeedVideo 为幂等调用,即使宫格视频已关闭再次调用也不会抛出异常。

🧩 公共配置项(GridVideoConfig)

以下参数两种接入方式均可使用,未显式赋值时会使用 SDK 默认值。

通用配置

参数类型说明默认值
cardStyleGridVideoCardStyle?卡片样式:<br/>- GridVideoCardStyle.normal: 普通样式<br/>- GridVideoCardStyle.staggered: 瀑布流样式null(使用SDK默认)
sceneString?场景标识,用于内容定制业务,建议传入有业务含义的标识如 'home_grid''feed_channel'null
enableRefreshbool?是否支持下拉刷新null(使用SDK默认)
reportTopPaddingdouble?举报页面顶部内边距,单位 dp,用于适配自定义标题栏null
extraMap<String, dynamic>?自定义扩展字段,将在事件回调中透传null

📡 事件监听

通过 GridVideoListener 获取创建、销毁、交互回调。

常见宫格视频事件如下:

GridVideoEventType触发时机常用字段
gridReady宫格视图创建完成gridId
itemClick用户点击视频卡片videoId(视频ID)
itemShow视频卡片曝光videoId
refresh页面下拉刷新完成-
loadMore列表加载更多完成-
pageShow宫格视图对用户可见contentId(gridId)
pageHide宫格视图离开前台contentId
destroyed原生页面销毁contentId

💡 使用场景与实践

场景1:首页引导位

适合使用宫格布局(2x2),不自动播放,用户点击后进入沉浸式播放:

GridVideoNativeView(
  layout: GridVideoLayout.grid,
  config: const GridVideoConfig(
    scene: 'home_entry',
    cardStyle: GridVideoCardStyle.normal,
    enableRefresh: false,  // 首页引导位通常不需要刷新
  ),
)

场景2:频道聚合页

适合使用双Feed或宫格布局,配合下拉刷新和自动播放:

GridVideoNativeView(
  layout: GridVideoLayout.doubleFeed,
  config: const GridVideoConfig(
    scene: 'channel_feed',
    cardStyle: GridVideoCardStyle.staggered,  // 瀑布流提升视觉效果
    enableRefresh: true,
  ),
)

场景3:TabBar中的宫格视频

配合 GridVideoController 实现Tab切换时的暂停/恢复:

class VideoTabsPage extends StatefulWidget {
  const VideoTabsPage({super.key});

  @override
  State<VideoTabsPage> createState() => _VideoTabsPageState();
}

class _VideoTabsPageState extends State<VideoTabsPage> with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  late final GridVideoController _gridController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _gridController = GridVideoController();

    _tabController.addListener(() {
      // Tab切换时控制视频播放状态
      if (_tabController.index == 1) {
        _gridController.setUserVisible(true);  // 宫格Tab被选中,恢复播放
      } else {
        _gridController.setUserVisible(false);  // 切换到其他Tab,暂停播放
      }
    });
  }

  @override
  void dispose() {
    _tabController.dispose();
    _gridController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('视频内容'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '推荐'),
            Tab(text: '宫格'),
            Tab(text: '关注'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          const Center(child: Text('推荐页面')),
          GridVideoNativeView(
            layout: GridVideoLayout.grid,
            config: const GridVideoConfig(scene: 'tab_grid'),
            controller: _gridController,
          ),
          const Center(child: Text('关注页面')),
        ],
      ),
    );
  }
}

🔗 相关内容

信息流视频卡片(VideoCard)与单卡片(VideoSingleCard)

注意:VideoCard 和 VideoSingleCard 仅提供 API 方式,没有对应的 NativeView Widget。

如需在信息流中集成视频卡片,请使用以下 API 方法:

信息流视频卡片(VideoCard)

// 创建推荐流视频卡片
final card = await PangrowthContent.createVideoCardForRecommend(
  VideoCardConfig(
    scene: 'feed_recommend',
    // 更多配置参数请查看 VideoCardConfig 定义
  ),
);
final cardId = card['videoId'] as String;

// 使用完毕后销毁
await PangrowthContent.destroyVideoCard(cardId);

支持的创建方法:

  • createVideoCardForNews(config): 资讯/新闻信息流
  • createVideoCardForRecommend(config): 推荐流(默认)
  • createVideoCardForSearch(config): 搜索结果

单卡片入口(VideoSingleCard)

// 创建新闻单卡片
final singleCard = await PangrowthContent.createVideoSingleCardForNews(
  VideoSingleCardConfig(
    scene: 'campaign',
    // 更多配置参数请查看 VideoSingleCardConfig 定义
  ),
);
final singleCardId = singleCard['videoId'] as String;

// 使用完毕后销毁
await PangrowthContent.destroyVideoSingleCard(singleCardId);

由于 VideoCard 和 VideoSingleCard 没有 Widget 封装,需要开发者自行处理原生视图的嵌入和生命周期管理。如果需要在 Flutter 页面中使用,建议优先考虑 GridVideoNativeView

🛠️ 故障排查

问题1:宫格视频无法创建或显示空白

可能原因:

  1. 未完成插件初始化,确保调用了 PangrowthContent.start()
  2. 穿山甲SDK配置不正确,检查 appIdappName 是否正确
  3. iOS平台暂不支持,确认当前运行在 Android 设备上

解决方法:

// 1. 确保在使用宫格视频前完成初始化
await PangrowthContent.start(
  startDrama: true,  // 或 startStory: true
);

// 2. 检查当前平台
if (!Platform.isAndroid) {
  debugPrint('当前平台不支持宫格小视频');
  return;
}

// 3. 监听错误事件
GridVideoListener(
  onGridError: (error) {
    debugPrint('宫格视频错误: $error');
  },
)

问题2:切换Tab时宫格视频仍在播放

原因:未调用 setUserVisible 控制播放状态

解决方法:

// Tab切换监听
_tabController.addListener(() {
  final isGridTabActive = _tabController.index == 1;
  _gridController.setUserVisible(isGridTabActive);
});

问题3:宫格视频内存泄漏

原因:未正确销毁宫格视频实例

解决方法:

@override
void dispose() {
  // 方式1: NativeView会自动销毁,但如果使用了Controller需要手动释放
  _gridController.dispose();

  // 方式2: API方式必须手动销毁
  if (_videoId != null) {
    PangrowthContent.destroyGridVideo(_videoId!);
  }

  super.dispose();
}

问题4:宫格视频创建成功但showGridVideo失败

原因:Android端可能是容器ID配置问题,iOS端可能是展示样式不支持

解决方法:

// Android: 使用默认容器ID
await PangrowthContent.showGridVideo(videoId, containerId: null);

// iOS: 使用支持的展示样式
await PangrowthContent.showGridVideo(
  videoId,
  presentationStyle: 'pageSheet',  // 或 'fullScreen', 'formSheet'
);

🌍 平台差异与适配建议

特性AndroidiOS说明
宫格布局✅ 完整支持❌ 暂未实现iOS平台显示"当前平台暂不支持宫格小视频"
双Feed布局✅ 完整支持❌ 暂未实现同上
NativeView组件✅ 可用⚠️ 显示不支持提示iOS端虽然可以创建Widget但会显示提示文本
API方式✅ 可用⚠️ SDK未实现iOS端调用API会返回错误
容器ID(containerId)✅ 支持-仅Android使用,iOS无此参数
展示样式(presentationStyle)-✅ 支持仅iOS使用,Android无此参数

跨平台适配建议

如需在同一代码库中支持Android和iOS,建议:

Widget buildGridVideo() {
  if (Platform.isAndroid) {
    // Android: 使用GridVideoNativeView
    return GridVideoNativeView(
      layout: GridVideoLayout.grid,
      config: const GridVideoConfig(scene: 'home_grid'),
    );
  } else {
    // iOS: 显示占位内容或其他替代方案
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.video_library, size: 64, color: Colors.grey),
          const SizedBox(height: 16),
          const Text('iOS平台暂不支持宫格小视频'),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: () {
              // 可以跳转到沉浸式小视频页面作为替代
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const VideoNativeViewPage(),
                ),
              );
            },
            child: const Text('查看沉浸式小视频'),
          ),
        ],
      ),
    );
  }
}

📚 参考文档


需要进一步协助?

与 LightCore 技术顾问沟通,获取商业化策略与集成支持。