从卡片堆中呈现的 YouTube 视频列表中,第一个视频开始在每张卡片中滑动

From a list of YouTube videos rendered in a card stack, the 1st video starts playing in every card on swipe

我正在尝试使用 Youtube 视频创建类似 Tinder 的滑动功能。我将详细说明我要实现的目标。

分步分解:

  1. 使用 Youtube 数据 API v3.
  2. 获取 Youtube 视频

youtube _model.dart

// To parse this JSON data, do
//
//     final youtubeSearchVideos = youtubeSearchVideosFromJson(jsonString);

import 'dart:convert';

YoutubeSearchVideos youtubeSearchVideosFromJson(String str) =>
    YoutubeSearchVideos.fromJson(json.decode(str));

String youtubeSearchVideosToJson(YoutubeSearchVideos data) =>
    json.encode(data.toJson());

class YoutubeSearchVideos {
  YoutubeSearchVideos({
    required this.kind,
    required this.etag,
    this.nextPageToken,
    this.prevPageToken,
    required this.regionCode,
    required this.pageInfo,
    required this.items,
  });

  String kind;
  String etag;
  String? nextPageToken;
  String? prevPageToken;
  String regionCode;
  PageInfo pageInfo;
  List<Item> items;

  factory YoutubeSearchVideos.fromJson(Map<String, dynamic> json) =>
      YoutubeSearchVideos(
        kind: json["kind"],
        etag: json["etag"],
        nextPageToken: json["nextPageToken"],
        prevPageToken: json["prevPageToken"],
        regionCode: json["regionCode"],
        pageInfo: PageInfo.fromJson(json["pageInfo"]),
        items: List<Item>.from(json["items"].map((x) => Item.fromJson(x))),
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "etag": etag,
        "nextPageToken": nextPageToken,
        "prevPageToken": prevPageToken,
        "regionCode": regionCode,
        "pageInfo": pageInfo.toJson(),
        "items": List<dynamic>.from(items.map((x) => x.toJson())),
      };
}

class Item {
  Item({
    required this.kind,
    required this.etag,
    required this.id,
    required this.snippet,
  });

  String kind;
  String etag;
  Id id;
  Snippet snippet;

  factory Item.fromJson(Map<String, dynamic> json) => Item(
        kind: json["kind"],
        etag: json["etag"],
        id: Id.fromJson(json["id"]),
        snippet: Snippet.fromJson(json["snippet"]),
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "etag": etag,
        "id": id.toJson(),
        "snippet": snippet.toJson(),
      };
}

class Id {
  Id({
    required this.kind,
    required this.videoId,
  });

  String kind;
  String videoId;

  factory Id.fromJson(Map<String, dynamic> json) => Id(
        kind: json["kind"],
        videoId: json["videoId"],
      );

  Map<String, dynamic> toJson() => {
        "kind": kind,
        "videoId": videoId,
      };
}

class Snippet {
  Snippet({
    required this.publishedAt,
    required this.channelId,
    required this.title,
    required this.description,
    required this.thumbnails,
    required this.channelTitle,
    required this.liveBroadcastContent,
    required this.publishTime,
  });

  DateTime publishedAt;
  String channelId;
  String title;
  String description;
  Thumbnails thumbnails;
  String channelTitle;
  String liveBroadcastContent;
  DateTime publishTime;

  factory Snippet.fromJson(Map<String, dynamic> json) => Snippet(
        publishedAt: DateTime.parse(json["publishedAt"]),
        channelId: json["channelId"],
        title: json["title"],
        description: json["description"],
        thumbnails: Thumbnails.fromJson(json["thumbnails"]),
        channelTitle: json["channelTitle"],
        liveBroadcastContent: json["liveBroadcastContent"],
        publishTime: DateTime.parse(json["publishTime"]),
      );

  Map<String, dynamic> toJson() => {
        "publishedAt": publishedAt.toIso8601String(),
        "channelId": channelId,
        "title": title,
        "description": description,
        "thumbnails": thumbnails.toJson(),
        "channelTitle": channelTitle,
        "liveBroadcastContent": liveBroadcastContent,
        "publishTime": publishTime.toIso8601String(),
      };
}

class Thumbnails {
  Thumbnails({
    required this.thumbnailsDefault,
    required this.medium,
    required this.high,
  });

  Default thumbnailsDefault;
  Default medium;
  Default high;

  factory Thumbnails.fromJson(Map<String, dynamic> json) => Thumbnails(
        thumbnailsDefault: Default.fromJson(json["default"]),
        medium: Default.fromJson(json["medium"]),
        high: Default.fromJson(json["high"]),
      );

  Map<String, dynamic> toJson() => {
        "default": thumbnailsDefault.toJson(),
        "medium": medium.toJson(),
        "high": high.toJson(),
      };
}

class Default {
  Default({
    required this.url,
    required this.width,
    required this.height,
  });

  String url;
  int width;
  int height;

  factory Default.fromJson(Map<String, dynamic> json) => Default(
        url: json["url"],
        width: json["width"],
        height: json["height"],
      );

  Map<String, dynamic> toJson() => {
        "url": url,
        "width": width,
        "height": height,
      };
}

class PageInfo {
  PageInfo({
    required this.totalResults,
    required this.resultsPerPage,
  });

  int totalResults;
  int resultsPerPage;

  factory PageInfo.fromJson(Map<String, dynamic> json) => PageInfo(
        totalResults: json["totalResults"],
        resultsPerPage: json["resultsPerPage"],
      );

  Map<String, dynamic> toJson() => {
        "totalResults": totalResults,
        "resultsPerPage": resultsPerPage,
      };
}

youtube_api_service.dart

    import 'package:http/http.dart' as http;
    import 'package:starcast_intros/models/youtube_search.dart';
    import 'package:starcast_intros/private_keys.dart';
    
    class YoutubeApi {
      static const String youtubeAPI =
          'https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResults=5&q=surfing&type=video&videoDefinition=standard&videoDimension=2d&videoDuration=short&videoEmbeddable=true&key=$YOUTUBE_DATA_API_KEY';
    
      Future<YoutubeSearchVideos> fetchVideos() async {
        try {
          final response = await http.get(Uri.parse(youtubeAPI));
    
          if (response.statusCode == 200) {
            return youtubeSearchVideosFromJson(response.body);
          }
    
          throw Exception('Failed to fetch videos ${response.body}');
        } catch (e) {
          print(e);
          throw Exception('Failed to fetch videos $e');
        }
      }
    }

2. After retrieving the list of youtube video IDs from the API, render the Youtube videos like Tinder cards which can be swiped left or right.

import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:starcast_intros/models/youtube_search.dart';
import 'package:starcast_intros/services/youtube_api.dart';
import 'package:tcard/tcard.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);
  static const HOME = 'Home';

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

class _HomeState extends State<Home> {
  late Future<YoutubeSearchVideos> futureVideos;

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

    final youtubeAPI = YoutubeApi();
    futureVideos = youtubeAPI.fetchVideos();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<YoutubeSearchVideos>(
      future: futureVideos,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          List<Widget> cards = List.generate(
            snapshot.data!.items.length,
            (int index) {
              YoutubePlayerController _controller = YoutubePlayerController(
                initialVideoId: snapshot.data!.items[index].id.videoId,
                flags: YoutubePlayerFlags(
                  autoPlay: false,
                  mute: true,
                  isLive: false,
                  disableDragSeek: true,
                  loop: false,
                  forceHD: false,
                ),
              );

              return Container(
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(16.0),
                  boxShadow: [
                    BoxShadow(
                      offset: Offset(0, 17),
                      blurRadius: 23.0,
                      spreadRadius: -13.0,
                      color: Colors.black54,
                    )
                  ],
                ),
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(16.0),
                  child: YoutubePlayer(
                    controller: _controller,
                  ),
                ),
              );
            },
          );

          return TCard(
            size: Size(
              MediaQuery.of(context).size.width,
              MediaQuery.of(context).size.height,
            ),
            cards: cards,
          );
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }

        // By default, show a loading spinner.
        return SpinKitDoubleBounce(
          color: Theme.of(context).accentColor,
          size: 75.0,
        );
      },
    );
  }
}
  1. 请注意,您需要一个 Youtube API 密钥(使用 Google 控制台创建)来检索视频列表。我正在使用 Youtube 搜索 API。如果您不想发出请求或创建 API 密钥,您可能可以使用下面给出的 JSON:

{ "kind": "youtube#searchListResponse", "etag": "E2FpjhO0gVzn8gmf9Q1VSJ72Rwk", "nextPageToken": "CAUQAA",
"regionCode": "IN", "pageInfo": { "totalResults": 1000000, "resultsPerPage": 5 }, "items": [ { "kind": "youtube#searchResult", "etag": "HmJbuO71viHMk8216TydPkfPIAg", "id": { "kind": "youtube#video", "videoId": "xIspFfN3vfs" }, "snippet": { "publishedAt": "2020-03-06T22:13:55Z", "channelId": "UCR03gYk1xLMV4ko8ljxTeIA", "title": "Nick O'Bea : How to Wing surf", "description": "Wing Surfing How to: Skills and Drills on the wing with Nick O'Bea 5'1"x26" 90 liters from supsurfmachines.com 6 meter F1 Swing in 10-12 mph 1020 AXIS ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/xIspFfN3vfs/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Rush Mark Rush", "liveBroadcastContent": "none", "publishTime": "2020-03-06T22:13:55Z" } }, { "kind": "youtube#searchResult", "etag": "PbsGbvz61v4Fpq4DwtLexeIB708", "id": { "kind": "youtube#video", "videoId": "0zO26j6vNGg" }, "snippet": { "publishedAt": "2016-08-10T08:07:07Z", "channelId": "UCjYiM-YLuOQN-4S0I7PfgVg", "title": "Alison Teal: Surfing Hawaii Volcano Eruption", "description": "Kilauea Volcano is erupting on the Big Island of Hawaii and flowing into the ocean for the first time since 2011. Alison, a surfer and film maker, travels the world ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/0zO26j6vNGg/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Alison's Adventures", "liveBroadcastContent": "none", "publishTime": "2016-08-10T08:07:07Z" } }, { "kind": "youtube#searchResult", "etag": "Ab0zXs-KAbFMVeH4jBajBEj60yU", "id": { "kind": "youtube#video", "videoId": "pPGaGZTMc_4" }, "snippet": { "publishedAt": "2011-12-05T17:08:27Z", "channelId": "UChug4c-a2tUGgZ-XeEKdxZQ", "title": "Strongbow Neon Night Surfing Bondi", "description": "To mark the start of summer, Strongbow joined forces with legendary surfing filmmaker Jack McCoy (Endless Summer II), Bali Strickland and Eugene Tan ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/pPGaGZTMc_4/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Surfers Village TV", "liveBroadcastContent": "none", "publishTime": "2011-12-05T17:08:27Z" } }, { "kind": "youtube#searchResult", "etag": "XjmemrMgMcym-3mlYs53ie_w3t4", "id": { "kind": "youtube#video", "videoId": "4uwtqRBE4Kk" }, "snippet": { "publishedAt": "2010-08-13T02:10:28Z", "channelId": "UCTYHNSWYy4jCSCj1Q1Fq0ew", "title": "Andy Irons - i surf because short film", "description": "Andy Irons is one of the world's greatest ever surfers. A 3 times world champion made famous by his epic battles with Kelly Slater. But outside all the victories ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/4uwtqRBE4Kk/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "Billabong", "liveBroadcastContent": "none", "publishTime": "2010-08-13T02:10:28Z" } }, { "kind": "youtube#searchResult", "etag": "FZQyT7mq6dN1LX4M5RjXzVJxrtQ", "id": { "kind": "youtube#video", "videoId": "kGvs0Nv5zJo" }, "snippet": { "publishedAt": "2013-11-15T09:31:05Z", "channelId": "UCNSfJB-VQeHpv5ThtV1VtBA", "title": "Wave cinematographer captures surfer's last wave", "description": "On Wednesday morning, well known Wave Cinematographer Larry Haynes was filming those big sets from the shore, and was rolling on Kirk Passmore as the ...", "thumbnails": { "default": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/default.jpg", "width": 120, "height": 90 }, "medium": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/mqdefault.jpg", "width": 320, "height": 180 }, "high": { "url": "https://i.ytimg.com/vi/kGvs0Nv5zJo/hqdefault.jpg", "width": 480, "height": 360 } }, "channelTitle": "KITV", "liveBroadcastContent": "none", "publishTime": "2013-11-15T09:31:05Z" } } ] }

卡上堆满不同的视频后,我在最上面刷卡。我一滑动,第一张卡片中的视频就会出现在下面的卡片中(第二张卡片)。我希望第二个视频在第二张卡中播放,因为所有视频 ID 都不同。

如果我稍微拖动并按住它,我可以在第二张卡片中看到第二个视频的缩略图。但是,一旦我向右滑动,第二张卡片中的视频(第二个视频)就会被第一张卡片中的视频(第一个视频)替换。

如此重复直到最后一张牌。

任何破解此问题的帮助将不胜感激。感谢期待。

干杯。

为每张 YouTube 卡片添加一个唯一的密钥,您可以使用 YouTube ID 作为密钥

When to Use Keys