Bakgrunnslyd på Android med MediaSessionCompat

En av de mest populære bruksområder for mobile enheter er å spille av lyd gjennom musikkstrømningstjenester, nedlastede podcaster eller et annet antall lydkilder. Selv om dette er en ganske vanlig funksjon, er det vanskelig å implementere, med mange forskjellige stykker som må bygges riktig for å gi brukeren din full Android-opplevelse. 

I denne opplæringen lærer du om MediaSessionCompat fra Android support-biblioteket, og hvordan det kan brukes til å skape en riktig bakgrunnslydtjeneste for brukerne.

Setup

Det første du må gjøre er å inkludere Android support-biblioteket i prosjektet ditt. Dette kan gjøres ved å legge til følgende linje i modulens build.gradle filen under avhengighetsnoden.

kompilere 'com.android.support:support-v13:24.2.1'

Etter at du har synkronisert prosjektet ditt, opprett en ny Java-klasse. For dette eksempelet vil jeg ringe til klassen BackgroundAudioService. Denne klassen må utvides MediaBrowserServiceCompat. Vi vil også implementere følgende grensesnitt: MediaPlayer.OnCompletionListener og AudioManager.OnAudioFocusChangeListener.

Nå som din MediaBrowserServiceCompat implementering er opprettet, la oss ta et øyeblikk å oppdatere AndroidManifest.xml før du går tilbake til denne klassen. På toppen av klassen må du be om WAKE_LOCK tillatelse.

Deretter, innenfor applikasjon node, erklære din nye tjeneste med følgende intent-filteret elementer. Disse vil tillate at tjenesten din kan avskjære kontrollknapper, hodetelefonhendelser og mediesøking for enheter, for eksempel Android Auto (selv om vi ikke vil gjøre noe med Android Auto for denne opplæringen, er det fortsatt nødvendig med grunnleggende støtte for den. MediaBrowserServiceCompat).

      

Til slutt må du erklære bruken av MediaButtonReceiver fra Android supportbiblioteket. Dette vil tillate deg å avskjære mediekontrollknappens interaksjoner og hodetelefonhendelser på enheter som kjører KitKat og tidligere.

     

Nå som din AndroidManifest.xml filen er ferdig, du kan lukke den. Vi skal også lage en annen klassen som heter MediaStyleHelper, som ble skrevet av Ian Lake, Developer Advocate på Google, for å rydde opp etableringen av mediestilvarsler.

offentlig klasse MediaStyleHelper / ** * Bygg et varsel ved hjelp av informasjonen fra den aktuelle mediesesjonen. Gjør tung bruk * av @link MediaMetadataCompat # getDescription () for å trekke ut relevant informasjon. * @param-kontekst Kontekst brukt til å konstruere varselet. * @param mediaSession Media økt for å få informasjon. * @return En forhåndsbygd melding med informasjon fra den aktuelle mediesesjonen. * / offentlig statisk NotificationCompat.Builder fra (Kontekst kontekst, MediaSessionCompat mediaSession) MediaControllerCompat controller = mediaSession.getController (); MediaMetadataCompat mediaMetadata = controller.getMetadata (); MediaDescriptionCompat description = mediaMetadata.getDescription (); NotificationCompat.Builder builder = ny NotificationCompat.Builder (kontekst); byggeren .setContentTitle (description.getTitle ()) .setContentText (description.getSubtitle ()) .setSubText (description.getDescription ()) .setLargeIcon (description.getIconBitmap ()) .setContentIntent (controller.getSessionActivity ()) .setDeleteIntent (MediaButtonReceiver .buildMediaButtonPendingIntent (kontekst, PlaybackStateCompat.ACTION_STOP)) .setVisibility (NotificationCompat.VISIBILITY_PUBLIC); returbygger; 

Når det er opprettet, fortsett og lukk filen. Vi vil fokusere på bakgrunnslyttjenesten i neste avsnitt.

Bygg ut bakgrunnslyden

Nå er det på tide å grave inn i kjernen i å lage medieappen din. Det er noen medlemsvariabler som du vil deklarere først for denne prøveappen: a Mediaspiller for den faktiske avspillingen, og a MediaSessionCompat objekt som vil administrere metadata og avspillingskontroller / tilstander.

privat MediaPlayer mMediaPlayer; privat MediaSessionCompat mMediaSessionCompat;

I tillegg trenger du en BroadcastReceiver som lytter etter endringer i hodetelefontilstanden. For å holde det enkelt, vil denne mottakeren sette pause på Mediaspiller, hvis det spiller.

privat BroadcastReceiver mNoisyReceiver = ny BroadcastReceiver () @Override public void onReceive (Kontekst kontekst, Intent intent) if (mMediaPlayer! = null && mMediaPlayer.isPlaying ()) mMediaPlayer.pause (); ;

For den endelige medlemsvariabelen vil du opprette en MediaSessionCompat.Callback objekt, som brukes til å håndtere avspillingsstatus når mediesesjon handler.

privat MediaSessionCompat.Callback mMediaSessionCallback = ny MediaSessionCompat.Callback () @Override public void onPlay () super.onPlay ();  @Override public void onPause () super.onPause ();  @Override public void onPlayFromMediaId (String mediaId, Bundle extras) super.onPlayFromMediaId (mediaId, extras); ;

Vi vil revidere hver av metodene senere i denne opplæringen, da de vil bli brukt til å drive operasjoner i vår medieapp.

Det er to metoder som vi også må deklarere, selv om de ikke trenger å gjøre noe i forbindelse med denne opplæringen: onGetRoot () og onLoadChildren (). Du kan bruke følgende kode for standardinnstillingene dine.

@Nullable @Override public BrowserRoot onGetRoot (@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) if (TextUtils.equals (clientPackageName, getPackageName ())) returner ny BrowserRoot (getString (R.string.app_name) null );  returnere null;  // Ikke viktig for generell lydtjeneste, kreves for klassen @Override public void onLoadChildren (@NonNull String parentId, @NonNull Result> resultat) result.sendResult (null); 

Til slutt vil du vil overstyre onStartCommand () metode, som er inngangspunktet i din Service. Denne metoden vil ta Intent som sendes til Service og send det til MediaButtonReceiver klasse.

@Override public int onStartCommand (Intent intent, int flagg, int startId) MediaButtonReceiver.handleIntent (mMediaSessionCompat, hensikt); returnere super.onStartCommand (hensikt, flagg, startId); 

Initialisering av alle ting

Nå som du har opprettet basiselementvariablene, er det på tide å initialisere alt. Vi gjør dette ved å ringe til ulike hjelpemetoder i onCreate ().

@Override public void onCreate () super.onCreate (); initMediaPlayer (); initMediaSession (); initNoisyReceiver (); 

Den første metoden, initMediaPlayer (), vil initialisere Mediaspiller objekt som vi opprettet øverst i klassen, be om partial wake lock (som er grunnen til at vi krevde tillatelsen i AndroidManifest.xml), og sett spillerens volum.

privat tomt initMediaPlayer () mMediaPlayer = nytt MediaPlayer (); mMediaPlayer.setWakeMode (getApplicationContext (), PowerManager.PARTIAL_WAKE_LOCK); mMediaPlayer.setAudioStreamType (AudioManager.STREAM_MUSIC); mMediaPlayer.setVolume (1.0f, 1.0f); 

Den neste metoden, initMediaSession (), er hvor vi initialiserer MediaSessionCompat objekt og led den til medieknappene og kontrollmetoder som tillater oss å håndtere avspilling og brukerinngang. Denne metoden starter ved å opprette en ComponentName objekt som peker på Android support biblioteket MediaButtonReceiver klassen, og bruker det til å opprette en ny MediaSessionCompat. Vi passerer da MediaSession.Callback objekt som vi opprettet tidligere til det, og angi flaggene som er nødvendige for å motta medieknappinnganger og styresignaler. Deretter oppretter vi en ny Intent for å håndtere medieknappinnganger på pre-Lollipop-enheter, og angi mediesessionstoken for vår tjeneste.

private void initMediaSession () ComponentName mediaButtonReceiver = nytt ComponentName (getApplicationContext (), MediaButtonReceiver.class); mMediaSessionCompat = ny MediaSessionCompat (getApplicationContext (), "Tag", mediaButtonReceiver, null); mMediaSessionCompat.setCallback (mMediaSessionCallback); mMediaSessionCompat.setFlags (MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); Intent mediaButtonIntent = ny Intent (Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setClass (dette, MediaButtonReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast (dette, 0, mediaButtonIntent, 0); mMediaSessionCompat.setMediaButtonReceiver (pendingIntent); setSessionToken (mMediaSessionCompat.getSessionToken ()); 

Endelig registrerer vi BroadcastReceiver som vi opprettet på toppen av klassen slik at vi kan lytte til hodetelefonbytterhendelser.

Private void initNoisyReceiver () // Håndtak hodetelefoner kommer unplugged. kan ikke gjøres via en åpen mottaker IntentFilter filter = ny IntentFilter (AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver (mEoisyReceiver, filter); 

Håndtere lydfokus

Nå som du er ferdig med å initialisere BroadcastReceiver, MediaSessionCompat og Mediaspiller Objekter, det er på tide å se på lydfokus. 

Selv om vi kanskje tror at våre egne lydapps er de viktigste for øyeblikket, vil andre apper på enheten konkurrere om å lage egne lyder, for eksempel et e-postvarsel eller mobilspill. For å kunne jobbe med disse ulike situasjonene bruker Android-systemet lydfokus for å bestemme hvordan lyd skal håndteres. 

Det første tilfellet vi vil håndtere, begynner avspilling og forsøker å motta enhetens fokus. I din MediaSessionCompat.Callback objekt, gå inn i onPlay () metode og legg til følgende tilstandskontroll.

@Override public void onPlay () super.onPlay (); hvis (! successfullyRetrievedAudioFocus ()) return; 

Ovennevnte kode vil ringe en hjelpemetode som forsøker å hente fokus, og hvis det ikke kan, vil det bare returnere. I en ekte app, vil du ønske å håndtere mislykket lydavspilling mer grasiøst. successfullyRetrievedAudioFocus () vil få en referanse til systemet Lydbehandling, og forsøk å be om lydfokus for streaming av musikk. Det vil da returnere a boolean som representerer om forespørselen lyktes eller ikke.

privat boolsk vellykketRetrievedAudioFocus () AudioManager audioManager = (AudioManager) getSystemService (Context.AUDIO_SERVICE); int resultat = audioManager.requestAudioFocus (dette, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); returresultat == AudioManager.AUDIOFOCUS_GAIN; 

Du vil legge merke til at vi også går forbi dette inn i det requestAudioFocus () metode, som forbinder OnAudioFocusChangeListener med vår tjeneste. Det er noen forskjellige stater du vil lytte etter for å være en "god medborger" i enhetens app-økosystem.

  • AudioManager.AUDIOFOCUS_LOSS: Dette skjer når en annen app har bedt om lydfokus. Når dette skjer, bør du stoppe lydavspilling i appen din.
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: Denne tilstanden er angitt når en annen app ønsker å spille av lyd, men det forventer bare å trenge fokus for en kort stund. Du kan bruke denne tilstanden til å pause lydavspillingen.
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: Når lydfokus blir bedt om, men kaster en "can duck" -status, betyr det at du kan fortsette avspillingen, men bør ta volumet litt ned. Dette kan oppstå når en varslingslyd spilles av enheten.
  • AudioManager.AUDIOFOCUS_GAIN: Den endelige staten vi skal diskutere er AUDIOFOCUS_GAIN. Dette er staten når en avspillbar lydavspilling har fullført, og appen din kan gjenoppta på sine tidligere nivåer.

En forenklet onAudioFocusChange () tilbakeringing kan se slik ut:

@Override public void onAudioFocusChange (int fokusChange) switch (focusChange) tilfelle AudioManager.AUDIOFOCUS_LOSS: if (mMediaPlayer.isPlaying ()) mMediaPlayer.stop ();  gå i stykker;  tilfelle AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: mMediaPlayer.pause (); gå i stykker;  tilfelle AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: if (mMediaPlayer! = null) mMediaPlayer.setVolume (0.3f, 0.3f);  gå i stykker;  tilfelle AudioManager.AUDIOFOCUS_GAIN: if (mMediaPlayer! = null) if (! mMediaPlayer.isPlaying ()) mMediaPlayer.start ();  mMediaPlayer.setVolume (1.0f, 1.0f);  gå i stykker; 

Forstå MediaSessionCompat.Callback

Nå som du har en generell struktur sammen for din Service, det er på tide å dykke inn i MediaSessionCompat.Callback. I den siste delen har du lagt litt til onPlay () for å sjekke om lydfokus ble gitt. Under betinget utsagn vil du sette inn MediaSessionCompat protester mot aktiv, gi den en tilstand av STATE_PLAYING, og tilordne de riktige tiltakene som er nødvendige for å opprette pause-knapper på kontrollpanelene for Lollipop Lock-skjerm, telefon og Android Wear.

@Override public void onPlay () super.onPlay (); hvis (! successfullyRetrievedAudioFocus ()) return;  mMediaSessionCompat.setActive (true); setMediaPlaybackState (PlaybackStateCompat.STATE_PLAYING); ...

De setMediaPlaybackState () Metoden ovenfor er en hjelpemetode som lager en PlaybackStateCompat.Builder objekt og gir den de riktige handlinger og tilstander, og deretter bygger og forbinder a PlaybackStateCompat med din MediaSessionCompat gjenstand.

privat tomt settMediaPlaybackState (int state) PlaybackStateCompat.Builder playbackstateBuilder = ny PlaybackStateCompat.Builder (); hvis (state == PlaybackStateCompat.STATE_PLAYING) playbackstateBuilder.setActions (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE);  ellers playbackstateBuilder.setActions (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY);  playbackstateBuilder.setState (state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0); mMediaSessionCompat.setPlaybackState (playbackstateBuilder.build ()); 

Det er viktig å merke seg at du trenger både ACTION_PLAY_PAUSE og heller ikke ACTION_PAUSE eller ACTION_PLAY flagg i handlingene dine for å få ordentlig kontroll på Android Wear.

Tilbake i onPlay (), du vil vise et spillvarsel som er knyttet til din MediaSessionCompat objekt ved å bruke MediaStyleHelper klasse som vi definerte tidligere, og deretter vise det varselet.

privat tomt showPlayingNotification () NotificationCompat.Builder builder = MediaStyleHelper.from (BackgroundAudioService.this, mMediaSessionCompat); hvis (builder == null) return;  builder.addAction (new NotificationCompat.Action (android.R.drawable.ic_media_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent (dette, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle (new NotificationCompat.MediaStyle (). setShowActionsInCompactView (0) .setMediaSession (mMediaSessionCompat.getSessionToken ())); builder.setSmallIcon (R.mipmap.ic_launcher); NotificationManagerCompat.from (BackgroundAudioService.this) .notify (1, builder.build ()); 

Til slutt begynner du Mediaspiller ved slutten av onPlay ().

@Override public void onPlay () super.onPlay (); ... showPlayingNotification (); mMediaPlayer.start (); 

Når tilbakeringingen mottar en pause-kommando, onPause () vil bli kalt. Her vil du sette pause på Mediaspiller, sett staten til STATE_PAUSED, og vis en midlertidig melding.

@Override public void onPause () super.onPause (); hvis (mMediaPlayer.isPlaying ()) mMediaPlayer.pause (); setMediaPlaybackState (PlaybackStateCompat.STATE_PAUSED); showPausedNotification (); 

Våre showPausedNotification () hjelpemetoden vil se ut som showPlayNotification () metode.

privat tomt showPausedNotification () NotificationCompat.Builder builder = MediaStyleHelper.from (dette, mMediaSessionCompat); hvis (builder == null) return;  builder.addAction (new NotificationCompat.Action (android.R.drawable.ic_media_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent (dette, PlaybackStateCompat.ACTION_PLAY_PAUSE))); builder.setStyle (new NotificationCompat.MediaStyle (). setShowActionsInCompactView (0) .setMediaSession (mMediaSessionCompat.getSessionToken ())); builder.setSmallIcon (R.mipmap.ic_launcher); NotificationManagerCompat.from (this) .notify (1, builder.build ()); 

Den neste metoden i tilbakekallingen som vi skal diskutere, onPlayFromMediaId (), tar a string og a Bunt som parametere. Dette er tilbakeringingsmetoden du kan bruke til å endre lydspor / innhold i appen din. 

For denne opplæringen vil vi ganske enkelt godta en ressurs-ID og forsøke å spille det, og deretter gjenopprette sessions metadata. Som du har lov til å passere en Bunt inn i denne metoden kan du bruke den til å tilpasse andre aspekter av medieavspillingen, for eksempel å sette opp en tilpasset bakgrunnslyd for et spor.

@Override public void onPlayFromMediaId (String mediaId, Bundle extras) super.onPlayFromMediaId (mediaId, extras); prøv AssetFileDescriptor afd = getResources (). openRawResourceFd (Integer.valueOf (mediaId)); hvis (afd == null) return;  prøv mMediaPlayer.setDataSource (afd.getFileDescriptor (), afd.getStartOffset (), afd.getLength ());  fangst (IllegalStateException e) mMediaPlayer.release (); initMediaPlayer (); mMediaPlayer.setDataSource (afd.getFileDescriptor (), afd.getStartOffset (), afd.getLength ());  afd.close (); initMediaSessionMetadata ();  fangst (IOException e) return;  prøv mMediaPlayer.prepare ();  fangst (IOException e)  // Arbeid med statister her hvis du vil

Nå som vi har diskutert de to hovedmetodene i denne tilbakeringingen du vil bruke i appene dine, er det viktig å vite at det finnes andre valgfrie metoder du kan bruke til å tilpasse tjenesten. Noen metoder inkluderer onSeekTo (), som lar deg endre avspillingsposisjonen til innholdet ditt, og på bestilling (), som vil akseptere en string betegner typen kommando, a Bunt for ekstra informasjon om kommandoen, og a ResultReceiver tilbakeringing, som vil tillate deg å sende egendefinerte kommandoer til din Service.

@Override public void onCommand (String command, Bundle extras, ResultReceiver cb) super.onCommand (command, extras, cb); hvis (COMMAND_EXAMPLE.equalsIgnoreCase (kommando)) // Custom command here @Override public void onSeekTo (long pos) super.onSeekTo (pos); 

Rive ned

Når lydfilen vår er ferdig, vil vi ønske å bestemme hva vår neste tiltak vil være. Mens du kanskje vil spille det neste sporet i appen din, holder vi det enkelt og slipper Mediaspiller.

@Override public void onCompletion (MediaPlayer mediaPlayer) if (mMediaPlayer! = Null) mMediaPlayer.release (); 

Til slutt vil vi gjøre noen ting i onDestroy () metode for vår Service. Først, få en referanse til systemtjenesten Lydbehandling, og ring abandonAudioFocus () med vår AudioFocusChangeListener som en parameter, som vil varsle andre apper på enheten som du gir opp lydfokus. Deretter avregistreres BroadcastReceiver Det ble satt opp for å lytte til hodetelefonendringer, og slippe ut MediaSessionCompat gjenstand. Til slutt vil du avbryte avspillingskontrollvarselet.

@Override public void onDestroy () super.onDestroy (); AudioManager audioManager = (AudioManager) getSystemService (Context.AUDIO_SERVICE); audioManager.abandonAudioFocus (this); unregisterReceiver (mNoisyReceiver); mMediaSessionCompat.release (); NotificationManagerCompat.from (dette) .cancel (1); 

På dette tidspunktet bør du ha en fungerende grunnleggende bakgrunnslyd Service ved hjelp av MediaSessionCompat for avspillingskontroll over enheter. Selv om det allerede har vært mye involvert i å skape tjenesten, bør du kunne styre avspilling fra appen din, et varsel, låseskjermkontroller på pre-Lollipop-enheter (Lollipop og over vil bruke varselet på låseskjermen), og fra eksterne enheter, for eksempel Android Wear, en gang på Service har blitt startet.

Starte og kontrollere innhold fra en aktivitet

Mens de fleste kontroller blir automatiske, vil du fortsatt ha litt arbeid for å starte og kontrollere en mediesesjon fra kontrollene dine i appen. I det minste vil du ha en MediaBrowserCompat.ConnectionCallback, MediaControllerCompat.CallbackMediaBrowserCompat, og MediaControllerCompat objekter opprettet i appen din.

MediaControllerCompat.Callback vil ha en metode kalt onPlaybackStateChanged () som mottar endringer i avspillingsstatus, og kan brukes til å holde brukergrensesnittet synkronisert.

privat MediaControllerCompat.Callback mMediaControllerCompatCallback = ny MediaControllerCompat.Callback () @Override public void onPlaybackStateChanged (PlaybackStateCompat state) super.onPlaybackStateChanged (state); hvis (tilstand == null) return;  bytte (state.getState ()) tilfelle PlaybackStateCompat.STATE_PLAYING: mCurrentState = STATE_PLAYING; gå i stykker;  tilfelle PlaybackStateCompat.STATE_PAUSED: mCurrentState = STATE_PAUSED; gå i stykker; ;

MediaBrowserCompat.ConnectionCallback har en onConnected () metode som vil bli kalt når en ny MediaBrowserCompat objekt er opprettet og tilkoblet. Du kan bruke dette til å initialisere din MediaControllerCompat objekt, knytt det til din MediaControllerCompat.Callback, og knytte den sammen med MediaSessionCompat fra din Service. Når det er ferdig, kan du starte lydavspilling fra denne metoden.

privat MediaBrowserCompat.ConnectionCallback mMediaBrowserCompatConnectionCallback = ny MediaBrowserCompat.ConnectionCallback () @Override public void onConnected () super.onConnected (); prøv mMediaControllerCompat = ny MediaControllerCompat (MainActivity.this, mMediaBrowserCompat.getSessionToken ()); mMediaControllerCompat.registerCallback (mMediaControllerCompatCallback); setSupportMediaController (mMediaControllerCompat); getSupportMediaController (). getTransportControls (). playFromMediaId (String.valueOf (R.raw.warner_tautz_off_broadway), null);  fangst (RemoteException e) ;

Du vil merke at kodestykket ovenfor bruker getSupportMediaController (). getTransportControls () å kommunisere med mediesesjonen. Med samme teknikk kan du ringe onPlay () og onPause () i lydtjenesten din MediaSessionCompat.Callback gjenstand.

hvis (mCurrentState == STATE_PAUSED) getSupportMediaController (). getTransportControls (). spill (); mCurrentState = STATE_PLAYING;  andre hvis (getSupportMediaController (). getPlaybackState (). getState () == PlaybackStateCompat.STATE_PLAYING) getSupportMediaController (). getTransportControls (). pause ();  mCurrentState = STATE_PAUSED; 

Når du er ferdig med lydavspillingen, kan du stoppe lydtjenesten og koble fra MediaBrowserCompat objekt, som vi vil gjøre i denne opplæringen når dette Aktivitet er ødelagt.

@Override protected void onDestroy () super.onDestroy (); hvis (getSupportMediaController (). getPlaybackState (). getState () == PlaybackStateCompat.STATE_PLAYING) getSupportMediaController (). getTransportControls (). pause ();  mMediaBrowserCompat.disconnect (); 

Wrapping Up

Puh! Som du ser, er det mange bevegelige stykker involvert i å skape og bruke en bakgrunnslydtjeneste på riktig måte. 

I denne opplæringen har du opprettet en tjeneste som spiller en enkel lydfil, lytter etter endringer i lydfokus og koblinger til MediaSessionCompat å gi universell avspillingskontroll på Android-enheter, inkludert telefoner og Android Wear. Hvis du kjører i veibeskyttelse mens du arbeider gjennom denne opplæringen, anbefaler jeg sterkt at du sjekker ut den tilhørende Android-prosjektkoden på Envato Tuts + s GitHub.

Og sjekk ut noen av våre andre Android-kurs og opplæringsprogrammer her på Envato Tuts+!