Przejdź do treści

Jak osadzić w swojej aplikacji player YouTube.

  • przez

android-128Cześć. W czwartym już z kolei tutorialu androidowym stworzymy aplikację z osadzonym playerem filmów z serwisu YouTube. Do czego może nam posłużyć taka funkcjonalność? Dajmy na to, że stworzyliśmy grę i chcemy zamieścić w niej filmik będący instrukcją jak grać. Albo potrzebujemy prezentacji video jakiegoś miejsca na Ziemi w aplikacji do rezerwacji wycieczek. Filmik instruktażowy w interaktywnej książce kucharskiej? A może klient zażyczył sobie po prostu umieścić swoją reklamówkę w sekcji „O nas” 🙂

Zastosowań jest wiele. Na potrzeby tego tutoriala stworzymy aplikację, która będzie wyświetlała trailery kolejnych epizodów sagi Star Wars. Nie lubisz Gwiezdnych Wojen? (jest ktoś w ogóle taki? ;)) Nie ma sprawy – weź poszukaj sobie na YouTube kilka filmików które lubisz i zapisz gdzieś do nich linki.

Zastosujemy też w praktyce wiedzę o Recyclerach z poprzednich artykułów.

OK. Zaczynamy.

1. Nowy projekt i dodanie biblioteki YouTubeAndroidPlayerApi

Przede wszystkim pobierz najnowszą wersję biblioteki do obsługi API YouTube’a:

https://developers.google.com/youtube/android/player/downloads/

Gdy już stworzysz nowy projekt w Android Studio, utwórz katalog libs w katalogu app, a następnie skopiuj do niego pobrany plik YouTubeAndroidPlayer.jar

W pliku app/build.gradle dodaj nową bibliotekę:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:recyclerview-v7:27.1.1'

    implementation files('libs/YouTubeAndroidPlayerApi.jar')
}

Nie zapomnij też dodać do AndroidManifest.xml prawa dostępu do internetu:

<uses-permission android:name="android.permission.INTERNET" />

Teraz musisz zarejestrować swoją aplikację aby móc korzystać z API YouTube’a.

https://developers.google.com/youtube/android/player/register

Oczywiście musisz posiadać konto developera, więc jeśli nie masz tam dostępu, to koniecznie utwórz sobie takie konto. W ramach konta tworzysz nowy projekt a następnie w sekcji API & Services / Credentials tworzysz sobie nowy klucz do API (przycisk „Utwórz dane logowania / Klucz API):

Jak już przejdziesz ten proces, skopiuj sobie utworzony klucz do kodu źródłowego Twojej aplikacji, tworząc plik Config.java:

package pl.devzine.tutorial.config;

public interface Config {

    interface Keys {
        String YOUTUBE_API_KEY = "tuTAJ_wklEj_skOPIiowAny_kLUCz-do_API";
    }
}

2. Osadzenie odtwarzacza w aplikacji

W pliku activity_main.xml dodaj kontrolkę FrameLayout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/youtube_frame_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>

Na razie to wystarczy. W tym momencie chcemy jedynie wyświetlić jakikolwiek filmik.

W MainActivity.java dodajmy następujący kod:

public class MainActivity extends AppCompatActivity {

    // fragment z osadzanym playerem
    private YouTubeFragment playerFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();

        // inicjujemy video kluczem do API oraz identyfikatorem filmiku
        // który kopiujemy z parametrów odnośnika do niego:
        // np. https://www.youtube.com/watch?v=Q0CbN8sfihY

        initVideo(Config.Keys.YOUTUBE_API_KEY, "Q0CbN8sfihY");
    }

    private void initVideo(String key, String videoId) {
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();

        // tworzymy fragment z playerem YouTube'a
        playerFragment = new YouTubeFragment();

        // tworzymy paczkę z danymi z kluczem do API i identyfikatorem filmiku
        Bundle bundle = new Bundle();
        bundle.putString(YouTubeFragment.VIDEO_ID, videoId);
        bundle.putString(YouTubeFragment.API_KEY, key);

        // przekazujemy paczkę jako argument do fragmentu
        playerFragment.setArguments(bundle);

        // dodajemy utworzony fragment z playerem do naszego kontenera w layoucie
        ft.add(R.id.youtube_frame_layout, playerFragment).commit();
    }
}

Zdefiniujmy jeszcze klasę YouTubeFragment której instancję tworzyliśmy w powyższym kodzie. Klasa ta będzie dziedziczyć po YouTubePlayerSupportFragment. Obsługiwać będzie ona zdarzenia związane z inicjowaniem playera, a także posłuży do sterowania odtwarzaniem:

public class YouTubeFragment extends YouTubePlayerSupportFragment {

    private static final String TAG = YouTubeFragment.class.getName();

    public static final String VIDEO_ID = "VIDEO_ID";
    public static final String API_KEY = "API_KEY";

    private YouTubePlayer player;
    private ProgressBar progressBar;

    public YouTubeFragment() {
    }

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);

        Bundle data = getArguments();
        String key = data.getString(API_KEY);
        final String videoId = data.getString(VIDEO_ID);

        initialize(key, new YouTubePlayer.OnInitializedListener() {
            @Override
            public void onInitializationSuccess(YouTubePlayer.Provider provider, YouTubePlayer youTubePlayer, boolean wasRestored) {
                YouTubeFragment.this.player = youTubePlayer;
                if (!wasRestored) {
                    playVideo(videoId);
                }
            }

            @Override
            public void onInitializationFailure(YouTubePlayer.Provider provider, YouTubeInitializationResult youTubeInitializationResult) {
                Log.e(TAG, youTubeInitializationResult.toString());
            }
        });
    }

    public void playVideo(String videoId) {
        this.player.cueVideo(videoId);
    }
}

Pobiera ona dane potrzebne do zainicjowania playera, a także implementuje dwie metody z listeneraYouTubePlayer.OnInitializedListener :

  • onInitializationSuccess – metoda wywoływana kiedy inicjalizacja przebiegnie poprawnie, dostaniemy w niej instancję klasy YouTubePlayer, którą sobie przechowamy
  • onInitializationFailure – metoda wywoływana gdy coś pójdzie nie tak, pozwala na obsługę błędów

Dodaliśmy też metodę playVideo która pozwoli na zmianę odtwarzanego filmiku.
Budujemy projekt, uruchamiamy aplikację i już możemy cieszyć się osadzonym filmikiem 🙂

3. Model danych dla filmików oraz layout miniaturki

Mamy już możliwość odtwarzania video w aplikacji. Chcielibyśmy wyświetlać jednak listę z miniaturkami filmików do odtworzenia. Na początek potrzebujemy prostego modelu danych na przechowanie ID filmu oraz jego tytułu:

package pl.devzine.tutorial.model;

public class Video {

    private String videoId;
    private String title;

    public Video(String videoId, String title) {
        this.videoId = videoId;
        this.title = title;
    }

    public String getVideoId() {
        return videoId;
    }

    public void setVideoId(String videoId) {
        this.videoId = videoId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Video{" +
                "videoId='" + videoId + '\'' +
                ", title='" + title + '\'' +
                '}';
    }
}

Mamy model, no to teraz zdefiniujmy widok:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="2dp">

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_centerHorizontal="true"
        android:indeterminate="true" />

    <com.google.android.youtube.player.YouTubeThumbnailView
        android:id="@+id/youtube_thumb_view"
        android:layout_width="match_parent"
        android:layout_height="100dp" />

    <TextView
        android:id="@+id/video_title"
        android:layout_below="@id/youtube_thumb_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/black"
        android:textColor="@color/white"
        android:padding="4dp"
        tools:text="Title" />

</RelativeLayout>

W XMLu z widokiem mamy layout zawierający trzy elementy:

  • ProgressBar – „kręciołek” wyświetlany na czas pobierania miniaturki z internetu
  • YouTubeThumbnailView – kontrolka wyświetlająca miniaturkę z YouTube’a
  • TextView – pole tekstowe na tytuł

Potrzebujemy jeszcze obiektu będącego elementem listy. Jak pamiętamy z poprzednich części, będziemy mogli zasilić kontrolkę RecyclerView elementami różnych typów.

public class ThumbnailListItem {

    public enum ItemType {
        VIDEO
    }

    private ItemType type;
    private Video video;

    public ThumbnailListItem(Video video) {
        this.video = video;
        this.type = ItemType.VIDEO;
    }

    public ItemType getType() {
        return type;
    }

    public Object getValue() {
        if (ItemType.VIDEO.equals(this.type)) {
            return this.video;
        } else {
            return null;
        }
    }
}

4. Źródło danych i RecyclerView

W prawdziwej aplikacji prawdopodobnie pobralibyśmy sobie dane naszych ulubionych filmików z RESTowego API, zapisali w lokalnej bazie danych i pobierali z niej do wyświetlenia. Na potrzeby tutoriala posłużymy się jednak szybkim mockiem, dodając do głównej aktywności metodę:

    private List<ThumbnailListItem> getVideoData() {

        List<ThumbnailListItem> list = Arrays.asList(
            new ThumbnailListItem(new Video("bD7bpG-zDJQ", "The Phantom Menace")),
            new ThumbnailListItem(new Video("gYbW1F_c9eM", "Attack Of The Clones")),
            new ThumbnailListItem(new Video("5UnjrG_N8hU", "Revenge Of The Sith")),
            new ThumbnailListItem(new Video("vZ734NWnAHA", "A New Hope")),
            new ThumbnailListItem(new Video("JNwNXF9Y6kY", "The Empire Strikes Back")),
            new ThumbnailListItem(new Video("16YLjTkK5jE", "Return Of The Jedi")),
            new ThumbnailListItem(new Video("sGbxmsDFVnE", "The Force Awakens")),
            new ThumbnailListItem(new Video("Q0CbN8sfihY", "The Last Jedi"))
        );

        return list;
    }

Przyszedł moment, żeby użyć do czegoś praktycznego wiedzę o Recyclerach, skopiujmy więc sobie kod z poprzedniego samouczka, modyfikując go nieco:

common/recycler/AbstractRecyclerViewHolder.java

public abstract class AbstractRecyclerViewHolder<T> extends RecyclerView.ViewHolder
        implements View.OnClickListener {

    protected ItemClickListener mItemClickListener;

    public AbstractRecyclerViewHolder(View itemView) {
        super(itemView);
        super.itemView.setOnClickListener(this);
    }

    public void setItemClickListener(ItemClickListener itemClickListener) {
        mItemClickListener = itemClickListener;
    }

    @Override
    public void onClick(View view) {
        if (mItemClickListener != null) {
            int adapterPosition = getAdapterPosition();
            if (adapterPosition != RecyclerView.NO_POSITION) {
                mItemClickListener.onItemClick(view, adapterPosition);
            }
        }
    }

    public abstract void bindValues(T item, int position);
}

common/recycler/AbstractAdapter.java

public abstract class AbstractAdapter<I, VH extends AbstractRecyclerViewHolder>
        extends RecyclerView.Adapter<VH> implements ItemClickListener {

    protected List<I> items;

    public AbstractAdapter() {
        this.items = new ArrayList<>();
    }

    public void setItems(Collection<I> items) {
        this.items.clear();
        this.items.addAll(items);
        notifyDataSetChanged();
    }

    public I getItem(int position) {
        return items.get(position);
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    protected View inflateView(ViewGroup parent, int layoutId) {
        return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
    }

}

common/recycler/ItemClickListener.java

public interface ItemClickListener {
    void onItemClick(View view, int position);
}

W poprzednich samouczkach wyjaśniałem ideę stojącą za takim podejściem, nie będę więc teraz przytaczał co, po co i dlaczego 🙂 Można oczywiście polemizować, czy potrzebny nam jest uniwersalny mechanizm obsługi wielu typów elementów listy, skoro mamy w tym przypadku tylko jeden. Po prostu używamy uniwersalnego kodu do szczególnego problemu.

5. ViewHolder dla miniaturki

No dobrze. Mamy abstrakcyjny ViewHolder, stwórzmy więc teraz konkretny dla naszych miniaturek. Będzie on implementował metody z dwóch intefejsów:

  • YouTubeThumbnailLoader.OnThumbnailLoadedListener – zdarzenia związane z pobieraniem miniaturek z YouTube’a
  • YouTubeThumbnailView.OnInitializedListener – zdarzenia związane z inicjacją kontrolek

Posłużą nam one do wykrycia, czy miniaturka została już załadowana i można ukryć „kręciołek”. Dzięki nim obsłużymy też sytuacje gdy wystąpi jakiś błąd.

public class ThumbnailViewHolder extends AbstractRecyclerViewHolder<Video>
        implements YouTubeThumbnailLoader.OnThumbnailLoadedListener, YouTubeThumbnailView.OnInitializedListener {

    private static final String TAG = ThumbnailViewHolder.class.getName();

    YouTubeThumbnailView thumbnailView;
    TextView tilteTextView;
    ProgressBar progressBar;

    public ThumbnailViewHolder(View itemView) {
        super(itemView);
        thumbnailView = itemView.findViewById(R.id.youtube_thumb_view);
        tilteTextView = itemView.findViewById(R.id.video_title);
        progressBar = itemView.findViewById(R.id.progress_bar);
    }

    @Override
    public void bindValues(Video video, int position) {
        tilteTextView.setText(video.getTitle());
        thumbnailView.setTag(video.getVideoId());
        thumbnailView.initialize(Config.Keys.YOUTUBE_API_KEY, this);
    }

    @Override
    public void onThumbnailLoaded(YouTubeThumbnailView youTubeThumbnailView, String s) {
        progressBar.setVisibility(View.GONE);
        Log.d(TAG, "onThumbnailLoaded: " + s);
    }

    @Override
    public void onThumbnailError(YouTubeThumbnailView youTubeThumbnailView, YouTubeThumbnailLoader.ErrorReason errorReason) {
        progressBar.setVisibility(View.GONE);
        Log.e(TAG, "onThumbnailError: " + errorReason.toString());
    }

    @Override
    public void onInitializationSuccess(YouTubeThumbnailView youTubeThumbnailView, YouTubeThumbnailLoader youTubeThumbnailLoader) {
        Log.d(TAG, "onInitializationSuccess");
        youTubeThumbnailLoader.setOnThumbnailLoadedListener(this);
        youTubeThumbnailLoader.setVideo(thumbnailView.getTag().toString());
    }

    @Override
    public void onInitializationFailure(YouTubeThumbnailView youTubeThumbnailView, YouTubeInitializationResult youTubeInitializationResult) {
        progressBar.setVisibility(View.GONE);
        Log.e(TAG, "onInitializationFailure: " + youTubeInitializationResult.toString());
    }
}

6. Adapter dla listy miniaturek

Potrzebujemy jeszcze konkretny adapter dla listy miniaturek, dziedziczący po zdefiniowanej wcześniej klasie abstrakcyjnej AbstractAdapter. Będzie nam tworzył viewholdery dla widoków miniaturek.

public class ThumbnailListAdapter extends AbstractAdapter<ThumbnailListItem, AbstractRecyclerViewHolder> {

    private static final String TAG = ThumbnailListAdapter.class.getSimpleName();

    private ThumbnailClickListener listener;

    @Override
    public AbstractRecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        AbstractRecyclerViewHolder viewHolder = null;

        switch (ThumbnailListItem.ItemType.values()[viewType]) {
            case VIDEO:
                viewHolder = new ThumbnailViewHolder(inflateView(parent, R.layout.thumbnail_item_layout));
                break;
        }

        viewHolder.setItemClickListener(this);

        return viewHolder;
    }

    @Override
    public int getItemViewType(int position) {
        return items.get(position).getType().ordinal();
    }

    @Override
    public void onBindViewHolder(AbstractRecyclerViewHolder holder, int position) {
        holder.bindValues(getItem(position).getValue(), position);
    }

    @Override
    public void onItemClick(View view, int position) {

        Log.d(TAG, "onItemClick: " + Integer.toString(position));

        int itemViewType = getItemViewType(position);

        switch (ThumbnailListItem.ItemType.values()[itemViewType]) {
            case VIDEO:
                listener.onThumbnailClicked((Video) items.get(position).getValue());
                break;
            default:
                break;
        }
    }

    public void setItemClickListener(ThumbnailClickListener listener) {
        this.listener = listener;
    }
}

Jak widzimy w metodzie setItemClickListener, potrzebny jeszcze interfejs do implementacji zdarzenia kliknięcia w miniaturkę:

public interface ThumbnailClickListener {

    void onThumbnailClicked(Video video);
}

7. MainActivity

W końcu możemy poskładać wszystko w całość. Zaimplementujmy w naszej MainActivity interfejs ThumbnailClickListener. Wywołamy w nim kod, który pobierze model powiązany z klikniętym elementem (dane filmiku do odtworzenia). Zainstancjonujmy recycler z odpowiednim adapterem w metodzie onStart oraz przekażmy mu dane do wyrenderowania oraz listener na kliknięcie, którym jest nasza Activity.

public class  MainActivity extends AppCompatActivity implements ThumbnailClickListener {

    private static final String TAG = MainActivity.class.getName();

    private RecyclerView thumbnailsRecycler;
    private ThumbnailListAdapter adapter;
    private YouTubeFragment playerFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        initVideo(Config.Keys.YOUTUBE_API_KEY, "Q0CbN8sfihY");

        // tworzymy adapter dla listy miniaturek
        adapter = new ThumbnailListAdapter();

        // pobieramy z layoutu recycler
        thumbnailsRecycler = findViewById(R.id.thumbnails_recycler);

        // ustawiamy mu layout typu "grid" z dwiema kolumnami
        thumbnailsRecycler.setLayoutManager(new GridLayoutManager(this, 2));

        // ustawiamy recyclerowi adapter
        thumbnailsRecycler.setAdapter(adapter);

        // dodajemy do adaptera elementy stworzone na podstawie danych z "API"
        adapter.setItems(getVideoData());

        // ustawiamy listener na kliknięcie w element listy
        adapter.setItemClickListener(this);
    }

    private void initVideo(String key, String videoId) {

        // ...
    }

    @Override
    public void onThumbnailClicked(Video video) {
        Log.d(TAG, "video: " + video.toString());

        // przekazanie fragmentowi z playerem identyfikatora filmiku do odtworzenia
        playerFragment.playVideo(video.getVideoId());
    }
 
  // ...
}

Dodajmy jeszcze tylko recycler do layoutu activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/youtube_frame_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/thumbnails_recycler"
        android:layout_below="@+id/youtube_frame_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbarStyle="outsideOverlay"
        android:scrollbars="vertical" />

</RelativeLayout>

To wszystko! Aplikacja gotowa, skompilujmy i cieszmy się efektem 🙂 Cały kod niniejszej aplikacji możesz pobrać z GitHub’a. Jeśli nauczyłeś się czegoś przydatnego (a mam nadzieję, że tak), to zostaw komentarz, kliknij w reklamę, czy podziel się linkiem do tego samouczka z innymi na swoim facebooku. Dzięki!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *


The reCAPTCHA verification period has expired. Please reload the page.