Mobile Programming PDF
Document Details
Uploaded by SkillfulPrehistoricArt9858
University of Salerno
Tags
Summary
This document provides an overview of mobile programming, focusing on layout design within Android applications. It discusses different layout types, including XML and programmatic approaches, and explains various view attributes and considerations for creating user interfaces. It also covers the use of different layout types, such as Linear, Relative, and Grid layouts, and how to use constraints in layout design.
Full Transcript
MOBILE PROGRAMMING Layouts I layout servono a definire l’aspetto grafico delle interfacce utente. Questo può essere fatto tramite due modi: Mediante file XML e/o in modo programmatico. XML: Viene redatto un foglio di layout xml che definisce l’aspetto di una interfaccia in mani...
MOBILE PROGRAMMING Layouts I layout servono a definire l’aspetto grafico delle interfacce utente. Questo può essere fatto tramite due modi: Mediante file XML e/o in modo programmatico. XML: Viene redatto un foglio di layout xml che definisce l’aspetto di una interfaccia in maniera statica con un elemento root e tutti gli elementi contenuti nella root. Programmatico: L’aspetto di una interfaccia viene definito durante la fase di esecuzione tramite righe di codice. XML Vantaggio: Il layout, essendo definito in un file xml a parte, non è accoppiato al codice della activity favorendo così eventuali modifiche future. Svantaggio: Non permette di mutare l’aspetto della interfaccia in relazione a interazione con l’utente. Programmatico Vantaggio: Permette di mutare l’aspetto di una interfaccia a tempo di esecuzione a favore dell’interattività con l’utente. Svantaggio: L’aspetto della interfaccia è accoppiato al codice Java che ne gestisce la logica incrementando potenzialmente la necessità di modifiche future. Si possono usare in sinergia. Vi sono casi però in cui conviene usare uno solo dei due metodi. Ad esempio se l’app prevede solo la visualizzazione di contenuti allora si può scegliere un approccio statico tramite xml poichè non va gestita alcuna interazione che possa mutare l’aspetto dell’interfaccia. Una View è un elemento grafico. Esso può essere un widget base come una TextView o un Button. In xml è possibile definire per ognuno di esso degli attributi. Essi hanno degli attributi comuni come layout_height e layout_width mentre altri sono specifici per la categoria Altri attributi sono id che consentono di identificare univocamente un elemento nell’interfaccia (android:id=”@+id/someid” creo un id che si chiama someid - android:id=”@id/someid” faccio riferimento a id someid) Gli attributi che modificano il layout iniziano con android:layout_qualcosa Gli elementi in una view possono essere posizionati rispetto al vertice sinistro superiore del parent che li contiene ed essere dimensionati con misure precise oppure in modo adattivo rispetto al parent (match_parent) o rispetto al contenuto (wrap_content). Ad ogni elemento è possibile aggiungere un margine o un padding. Inoltre è possibile allineare rispetto al parent (esempio: layout_alignParentTop che pone l’elemento in questione nella parte alta del parent che lo contiene). Nell’ambito dei layout è possibile fare riferimento a misure reali, in pixel (px), oppure convenzionali (dp) le quali servono a creare interfacce capaci di adattarsi a più schermi. La dimensione dello schermo è misurata in pollici (‘’) mentre la densità di pixel dello schermo è misurata in dpi (dot per inch) che indica quanti pixel ci sono in un’unità di area (in tal caso l’unità è il pollice). Abbiamo fasce di dpi: low, medium, high e extra high. I dp = density indipendent pixels è una misura convenzionale calcolata su una densità di 160 dpi. Ciò significa che un dp ha la dimensione di un pixel in uno schermo a 160 dpi. Android consente di fornire più versioni dello stesso drawable a diverse risoluzioni così che venga scelto quello più adatto in base ai dpi dello schermo. Abbiamo altre unità di misura oltre dp: sp: scale indipendent pixels (scalato in base alle preferenze visive dell’utente) pt (points ossia 1/72 di pollice), px (pixel), mm (millimetri), in (pollici) I ViewGroup sono elementi capaci di raggruppare elementi base o altri gruppi: In Android abbiamo i seguenti ViewGroup: Linear Layout Visualizzazione lineare degli elementi contenuti in esso. È possibile indicare se in orizzontale o verticale. Per ogni elemento nel linear layout è possibile stabilire un “peso” che determina quanto spazio occuperà in rapporto agli altri lungo la direzione (android:layout_weight). Relative Layout In questo GroupView la posizione degli elementi figli dipende dal layout del padre e dagli altri elementi del layout Alcuni esempi: android:layout_alignParentTop=”true” pone l’elemento nella parte alta del parent che lo contiene. android:layout_centerVertical="true" pone l’elemento centrato verticalmente rispetto al parent che lo contiene. android:layout_below=”@id/someid” pone l’elemento sotto l’elemento con id someid. android:layout_toRightOf=”@id/someid” pone l’elemento alla destra dell’elemento con id someid. Grid Layout Permette di disporre gli elementi in griglia, le cui righe e colonne sono esprimibili mediante attributi android:layout_columnCount=”n” e android:layout_rowCount=”m”. Se gli elementi non entrano nello spazio visibile disponibile è possibile effettuare lo scroll. Gli elementi di un GridLayout possono essere collocato in un indice di riga e colonna specifico mediante gli attributi android:layout_row=”m” android:layout_column=”n” Constraint Layout Come relative Layout permettendo di creare dei vincoli di posizionamento tra gli elementi senza creare un nesting troppo profondo che renderebbe la gestione più complessa (da usare in combo con l’editor di layout di android studio) FrameLayout Utile quando si vogliono sovrapporre elementi ad altri elementi o gruppi di elementi. Esse vengono posizionate in modo assoluto (origine angolo sinistro in alto) e stabiliscono un ordine di sovrapposizione tra gli elementi del layout frame secondo una logica fifo (stack). ListView Esso è un widget che consente di visualizzare le liste di oggetti. Esso divide l’area messa a disposizione in parti uguali per visualizzare gli oggetti ma se l’area di visualizzazione fosse troppo piccola è possibile effettuare uno scroll verticale. Un adapter consente di accoppiare i dati da visualizzare nella lista con la lista stessa e di associare ai singoli elementi il layout di visualizzazione definito per loro. Passi da seguire per creare una ListView (semplice) 1. Definire array con elementi String [] dati = {“Michele”, “Arianna”, “Ginetto”, “Pippo”}; 2. Definire un adapter ArrayAdapter adapter = new ArrayAdapter(this, R.layout.list_element_layout, R.id.textViewList, dati); 3. Individuare il widget ListView nel layout ListView listView = findViewById(R.id.myListView); 4. Associare l’adapter alla ListView listView.setAdapter(adapter) 5. Definire un listener per il click sull’elemento della ListView //dichiare come attributo della classe e non come oggetto locale listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { //Permette di ottenere il contenuto (sotto forma di stringa) dell’item cliccato String s = listView.getitemAtPosition(position).toString();... }) Questo era un esempio di ListView semplice con stringhe. È possibile definire Adapter Custom che consente di creare ListView i cui oggetti sono personalizzati con un layout più complesso (rubrica con contatti con foto, nome e numero). Il click rimane sempre su tutto l’item ma è possibile particolarizzare il click sul singolo elemento del listItem della listView mediante l’uso di setTag(position) e getTag(position). Ciclo di vita Vediamo il ciclo di vita per esteso con tanto di situazioni che determinano il passaggio da uno stato all’altro: Quando si preme su Home mentre si è su una activity vengono chiamate onPause() e poi onStop() Quando viene riportata l’activity in foreground vengono chiamate onRestart(), on Start() e onResume(). Quando invece si ruota il telefono l’activity viene distrutta e ricreata (perchè permette ad Android di applicare meglio il cambio di configurazione) per cui alla rotazione vengono chiamate onPause(), onStop() e on Destroy() e poi onCreate(), onStart() e onResume() Per cui, poiché l’activity viene distrutta e ricreata, si perde lo stato dell'activity! Per non perdere lo stato si fa l’override del metodo seguente: @Override public void onSaveInstanceState (Bundle savedInstanceState) { savedInstanceState.putStringArrayList(“listaStringhe”, listStrings); savedInstanceState.putInt(“counter”, counter); super.onSaveInstanceState(savedInstanceState); } dove la variabile savedInstanceState viene usata per salvare lo stato delle variabili di cui ci interessa salvare lo stato in caso di struzione della activity (quindi anche rotazione schermo) Sarà poi possibile recuperare le variabili salvate in onCreate(Bundle savedInstanceState) @Override public void onCreate (Bundle savedInstanceState) { if(savedInstanceState != null){ listString = savedInstanceState.getStringArrayList(“listaStringhe”); counter = savedInstanceState.getInt(“counter”); } } Per salvare lo stato è possibile usare degli oggetti persistenti ViewModel che permettono di fornire i dati alla nuova istanza dell’activity quando viene ricreata. I cambi di configurazione possono essere gestiti in modo personalizzando definendo un parametro dell’Android Manifest.xml Definendo un valore di questo parametro anziché distruggere l’app quando abbiamo un cambio di configurazione, viene eseguito il metodo onConfigurationChanged() L'oggetto configuration assume dei valori interi a seconda della configuration (Le Macro dell’esempio sopra sono associate a tali interi). Backstack Un’app è composta, solitamente, da più di una activity. Una activity può lanciare un'altra activity (anche di un’altra app). (Ciò è permesso dalla classe Intent che consente di lanciare una nuova attività passando ad essa dei dati - prossimo capitolo -). Una Task invece è un insieme di attività con cui l’utente interagisce. Le attività vengono organizzate in un backstack. Un task si innesca con il lancio dell’app dalla home screen e questa viene portata in foreground. Quando viene lanciata una nuova activity, quella presente passa in background e viene posta in cima al backstack mentre questa appena invocata passa in foreground. Per ritornare all’activity precedente si preme sul tasto back. Si possono avere anche più istanze della stessa activity. Facciamo un esempio con 3 activity: A, B e C (da ognuno di queste è possibile chiamare le altre 2) Chiamo A A in foreground A Chiamo B B in foreground B A Faccio back A in foreground A Chiamo C C in foreground C A Intent Un’Intent è una descrizione astratta di un’operazione da svolgere. Permette di lanciare una nuova activity con startActivity, spedire una intent in broadcast con broadcast Intent e ricevuta da un broadcastReceiver a cui è interessato e di comunicare un con servizio in background mediante startService e bindService. Un oggetto Intent è dotato di Action ossia azione da svolgere e Data ossia i dati su cui lavorare espressi come URI (indirizzo web, email o numero di telefono). Altre parti di un’intent sono: Category: informazioni aggiuntive sull’azione da eseguire Type: Esplicitare il tipo di dati (se non viene definito type il tipo viene rilevato in automatico) Component: Esplicita l’activity da eseguire (se non definito viene rilevato in base ad altre informazioni). Extras: bundle di informazioni specifiche per l’activity. Un intent può essere implicito o esplicito. Un intent esplicito permette di specificare in modo esplicito quale activity (component) deve lanciare l’intent e su quali dati addizionali lavorare (con extras). Un intent implicito invece non prevede di esplicitare il component (l'activity da lanciare viene dedotta) sulla base dei valori di Action, Type, Uri e Category. Ogni activity dichiara nel Manifesto dell’app quali action possono soddisfare! Vediamo come usare un intent. Ci troviamo nella activity che vuole lanciare una nuova activity Intent i; i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); startActivityForResult(i, REQUEST_CODE); In questo esempio l’Action è ACTION_PICK che serve a selezionare un item e Data è espresso sotto forma di URI startActivityForResult serve a lanciare l’activity che serve a gestire l’intent i che abbiamo definito e si aspetta la restituzione di un risultato. REQUEST_CODE serve a identificare il tipo di richiesta che facciamo alla activity che stiamo lanciando. @Override protected void onActivityResult(int request, int result, Intent data){ if (request == REQUEST_CODE && result == Activity.RESULT_OK){ //fai qualcosa con i dati restituiti mediante data } } Questo metodo viene sempre definito nella activity che lancia l’activity e serve a gestire il risultato della activity lanciata da essa. Il controllo su result ci serve a capire l’esito della chiamata alla activity. Extra, come anticipato, consente di inviare dati specifici in bundle chiave - valore alle activity mediante l’intent. Per settare dati intent.putExtra(“DIEGO”, i); intent.putExtra(“A”, armando) intent.putExtras(bundle); //inserisce un insieme di dati tramite un oggetto bundle Per prelevare dati int c = intent.getExtra(“DIEGO”); String s = intent.getExtra(“DIEGO”); Bundle bundle = intent.getExtras(); //preleva tutto il bundle (tutto l’insieme di extra) Gli intent flags consentono di settare informazioni sulla gestione dell’intent Alcuni esempi: FLAG_ACTIVITY_NO_HISTORY: non memorizza l’activity nel backstack FLAG_DEBUG_LOG_RESOLUTION: stampa info aggiuntive quando viene eseguito l’intent. Utile in fase di debug quando l’intent non parte. Gli intent component specificano l’activity target ed è usata quando l’intent deve rivolgersi ad uno specifico component (activity). Intent i = new Intent (Contect context, Class class); //oppure intent.setComponent(...); intent.setClass(...); intent.setClassName(...); Ovviamente un’app che utilizza intent espliciti potrebbe riferire a servizi esterni per cui necessita di permessi! Permessi I permessi sono un meccanismo di protezione che consente di proteggere dati e risorse dall’accesso di app. Le app potranno accedere a dati e risorse solo se l’utente avrà esplicitamente dato il permesso all’app di farlo. I permessi possono limitare l’accesso a dati personali (accesso a rubrica), servizi a consumo (invio di un SMS) e risorse di sistema come GPS o Fotocamera. Ogni app deve dichiarare nel proprio AndroidManifest.XML i permessi che intende chiedere all’utente e sono rappresentati da stringhe (android.permission.CAMERA android.permission.SEND_SMS) Vediamo degli esempi nel Manifesto I permessi possono essere normali o pericolosi. Permessi normali: vengono concessi senza chiedere all’utente poiché non sono legati all’accesso di informazioni sensibili per l’utente come utilizzo di Wi-Fi o Bluetooth Permessi pericolosi: devono essere approvati dall’utente poiché richiedono di accedere a risorse e dati sensibili per l’utente. Fino alla API 22 i permessi venivano richiesti durante l’installazione. Dalla API 23 in poi vengono chiesti a runtime quando servono. L’utente può decidere di cambiare le sue preferenze sui permessi accettando permessi che non aveva accetto prima e negando permessi che in precedenza aveva accettato per cui il programmatore deve sempre controllare lo stato del permesso sulla risorsa mediante Context.Compat.checkSelfPermission(...). Android raggruppa più permessi della stessa categoria in gruppi ed è possibile chiedere all’utente mediante una dialog box se intende consentire i permessi per l’intero gruppo. Se si è concesso il permesso sul gruppo allora si accettano i permessi a tutti gli elementi del gruppo (Il gruppo STORAGE ha un permesso per la lettura e uno per la scrittura). Threads I thread consentono di parallelizzare internamente l’esecuzione di un processo. I thread hanno un loro stack delle chiamate a funzione e un proprio program counter ma condividono con gli altri threads l’heap e la memoria statica. I thread in java si possono implementare con la classe thread oppure mediante l’interfaccia Runnable. Il loro comportamento è definito nel metodo public void run() e si rende pronto all’esecuzione mediante il metodo start(). Vi sono altri metodo come start(), sleep(time), wait() e notify(). Gli ultimi due servono rispettivamente per mettere in pausa e risvegliare un thread (notify deve essere invocato sul thread in wait da un altro thread che è in esecuzione) In Android i thread in background non possono interagire con l’interfaccia utente ma solo il main thread mediante i metodi View.post(Runnable action) e void Activity.runOnUiThread(Runnable Action). Introduciamo gli Async task che consentono di facilitare l’interazione tra thread in background e main thread che gestisce l’interfaccia mediante il seguente schema: Il thread in background esegue il task e notifica il main thread sullo stato di avanzamento il main thread invece effettua il setup iniziale, mostra l’avanzamento del thread in bg e usa i risultati (mostrandoli a schermo ad esempio) Vediamo la classe (generica poichè i valori di params, progress e result possono essere di tipi differenti a seconda del bg task) Async task { } Params: indica il tipo di dati su cui lavora il task in background Progress: tipo di dati usati per lo stato di avanzamento Result: tipo di dati per il risultato del task Vediamo adesso i metodi associati a questo parametri di classe void onPreExcute() viene eseguito dal thread main prima di doInBackground() Result doInBackground(Params…params) esegue il lavoro in background effettivo. Prende in input un numero variabile di parametri e restituisce un oggetto di tipo Result. Esso può chiamare publishProgress() per fornire al main thread i progressi (anche parziali) void onProgressUpdate (Progress…values) viene eseguito nel main thread in risposta a publishProgress void on PostExcute(Result result) viene eseguito dopo doInBackground e prende come risultato il Result fornito da doInBackgorund() Fragments Il frammento è una porzione di interfaccia. Un’ activity può ospitare uno o più frammenti che possono essere mostrati o rimossi. Un frammento può essere visto come una sub-activity con il suo ciclo di vita che è legato all’activity di cui fa parte. Se un’activity è su paused allora lo sono tutti i frammenti. Se l’activity è su resumed si possono gestire i frammenti che ospita. Nel layout va specificato come i fragment occupano l’interfaccia utente. I fragment consentono di creare interfacce utente dinamiche anche in funzione della grandezza dello schermo (mostrare un frammento se lo schermo è grande o nasconderlo se lo schermo è piccolo). Ciclo di vita di un fragment I fragment sono molto simili alle activity. Per poterle usare dobbiamo implementare i seguenti metodi: onCreate() dove la inizializziamo (come una activity) ma non definiamo il layout onCreateview() dove dopo aver definito il layout, lo associamo a una view tramite inflate. La view verrà poi restituita dal metodo. onPause() viene chiamato appena il fragment viene eliminato. Qui è opportuno rendere permanenti dei cambiamenti altrimenti andrebbero persi (usare onSaveInstanceState()). Creare un frammento public static class Frag extends Fragment { @Override public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { //associamo il layout alla view v View v = inflater.inflate(R.layout.esempio_layout, container, false); return v; } } Nella activity host si usa setContentView come equivalente a questo metodo. Container è un ViewGroup che specifica i parametri di layout. I frammenti possono essere inseriti staticamente tramite un file di layout.xml android:orientation=”horizontal” android:layout_width=”match_parent” android:layout_height=”match_parent”> I frammenti possono anche essere definiti in modo dinamico a runtime. FragmentTransaction ft = fm.beginTransaction(); FragmentManager fm = getFragmentManager(); Frag frag = new Frag(); ft.add(R.id.fragment_container, frag); //è anche possibile togliere o sostituire un fragment ft.commit(); R.id.fragment_container è un ViewGroup del layout dell’activity e serve a definire in quale parte dello schermo va posto il fragment addToBackStack() permette di aggiungere un fragment al backstack (non viene fatto in automatico come per le activity per cui cambiando activity e poi facendo back all’activity con il fragment avremmo perso le modifiche di aspetto fatte durante l’utilizzo precedente) Vediamo dei meccanismi di comunicazione tra activities. Si può fare creando dei metodi di callback. Un frammento può definire una sua interfaccia. A volte però, se abbiamo più di un fragment, e questi devono comunicare, non conviene che questi lo facciano direttamente poiché creerebbe accoppiamento che renderebbe più difficile apportare modifiche in futuro. Per questo utilizziamo la main activity per gestire la comunicazione tra due frammenti. Questo è possibile tramite un Comunicator che tiene il riferimento della main activity nel frammentoA. A questo punto il frammentoA indica l’indice selezionato tramite il metodo respond. la main activity in base al risultato del metodo respond gestirà il frammentoB a seconda delle condizioni. Data Storage Abbiamo 4 modi per salvare i dati in modo persistente: Classe SharedPreferences Permette di salvare e recuperare dati usando coppie chiave-valore. Usa due metodi della classe Activity: getSharedPreferences(“filename”) per recuperare più file di “preferenze” (dati) getDefaultSharedPreferences() quando basta un solo file getPreferences() recupera preferenze non condivise con altre apps SharedPreferences sp; Boolean b = sp.getBoolean(“chiave”); Per scrivere dati in uno SharedPreferences serve un oggetto di classe Editor SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(“chiave”, true); editor.commit(); //serve a effettuare la modifica del file associato all’oggetto sp File Ogni app ha una sua cartella privata che viene creata quando installata e eliminata quando disinstallata a cui solo l’app può accedere. Per usare i file si usano le librerie Java classiche Per scrivere su file FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE); //esiste MODE_APPEND String s = “stringadascrivere”; fos.write(s.getBytes()); close(): Per leggere openFileInput(fileName); read(); close(); Altri metodi utili getFilesDir() restituisce la directory privata dell’app getDir() crea o apre se esiste una directory all’interno dello spazio privato dell’app deleteFile() cancella il file nello spazio privato filelist() restituisce un array con i file presenti nello spazio privato Android consente di gestire file temporanei mediante una cache. Android cancella tali file se necessario (poco spazio disponibile) getCacheDir() restituisce la cartella di cache. L’app si occupa di eliminare i file i quali non dovrebbero superare 1MB. Se un file è su memoria esterna, prima di usarlo, bisogna controllare se l’utente ha dato i permessi per accedere alla memoria secondaria (Prima di Android 4.4 i permessi servivano anche per accedere alla memoria privata). Inoltre la SD essendo rimovibile, non sempre rende i file disponibili. Quindi aggiungiamo i permessi al manifesto e poi controlliamo da codice se abbiamo i permessi di scrittura e lettura (il permesso di scrittura ingloba quello di lettura). Per condividere file con altre app si può usare il metodo getExternalStoragePublicDirectory(type) dove type indica la cartella pubblica Questo metodo serve a creare una cartella nella cartella pubblica delle immagini. SQLite Android fornisce uno strumento per usare SQL per i database all’interno dell’app ossia SQLite.. Per usare un db bisogna creare una sottoclasse di SQLiteOpenHelper e sovrascrivere onCreate(). Successivamente si crea un nuovo Helper da cui si ricava il db Il db viene creato nel metodo onCreate del DatabaseOpenHelper si possono usare dei metodi integrati per inserire, modificare e cancellare una riga del db db.insert() db.delete() db.update() Per usare questi metodi serve scrivere i record del db come oggetti di classe ContentValues. db.insert(nomeTavola, null, oggettoContentValues) //il secondo attributo serve a dire cosa fare se l’oggetto ContentValues è nullo. null significa che non deve far nulla, nome_colonna invece serve a inserire un valore nullo per quella colonna db.query() esegue una query SQL sul database Cursor è una classe che contiene i risultati della query. Questi sono consultabili mediante alcuni metodi messi a disposizione da Cursor. cursor.moveToFirst() cursor.moveToNext() cursor.getInt(columnIndex) cursor.getColumnIndex(columnName) Pro & contro nell’uso delle 3 tipologie di gestione dei dati persistenti. SharedPreferences Vantaggio: Android consente di usarli in modo semplice mediante i metodi getSharedPreferences(“filename”) e getDefualtSharedPreferences() senza particolari usi di altre classi risultando veloci e efficienti nell’uso e nelle prestazioni.. Svantaggio: le preferenze devono stare nello spazio provato delle apps e inoltre sono di piccole dimensioni per cui non è possibile salvare grandi quantità di dati. Inoltre la struttura chiave - valore non permette di salvare dati complessi ma solo di tipo primitivo. File Vantaggio: I file consentono di salvare anche oggetti complessi e definiti dall’utente e consentono di salvare una mole di dati più elevata usando le classiche librerie di Java (complice anche la possibilità di salvarli su memoria esterna). Svantaggio: C’è bisogno di controllare i permessi di accesso, gestire il parsing dei dati e comunque non adatti per basi di dati complesse SQLite Vantaggio: Consente di usare database relazionali per la gestione di basi di dati molto complesse o più strutturate o per moli di dati molto elevate Svantaggio: SQLite, seppur utilizzabile in modo semplificato su Android, resta lento rispetto ad altri metodi e complesso da utilizzare. Grafica Un’immagine può essere disegnata in un oggetto View (grafica semplice che non cambia) o in un oggetto Canvas che presenta una grafica complessa soggetta a aggiornamenti frequenti. La classe Drawable rappresenta un oggetto che può essere disegnato. ShapeDrawable per forme, BitmapDrawable per matrici di pixel e ColorDrawable per colori uniformi. Un oggetto Drawabke può essere inserito in una View tramite layout.xml o in modo programmatico con View.setImageDrawable(). Android consente di applicare animazioni alle immagini mediante un file XML. Le animazioni possono essere di rotazione, traslazione, ridimensionamento, trasparenza ecc… Il file XML delle animazione viene poi usato dalla Class Animation che le applica alle ImageView. Android, come abbiamo visto, mette a disposizione moltissimi widgets. Però è possibile creare dei Custom Widgets che rispondono ad esigenze ad hoc! I Custom Widgets permettono di creare widgets complessi. Per i programmatori però diventano più difficili da usare rispetto ai Widgets base di Android. Come funziona il meccanismo di layout delle View. Come sappiamo i widgets sono organizzati in un albero che ha una radice, solitamente un GroupView, e tutti i Widgets che lo contengono. Il layout viene poi realizzato in tre fasi: Misura: Viene eseguita la funzione onMeasure() in cui l'elemento riceve le constraints dalla sua Parent e misura la sua larghezza e altezza con i dati che gli vengono restituiti dalle onMeasure dei figli (se esistono). Posizionamento: tramite la funzione onLayout() la Parent dimensiona e posiziona la figlia. Disegno: Tramite onDraw() la Parent disegna e dice alle figlie di disegnare Si nota che l’approccio di disegno delle view è di tipo top-down. La view Parent chiede alle view figlie le loro dimensioni. Le figlie comunicano alla parent le dimensioni che desiderano per loro con la classe LayoutParams e la View Padre, tramite la classe MeasureSpec, comunica alle figlie le dimensioni effettive. Facciamo quindi distinzione tra measured size (quella desiderata dalla figlia) e quella reale (quella che la view padre assegna alle figlie). In caso di cambiamenti della View dopo la fase di disegno (eseguita da onDraw()) è possibile chiamare di nuovo onDraw() sulla view con invalidate(). Se si vuole ripetere l’intero processo di misurazione e posizionamento su tutto l’albero delle view previsto dal layout è possibile usare requestLayout(). Quando viene invocato requestLayout() la view container (Linear, Relative…) può interagire con le figlie. onMeasure() comunica le dimensioni desiderate dalle figlie, in onLayout() il container assegna le misure effettive alle figlie e quindi l’effettivo posizionamento e in onDraw() vengono effettivamente disegnate avvalendosi di oggetti di classe Paint o Canvas. Se volessi creare una classe CustomWIdget che estende Widget per creare un widget custom dovremmo poi: definire gli attributi del widget (altezza, larghezza, colore) creare un costruttore che inizializzi gli attributi del Custom widget fare override del metodo onDraw() per disegnare il widget assegnando i valori custom di altezza, larghezza e colore servendoci dell’oggetto Paint per il colore e dell’oggetto canvas (preso come parametro da onDraw() per disegnare figure come rettangoli) Android consente di intercettare il multitouch dello schermo da più dita. Ciò è possibile tramite la classe MotionEvent che permette di catturare movimenti da un periferica che non sia necessariamente il tocco dello schermo touch. Il movimento è rappresentato da ACTION_CODE che registra un cambiamento e ACTION_VALUES che registra i valori del tocco come pressione, posizione, tempo ecc… Un Pointer è un singolo evento. Esso è associato a un MotionEvent (a volte più di uno. L’accesso al singolo poi è garantito da un indice per il MotionEvent che differisce dal Pointer ID che è costante per tutto l’uso del pointer). GLi ACTION_CODES più celebri sono: ACTION_DOWN: dito tocca lo schermo ed è il primo ACTION_POINTER_DOWN: dito tocca lo schermo ma non è il primo ACTION_MOVE: dito si muove su schermo ACTION_POINTER_UP: dito sullo schermo smette di toccarlo ACTION_UP: l’ultimo dito sullo schermo viene alzato Esistono tante fantastiche funzioni per gestire i MotionEvent. Le View possono essere notificate dell’avvento di un evento associato a un MotionEvent tramite View.onTouchEvent(MotionEvent e) il quale restituisce true o false a seconda di se l’evento si è consumato o meno. View.onTouchListener() e View.setOnTouchListener() invece consente a un oggetto di ricevere la notifica. onTouch invece viene invocata quando si verifica un evento prima che una View possa essere notificata. La classe GestureDetector riconosce dei gesti fatti a schermo e permette anche di importare gesti personalizzati fatti a schermo. ViewAnimator e ViewFlipper servono a creare delle animazioni per passare da un frame a un altro mediante showNext() e showPrevious(). Sensori Uno smartphone è dotato di diversi sensori: movimento (rotazione e accelerazione) ambiente (temperatura, umidità) posizione (giroscopio, bussola) Forniscono dati grezzi da interpretare (la precisione è intrinseca alla qualità del sensore) Android usa il SensorManager per capire i sensori disponibili e le loro caratteristiche. Ci permette di leggere i dati dei sensori e settare dei Listener in vista di cambiamenti di dati dei sensori. Per usare un sensore bisogna implementare l’interfaccia SensorEventListener e poi verificare che il sensore esista. Vi sono varie velocità di campionamento dei sensori: SENSOR_DELAY_NORMAL che offre una velocità di campionamento standard che non stressa troppo processore e batteria cercando di offrire un comporomesso tra performance e consumo energetico SENSOR_DELAY_GAME: Offre una velocità di campionamento elevatissima ed è usata per applicazioni che necessitano di rilevare costantemente i valori dei sensori come giochi o app complesse. SENSOR_DELAY_UI: Offre una velocità di campionamento compresa tra GAME e NORMAL ed è utile per app che necessitano di una velocità di campionamento elevata ma non estrema SENSOR_DELAY_FASTEST è la modalità di campionamento più veloce che ha come limite il limite fisico del sensore sfruttandolo al massimo della sua prestazione. Consumo energetico elevatissimo Per ottimizzare l’utilizzo della batteria è utile prendere possesso di un sensore in onResume() e rilasciarlo in onPause così che l’activity non usi i sensori quando non gli servono. Notifiche Le notifiche servono a dare info aggiuntive all’utente fuori dalla grafica dell’activity. Si possono manifestare mediante i Toast (sovraimpressione temporanea), Dialog (Box di testo con cui l’utente può interagire), Notifiche (Visibili nel cassetto delle notifiche o nella status bar). Con Android 5 sono state introdotte le notifiche a comparsa per le notifiche urgenti. Da Android 8 invece vi sono i canali di notifiche (Prima di questo ogni notifica aveva un proprio canale) e se non assegnata a un canale non viene mostrata. I canali permettono di gestire i comportamenti delle notifiche (suoni, disattivare alcuni canali di notifiche). Le classi più celebri sono NotificationCompat e NotificationManagerCompat per ottenere compatibilità con Android che si evolve. Alarms Gli Alarms consentono di eseguire intent in funzione del tempo (programmare l’esecuzione di un dato intent una volta al giorno o ogni ogni ecc). Un’app che usa un alarm riesce a eseguire codice anche se è terminata cioè un alarm consente di attivare un’app. Un alarm è attivo anche se il telefono va in sleep infatti può causare la ripresa delle attività o essere gestito quando il telefono ritorna in modalità normale. Gli alarms vengono disattivati quando si spegne il device o se vengono cancellati. Abbiamo alarms imprecisi vengono attivati entro un intervallo dal tempo esatto per minimizzare l’uso della batteria. Quelli esatti invece si attivano al tempo stabilito. Rendere un alarm esatto o inesatto va espresso nel manifest nella sezione dei permessi. Per usarli si usa la classe AlarmManager. Content Providers, Broadcast, Services Android ha 4 componenti fondamentali: Activity Broadcasts Content Providers Services Finora abbiamo visto le Activity che permettono lo sviluppo di app. Content Providers, Broadcasts e Services sono di ausilio alle app. Il broadcast di messaggi può essere globale (possono essere ricevuti da tutti) o locale (ricevuti solo all’interno dell’app). BroadcastSender genera un messaggio in broadcast. Broadcastreceiver riceve un messaggio. Intent serve a contenere il messaggio. Il messaggio è l’intent. Broadcast di sistema ossia i messaggi generati dal sistema operativo (esempio: android.intent.action.AIRPLANE_MODE). BroadcastReceiver statico va registrato nel manifesto mediante un intent filter. android:exported è obbligatorio (se true il receiver può essere usato da tutte le app, se false solo dai component della stessa app) I BroadcastReceiver registrati dinamicamente vanno programmati ereditado la classe BroadcastReceiver e facendo override del metodo public void onReceive(Context context, Intent intent) I receivers che non servono vanno eliminati mediante unregisterReceiver(bcastReceiver) in modo intelligente tipo dopo averlo registrato in onCreate() della activity e eliminato in onDestroy() dell’activity I LocalBroadcasts sono deprecati. Essi usano un sistema di registrazione mediante registerReceiver() mediante LocalBroadcastManager() o nel Context() per le global. I receiver statici vengono registrati nel boot dell’app mentre quelli dinamici quando viene invocato il registerReceiver (su un Local o Context se local a un receiver locale o globale al sistema). Per spedire un messaggio si usa sendBroadcast(Intent i) LocalBroadCastManager.sendBroadcast(intent i) per un invio locale Context.sendBroadcast(intent i) per un invio globale. sendBroadcast(Intent i, String permission) la stringa serve a definire che il messaggio deve essere spedito solo a chi ha il permesso indicato nella stringa permission. I ContentProvider sono interfacce per accedere a un database L’accesso ai db può essere fatto mediante ContentResolver con linguaggio SQL-like e notifica se cambiano i dati. Un db può essere acceduto da più content provider mediante URI content://authority/path/id (se l’id è omesso accedere a tutta la tabella) Si possono creare dei CustomContentProvider ereditando la classe ContentProvider che risponde a una URI specifica che va definita del Manifest dell’app, Egli espone un metodo Query che restituisce un Cursor contenente i dati richiesti. A partire da API 30, mediante va detto nel manifest a quale content provider si vuole accedere I Services eseguono operazioni complesse che richiedono molto tempo per essere portate a termine (come un download da un sito). La classe Service serve a gestire nuovi services o usarne di esistenti. I Servizi non hanno una UI e operano solo in background e interagiscono direttamente con le app. Un service una volta partito può continuare fino a che la device è acceso , auto completarsi o terminare per cedere risorse necessarie. Essi sono eseguiti nel main Thread dell’app di default ma possono essere fatti in un thread a parte se espresso esplicitamente. I service possono essere unbound (usano i metodi startservice e stopService per far partire e fermare il servizio) oppure bound cioè collegati a un component di una app (tramite metodo bindService). che diventa client del service. Il bound service è eseguito fino a che ci sono client agganciati. Un service può essere sia bound che unbound. Dipende da se lo si fa partire rispettivamente con onStartCommand() oppure onBind(). I Services vanno dichiarati nel Manifest