I Android er menuer en vigtig del af brugergrænsefladen, og de giver brugeren mulighed for at interagere med applikationen på en enkel og effektiv måde. Menusystemet i Android er meget fleksibelt og giver udviklere mulighed for at definere menuer i XML-ressourcer og derefter inflatere dem i deres aktiviteter. I denne sektion vil vi gå igennem, hvordan man opretter og håndterer et Options-menu i Android, herunder oprettelsen af et menu-XML, inflation af menuen i aktivitetens livscyklus og hvordan man reagerer på brugerens valg.

Et menu i Android kan oprettes i XML, som mange andre Android-ressourcer. Når du opretter et menu, er det vigtigt at forstå, hvordan forskellige menuelementer fungerer og interagerer med hinanden. Menuerne gemmes typisk i mappen res/menu og kan defineres med <menu>-tagget. Hvert menuobjekt defineres med et <item>-tag, der indeholder de nødvendige attributter som id, title, icon, og showAsAction.

Oprettelse af et Options-menu

For at oprette et Options-menu i Android, skal du følge nogle grundlæggende trin. Først skal du definere menuen i XML og derefter inflatere den i din aktivitet.

  1. Definer menuen i XML: Opret en XML-fil i res/menu mappen, som beskriver dine menuobjekter. Et grundlæggende eksempel på en menu i XML kunne være:

xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_settings" android:title="@string/settings" android:showAsAction="never"/> </menu>

I dette eksempel har vi defineret et enkelt menupunkt med id'et menu_settings og teksten "Settings". Attributten showAsAction="never" betyder, at dette menupunkt aldrig vil blive vist i Action Baren, men i stedet vil det blive vist i overflow-menuen.

  1. Inflatere menuen i aktivitetens livscyklus: Når du har oprettet XML-filen, skal du sørge for at inflatere menuen i din aktivitet. Dette gøres i metoden onCreateOptionsMenu(), som er ansvarlig for at oprette og vise menuen i din aktivitet. Du kan inflatere menuen som følger:

java
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; }

I denne metode anvender vi getMenuInflater()-metoden til at inflatere vores menu-XML og tilføje menuobjekterne til menuen.

  1. Håndtering af brugerens valg: Når brugeren vælger et menupunkt, skal applikationen kunne reagere på dette valg. Dette gøres i metoden onOptionsItemSelected(). Hvis brugeren vælger "Settings"-menupunktet, kan vi for eksempel vise en toast-meddelelse som feedback:

java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_settings) { Toast.makeText(this, "Settings", Toast.LENGTH_LONG).show(); } else { return super.onOptionsItemSelected(item); } return true; }

Dette er en simpel implementering, hvor vi viser en toast, når brugeren vælger indstillingsmenuen. Du kan naturligvis erstatte toast med en handling som at starte en ny aktivitet.

Brug af showAsAction-attributten

En af de vigtigste aspekter ved oprettelsen af et menu i Android er korrekt brug af showAsAction-attributten. Denne attribut styrer, hvordan menuen vises i Action Baren. De mulige værdier for showAsAction er:

  • ifRoom: Menuen vises i Action Baren, hvis der er nok plads.

  • withText: Både ikonet og teksten vises.

  • never: Menuen vises aldrig i Action Baren, kun i overflow-menuen.

  • always: Menuen vises altid i Action Baren, men vær opmærksom på, at pladsen er begrænset.

Du kan kombinere flere af disse muligheder med en lodret streg (|), som for eksempel showAsAction="ifRoom|withText", for at tilpasse visningen af menuen i Action Baren.

Håndtering af undermenuer og grupper af menupunkter

Android giver også mulighed for at oprette undermenuer. Undermenuer defineres ved at inkludere et <menu>-element indenfor et andet <menu>-element. For eksempel:

xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_main" android:title="Main"> <menu> <item android:id="@+id/menu_subitem1" android:title="Subitem 1"/> <item android:id="@+id/menu_subitem2" android:title="Subitem 2"/> </menu> </item> </menu>

Gruppering af menuobjekter kan også være nyttigt. Android understøtter grupper, hvor flere menuobjekter kan grupperes sammen og styres kollektivt. Du kan for eksempel vise eller skjule alle elementer i en gruppe med setGroupVisible(), aktivere eller deaktivere dem med setGroupEnabled() eller gøre dem checkable med setGroupCheckable().

xml
<group android:id="@+id/menu_group">
<item android:id="@+id/menu_item1" android:title="Item 1"/>
<item android:id="@+id/menu_item2" android:title="Item 2"/> </group>

Dette gør det nemt at håndtere flere relaterede menupunkter samtidigt.

Vigtige overvejelser

Når du arbejder med menuer i Android, er det vigtigt at overveje både brugervenlighed og præstation. At vise for mange elementer i Action Baren kan gøre brugergrænsefladen rodet og svær at navigere. Derfor bør man bruge showAsAction="ifRoom" og kun vise de vigtigste menupunkter direkte i Action Baren.

Desuden bør menuer kun indeholde de handlinger, der er relevante for den aktuelle kontekst af aktiviteten. En god praksis er at holde menuen enkel og intuitiv, så brugeren hurtigt kan finde de funktioner, de har brug for.

Hvordan gemme og læse data på Android-enheder

For at kunne arbejde med brugerdata effektivt i en Android-applikation, er det vigtigt at forstå de forskellige metoder til at gemme og hente data. Dette kan være alt fra at gemme simple navne i SharedPreferences, til at arbejde med tekstfiler i intern og ekstern lager. I denne sektion vil vi gennemgå, hvordan man implementerer datalagring og læsning i Android, både via SharedPreferences og ved at arbejde med tekstfiler på intern opbevaring.

Når du arbejder med Android, skal du forstå forskellen mellem de forskellige opbevaringsmetoder. For simple indstillinger og små data, som for eksempel brugerens navn eller præferencer, er SharedPreferences en enkel og effektiv løsning. Hvis du derimod har brug for at gemme større datamængder eller komplekse objekter, kan du vælge at arbejde med interne tekstfiler.

Brug af SharedPreferences til at gemme og hente data

For at gemme et navn ved hjælp af SharedPreferences i en Android-applikation, er det nødvendigt at oprette globale variabler og referencer til de nødvendige UI-elementer. For eksempel kan vi definere en konstant, der bruges som nøgle for at gemme brugerens navn:

java
private final String NAME = "NAME"; private EditText mEditTextName;

Herefter skal du få adgang til SharedPreferences og hente det gemte navn i onCreate()-metoden. Hvis navnet er blevet gemt tidligere, vises det på skærmen, ellers vises en standard hilsen:

java
TextView textView = (TextView)findViewById(R.id.textView);
SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE); String name = sharedPreferences.getString(NAME, null); if (name == null) { textView.setText("Hello"); } else { textView.setText("Welcome back " + name + "!"); } mEditTextName = (EditText)findViewById(R.id.editTextName);

Når navnet skal gemmes, kan du oprette en metode som denne:

java
public void saveName(View view) {
SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); editor.putString(NAME, mEditTextName.getText().toString()); editor.commit(); }

Når applikationen kører, kan brugeren gemme sit navn, og programmet vil automatisk hente det igen, næste gang applikationen åbnes. Det er vigtigt at forstå, at når man gemmer data med SharedPreferences, sker ændringerne ikke, medmindre commit()-metoden bliver kaldt. Uden dette vil ændringerne ikke blive gemt permanent.

Arbejde med interne filer

Når du har brug for at gemme tekstdata, som er større end de enkle navn/værdi-par i SharedPreferences, kan du bruge interne filer. For at demonstrere, hvordan man læser og skriver tekstfiler, kan du følge disse trin:

  1. Opret en ny projekt i Android Studio, og kald det InternalStorageFile.

  2. I layoutfilen (main_activity.xml), tilføj et EditText og to knapper til at skrive og læse filer.

  3. Opret globale variabler i ActivityMain.java:

java
private final String FILENAME = "testfile.txt";
EditText mEditText;
  1. I onCreate()-metoden, referér til EditText:

java
mEditText = (EditText)findViewById(R.id.editText);
  1. Opret en metode til at skrive til en fil:

java
public void writeFile(View view) {
try { FileOutputStream fileOutputStream = openFileOutput(FILENAME, Context.MODE_PRIVATE); fileOutputStream.write(mEditText.getText().toString().getBytes()); fileOutputStream.close(); } catch (java.io.IOException e) { e.printStackTrace(); } }
  1. Opret en metode til at læse fra filen:

java
public void readFile(View view) {
StringBuilder stringBuilder = new StringBuilder();
try { InputStream inputStream = openFileInput(FILENAME); if (inputStream != null) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String newLine;
while ((newLine = bufferedReader.readLine()) != null) { stringBuilder.append(newLine + "\n"); } inputStream.close(); } } catch (java.io.IOException e) { e.printStackTrace(); } mEditText.setText(stringBuilder); }

Disse metoder gør det muligt at skrive tekst til en intern fil og læse fra den igen. Filen gemmes i applikationens private opbevaring, og kan ikke tilgås af andre applikationer. Hvis du vil finde filen, kan du bruge Android Device Monitor til at navigere til dens placering.

Ekstra oplysninger om cache og ekstern opbevaring

Hvis applikationen kun skal gemme midlertidige data, kan du også benytte cache-mappen. Denne mappes indhold kan blive ryddet af systemet, hvis lagerpladsen bliver lav, og den kan også blive tømt af brugeren i appens indstillinger. Du kan få adgang til cache-mappen ved at bruge metoden getCacheDir(). Det er en god praksis at slette forældede filer fra cache-mappen for at undgå, at appen bruger unødvendig plads.

På den anden side, hvis du har brug for at gemme data, som skal være tilgængelige uden for applikationen, kan du bruge ekstern opbevaring. At arbejde med ekstern opbevaring er meget lig arbejdet med intern opbevaring, bortset fra at du skal have tilladelser til at skrive til ekstern lager, og du skal bruge metoder som getExternalStorageDirectory().

Afsluttende bemærkninger

Det er vigtigt at vælge den rette opbevaringsmetode afhængig af applikationens behov. SharedPreferences er ideel til at gemme små, enkle indstillinger som brugerens præferencer, mens interne filer er mere passende til større mængder data. Hvis appen skal kunne dele data med andre apps eller give brugeren mulighed for at tilgå dataene direkte, bør ekstern opbevaring overvejes. Det er også vigtigt at forstå de potentielle konsekvenser af at arbejde med cache og ekstern opbevaring, især hvad angår systemets opbevaringsadministration.

Hvordan man anvender projektion og kameravisning i OpenGL ES for at tilpasse skærmorientering

Når man arbejder med OpenGL ES til at tegne grafiske objekter, såsom trekanter, på en enhedsskærm, kan man støde på problemer med skærmorienteringen. Som vi så i den tidligere opskrift, når vi tegner et objekt til skærmen, vil objektet blive skævt, afhængigt af enhedens orientering. Årsagen til dette er, at OpenGL standardmæssigt antager, at skærmen er perfekt firkantet. Dette er sjældent tilfældet med moderne enheder, der ofte har skærme med forskellige dimensioner og opløsninger.

I OpenGL er dette problem løst gennem en teknik kaldet projektion. Projektionen hjælper med at tilpasse de virtuelle koordinater, der anvendes i OpenGL, til de fysiske dimensioner af den enhed, hvorpå objektet vises. Denne proces kræver, at vi først beregner en projektionsmatrix, som omdanner skærmens koordinatsystem til et system, der passer til den specifikke enhedsformat og dimensioner.

I denne opskrift vil vi dykke ned i, hvordan man anvender projektion og kameravisning til at tilpasse OpenGL-objekter, så de bliver korrekt gengivet på en skærm, uanset om skærmen er i portræt- eller landskabsorientering.

For at kunne gennemføre dette, skal vi modificere den kode, vi tidligere har arbejdet med, hvor vi tegnede trekanter på en GLSurfaceView. Vi vil nu introducere projektionsmatrixen og kameravisningen, så de objekter, vi tegner, tilpasses korrekt til den fysiske skærm. Først og fremmest skal vi opdatere vores shaderkode, så vi kan anvende en matrix til at beregne positionen af hvert punkt på objektet.

I shaderkoden til vertexen skal vi tilføje en matrixvariabel. Denne matrix vil blive brugt til at beregne positionen af hvert punkt, så det passer til de korrekte skærmkoordinater. Dette kan gøres ved at ændre vertex-shaderkoden til at inkludere en uniform matrix, der anvender transformationsmatrixen, før positionen beregnes.

Når det er gjort, skal vi modificere draw()-metoden til at tage matrixen som parameter og anvende den før rendering. På denne måde vil vi sikre, at hvert punkt i objektet bliver korrekt transformeret, så det kan tegnes korrekt i forhold til den aktuelle skærmorientering.

Dernæst skal vi beregne en projektionsmatrix i onSurfaceChanged()-metoden. Denne metode bliver kaldt, når størrelsen på skærmen ændres, og her vil vi beregne forholdet mellem bredden og højden på skærmen. Dette forhold bruges til at opdatere projektionsmatrixen, så objekterne stadig passer til den fysiske skærm, selvom enhedens orientering ændrer sig.

For at afslutte processen skal vi beregne en kameravisning i onDrawFrame()-metoden. Dette gøres ved at bruge metoden Matrix.setLookAtM(), som definerer kameraets position og orientering i forhold til objektet. Når vi har beregnet kameravisningen, kan vi kombinere den med projektionsmatrixen ved hjælp af Matrix.multiplyMM() for at skabe den endelige MVP-matrix, der bruges til at tegne objektet korrekt på skærmen.

Ved at følge disse trin vil vi sikre, at objekterne tegnes korrekt uanset enhedens orientering. I den næste opskrift vil vi videreudvikle dette ved at introducere rotation, så objekterne kan bevæge sig rundt på skærmen.

Det er vigtigt at bemærke, at vi i OpenGL ES arbejder med et koordinatsystem, der standardmæssigt antager en firkantet skærm. Når vi tilpasser vores objekt til en enhed med et ikke-firkantet skærmformat, er det nødvendigt at forstå, hvordan matrixtransformeringer fungerer, og hvordan de påvirker den endelige rendering. Når vi justerer både projektionen og kameravisningen, skaber vi en mere fleksibel og korrekt gengivelse af objekterne i forskellige skærmorienteringer.

Når disse grundlæggende transformationer er på plads, kan vi begynde at arbejde med mere avancerede teknikker som rotation og interaktiv animation. Dette åbner op for endnu mere dynamiske og engagerende brugeroplevelser, som kan udnytte den fulde kraft af OpenGL til at skabe komplekse grafiske applikationer.

Endtext

Hvordan man undgår ANR-fejl ved brug af baggrundstråde i Android

At udføre langvarige operationer på hovedtråden kan få din applikation til at føles langsom eller, i værste fald, føre til, at den fryser. Hvis en applikation ikke reagerer indenfor cirka 5 sekunder, vil systemet sandsynligvis vise en "Application Not Responding" (ANR) dialog, som giver brugeren mulighed for at afslutte appen. Dette er noget, man absolut vil undgå, da det er en effektiv måde at få din app afinstalleret på.

Android-applikationer arbejder med en en-tråds model, der er styret af to enkle regler:

  1. Bloker ikke hovedtråden

  2. Udfør alle UI-operationer på hovedtråden

Når Android starter din applikation, opretter det automatisk hovedtråden (eller UI-tråden), som er den tråd, hvor alle UI-operationer skal udføres. Den første regel – “Bloker ikke hovedtråden” – betyder, at enhver langvarig eller potentielt blokerende opgave skal køres på en baggrundstråd. Det er derfor, alle netværksbaserede opgaver skal udføres udenfor hovedtråden.

Android tilbyder flere muligheder, når du arbejder med baggrundstråde:

  • Activity.runOnUiThread()

  • View.post()

  • View.postDelayed()

  • Handler

  • AsyncTask

Denne opskrift vil fokusere på klassen AsyncTask, som tidligere blev oprettet, så du ikke behøver at bruge Handler eller post-metoderne direkte.

I Android Studio skal du oprette et nyt projekt kaldet AsyncTask. Vælg standardindstillingen "Phone & Tablet" og vælg "Empty Activity" som aktivitetstype. Vi skal kun bruge én knap i dette eksempel, og så kan vi definere en simpel AsyncTask for at vise, hvordan man kører langvarige opgaver uden at blokere hovedtråden.

I activity_main.xml skal du erstatte den eksisterende TextView med følgende knap:

xml
<Button android:id="@+id/buttonStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start Counting" />

I MainActivity.java definerer du en global variabel:

java
Button mButtonStart;

Derpå opretter du AsyncTask-klassen som en intern klasse i MainActivity:

java
private class CountingTask extends AsyncTask<Integer, Void, Integer> { @Override protected Integer doInBackground(Integer... params) { int count = params[0];
for (int x = 0; x <= count; x++) {
// Simulering af en langvarig opgave } return count; } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mButtonStart.setEnabled(true); // Genaktiver knappen efter opgaven er afsluttet } }

I onCreate() initialiserer du knappen:

java
mButtonStart = (Button) findViewById(R.id.buttonStart);

Og tilføj en metode, der aktiverer AsyncTask, når knappen bliver trykket:

java
public void start(View view) { mButtonStart.setEnabled(false); // Deaktiver knappen under operationen
new CountingTask().execute(10000000); // Start AsyncTask med et stort tal
}

Dette er en meget simpel implementering af AsyncTask. Teknisk set er det kun doInBackground()-metoden, der er nødvendig, men normalt vil man også bruge onPostExecute() for at få notifikationer, når opgaven er afsluttet. AsyncTask arbejder ved at oprette en arbejdertråd for doInBackground()-metoden og svarer tilbage på UI-tråden i onPostExecute()-callbacken.

Vær opmærksom på, at vi ikke udfører nogen UI-operationer i baggrundstråden, fordi det vil føre til kompilationsfejl eller runtime undtagelser. Derfor venter vi til onPostExecute() bliver kaldt, før vi gør noget med UI, som at aktivere knappen igen.

En vigtig detalje er, at en AsyncTask kun kan udføres én gang. Hvis du forsøger at køre execute() igen på den samme AsyncTask, vil det kaste en undtagelse. Derfor skal du oprette et nyt AsyncTask-objekt hver gang knappen bliver trykket.

AsyncTask er meget fleksibel og kan bruges på en simpel måde som i eksemplet, men giver også mulighed for at arbejde med mere komplekse scenarier, f.eks. opdateringer af fremskridt eller håndtering af annullering af opgaver. For eksempel kan opdatering af brugergrænsefladen med fremskridt ske ved at bruge metoden onProgressUpdate() i AsyncTask, som reagerer på kaldet publishProgress() i baggrundstråden.

Når du arbejder med AsyncTask, er det dog vigtigt at forstå, hvad der sker, når aktiviteten, som AsyncTask kører på, bliver ødelagt og oprettet igen (f.eks. ved en skiftende skærmrotation). Hvis AsyncTask stadig kører, kan det føre til fejl, da det vil forsøge at interagere med en nu destrueret aktivitet. Derfor er det ofte en god idé at bruge AsyncTask med en Fragment, som ikke bliver ødelagt ved skærmrotation, eller overveje at bruge en Loader, som er mere robust i sådanne scenarier.

En yderligere overvejelse ved brug af AsyncTask er de tre generiske parametre, der anvendes ved deklarationen af AsyncTask-klassen:

  • Params: Den type parameter, der bruges til at kalde doInBackground()

  • Progress: Den type parameter, der bruges til at opdatere fremskridt

  • Result: Den type parameter, der bruges til at sende resultater tilbage

Derudover kan du annullere en opgave ved at kalde cancel(true) på AsyncTask-objektet. Dette vil få isCancelled() i doInBackground() til at returnere true, hvilket kan bruges til at afslutte en løkke, hvis det er nødvendigt.

Det er også vigtigt at forstå, at AsyncTask ikke er den eneste måde at køre baggrundsoperationer på Android. Der findes andre metoder som f.eks. Handler, Runnable, og ExecutorService, der kan være bedre til mere komplekse opgaver, især når du har brug for flere baggrundsoperationer eller mere kontrol over tråde.

Endtext