Cześć. 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!