Autore Topic: [facile]Posizionare elementi dinamici in una ImageView  (Letto 3684 volte)

Offline undead

  • Utente senior
  • ****
  • Post: 666
  • Respect: +113
    • Mostra profilo
  • Dispositivo Android:
    Samsung Galaxy S6
  • Play Store ID:
    DrKappa
  • Sistema operativo:
    Windows 10 64-bit, Windows 8.1 64-bit
[facile]Posizionare elementi dinamici in una ImageView
« il: 12 Aprile 2013, 11:36:14 CEST »
+1
Livello di difficoltà: facile
Target SDK: 15
Min SDK: 10
Link al file compresso del progetto eclipse: http://www.drkappa.net/tutorialsandroid/LedTutorial-01.zip[/url][/b]

Può capitare di dover gestire componenti multipli e ci si ritrova spesso come unico riferimento la pagina http://developer.android.com/guide/practices/screens_support.html. Benchè tale guida sia utile (anzi sia di fatto un prerequisito di questo tutorial) essa non risolve ne affronta problemi più complessi e situazioni particolari che purtroppo a volte dovrete affrontare.

Questo tutorial spiega come creare una ImageView custom e come andare a posizionare al suo interno degli elementi dinamici che si integrino alla perfezione indipendentemente dalla risoluzione del device.

IL PROBLEMA
Vogliamo disegnare dei led che si accendano uno dietro l'altro ad intervalli regolari. Questi led devono apparire sopra ad una immagine di sfondo.

La prima soluzione che ci viene in mente è quella di creare N immagini, una per ogni combinazione, e cambiarle al volo.
Gli svantaggi di questa soluzione sono:
1- grosso quantitativo di memoria utilizzata (risoluzione immagine base moltiplicato per il numero di combinazioni)
2- impossibilità pratica nel gestire il tutto da codice quando le combinazioni diventano troppe

La seconda soluzione, che risolve entrambi i problemi, è quella di separare lo sfondo e creare delle piccole imageview che possiamo attivare/disattivare. Purtroppo le imageview debbono comunque sottostare alle regole di posizionamento tipiche di android per cui possiamo trovarci in difficoltà qualora dovessimo disegnare un componente molto complesso.

CHE TIPO DI SOLUZIONE STIAMO CERCANDO?
Prima di perderci in nuove e stravaganti teorie, cerchiamo di stabilire cosa vorremmo ottenere.
In una situazione ideale noi vorremmo creare una immagine di sfondo con un programma di disegno qualsiasi, dopodichè aggiungere dei layer a tale immagine (i nostri componenti dinamici o led) e riprodurre nel modo più fedele possibile quello che abbiamo appena disegnato.
I vantaggi di questo sistema sarebbero ovvi:
1- risparmio di memoria perchè abbiamo separato lo sfondo dagli elementi dinamici
2- possibilità di gestire facilmente le varie combinazioni
3- posizionamento il più possibile preciso al pixel senza doversi preoccupare di posizioni relative, layout xml e così via.

LA SOLUZIONE PROPOSTA
La prima cosa da fare è decidere come si deve comportare il contenitore (cioè la imageview che visualizza lo sfondo). Per fare questo si opera esattamente come per una qualsiasi applicazione.

Nel caso di questo tutorial ho scelto il posizionamento più complicato, cioè una imageview che mantenga l'aspect ratio dell'immagine con attributo fill_parent con una immagine di sfondo quadrata.

E' una situazione complicata perchè l'area dell'imageview copre tutto lo schermo ma lo sfondo è quadrato. Questo significa che la risoluzione del canvas della imageview copre tutta l'area visibile e centra l'immagine allineandola al primo bordo (verticale od orizzontale) che incontra, lasciando spazio vuoto all'esterno. Ho anche scelto una immagine di dimensioni basse (256x256) in modo da poter vedere visivamente come rende su un device con risoluzione più grande.

Per esempio in un device con risoluzione 480x800 l'immagine viene scalata a 480x480 e ci sono bordi (verticali o orizzontali a seconda dell'orientamento).

Il led ha risoluzione 32x32 ed il posizionamento è stato fatto in modo arbitrario.
Ci sono 5 led sull'asse x spaziati da 16 pixel con 16 pixel di bordo. Quindi le posizioni x sono:
16 64 112 160 208
Sull'asse y la coordinata è 71, per dimostrare che il posizionamento è appunto arbitrario.

PASSO 1: IMAGEVIEW CUSTOM
Il primo passo è quello di creare una imageview custom.
Si dichiara il tutto nell'xml dell'activity (in questo caso come ho detto il layout è fill_parent):

Codice (XML): [Seleziona]
<net.drkappa.tut.ledtutorial.LedBaseImageView
       android:id="@+id/LedBase"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:adjustViewBounds="true"
       android:src="@drawable/base" />

Andiamo quindi a creare la classe LedBaseImageView:

Codice (Java): [Seleziona]
public class LedBaseImageView extends ImageView {
                public LedBaseImageView(Context context, AttributeSet attrs) {
                super(context, attrs);
               
        }
}

Nel costruttore della nostra activity ci prendiamo un riferimento alla classe custom:

Codice (Java): [Seleziona]
setContentView(R.layout.activity_led);
m_LedImageView = (LedBaseImageView) findViewById(R.id.LedBase);


PASSO 2: CARICAMENTO LED E PREPARAZIONE THREAD
Estendiamo quindi la nostra classe custom in modo da caricare il led attraverso una funzione apposita. Dichiariamo due membri, uno che ci dice il numero del led acceso e uno che è la bitmap del led.

Codice (Java): [Seleziona]
protected int m_nLed = -1;
protected Bitmap m_LedImage = null;

Creiamo una funzione per settare il led e una funzione init per caricare l'immagine:

Codice (Java): [Seleziona]
public void init(){
                m_nLed = -1;
                BitmapFactory.Options opt = new BitmapFactory.Options();
                opt.inScaled = false;
                opt.inPreferredConfig = Bitmap.Config.ARGB_8888;
                m_LedImage = BitmapFactory.decodeResource(getResources(),R.drawable.led,opt);
        }
       
        public void setLed(int i_nLed){
                m_nLed = i_nLed;
        }

Torniamo nell'activity principale, implementiamo runnable (non è un tutorial sui thread) e cambiamo il costruttore in questo modo:

Codice (Java): [Seleziona]
protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_led);
                m_LedImageView = (LedBaseImageView) findViewById(R.id.LedBase);
                m_LedImageView.setLed(m_nLed);
                m_LedImageView.init();
                Thread currentThread = new Thread(this);        
                currentThread.start();
        }

Per la run semplicemente incrementiamo il led settiamo il suo numero nella nostra imageview e le chiediamo di ridisegnarsi.
Andiamo in sleep per 1 secondo e il ciclo continua (ripeto non è un tutorial sui thread).

Codice (Java): [Seleziona]
@Override
        public void run() {
       
               
                try {      
                        while(true){
                       
                        m_nLed = (m_nLed+1)%5;
                        m_LedImageView.setLed(m_nLed);
                        m_LedImageView.postInvalidate();
                               
                        Thread.sleep(1000);
                       
                }
        }
                catch (InterruptedException e)
                {            
                       
                }
        }

PASSO 3: DISEGNO
La logica è decisamente semplice, il codice forse un pò meno.

Ridefiniamo la onDraw della nostra imageview. Chiamiamo subito la super.OnDraw(canvas) così l'immagine di sfondo viene ridisegnata dal sistema.

Adesso disegnamo il led: in primo luogo sappiamo per il tipo di view che l'aspect ratio è mantenuto. Questo elimina diversi problemi ma ne introduce un altro. In pratica abbiamo delle bande riempitive nel nostro canvas.

Per prima cosa ci calcoliamo la dimensione maggiore e la dimensione minore.
La differenza tra questi due ci da lo spazio totale delle bande riempitive.
Dividendo per due otteniamo un offset che ci dice dopo quanti pixel l'immagine inizia.

Questo è il codice per calcolare gli offset:

Codice (Java): [Seleziona]
// Vedo delle due dimensioni quale è la maggiore.
                int maxDimension = canvas.getHeight();
                int minDimension = canvas.getHeight();
                if( maxDimension < canvas.getWidth())
                {
                        maxDimension = canvas.getWidth();
                }
                else
                        minDimension = canvas.getWidth();

                // Mi calcolo la differenza tra le due dimensioni (cioè la lunghezza dei due bordi) e divido per 2 (cioè la lunghezza di 1 bordo).
                int xoffset = (maxDimension-minDimension)/2;
                int yoffset = (maxDimension-minDimension)/2;
               
                // Se la dimensione più grande è la larghezza allora sono in landscape e quindi la banda sta sulla x... azzero l'offset y
                if( canvas.getWidth() == maxDimension )
                        yoffset = 0;
                else    // Altrimenti azzero l'offset x
                        xoffset = 0;

Mi trovo quindi con minDimension che è la risoluzione vera di questa immagine sul device e con xoffset e yoffset già calcolati correttamente.

A questo punto dobbiamo disegnare il led tenendo conto di due cose:
1- il posizionamento relativo
2- la scala dell'immagine delled

Il punto uno è semplice si calcolano le posizioni di un punto (x,y) come rapporto rispetto alle dimensioni totali dell'immagine.

Codice (Java): [Seleziona]
           // L'immagine è 256x256. Ci sono 16 pixel di scostamento sinistro (0.0625) ogni led è 32 pixel ed è spaziato dal successivo di altri 16 pixel.
                // 32+16 = 48 che rispetto a 256 è 0.1875.
                // Sommo gli offset x e y in modo da saltare la banda, ovunque essa sia.
                float x = (0.0625f+(m_nLed%5)*0.1875f)*(float)(minDimension)+xoffset;
                // Qua faccio un posizionamento verticale volutamente spreciso... ho posizionato la fila a 71 pixel, cioè 0.2773 e spiccioli.         
                float y = (0.2773f)*(float)(minDimension)+yoffset;

A questo punto abbiamo le coordinate, non ci resta che calcolare la scala.
La scala, banalmente è lo stesso rapporto che intercorre tra l'immagine originale e l'immagine sullo schermo.
Possiamo quindi creare una matrice:

Codice (Java): [Seleziona]
       
                Matrix matrix = new Matrix();
                matrix.postScale(minDimension/256.0f,minDimension/256.0f);
                matrix.postTranslate(x,y);

E infine disegnare:

Codice (Java): [Seleziona]
canvas.drawBitmap(m_LedImage,matrix,null);
CONSIDERAZIONI FINALI
Abbiamo ottenuto quello che volevamo, cioè poter posizionare elementi relativi all'interno di una imageview con precisione al pixel, renderli dinamici e poterli manipolare indipendentemente dalla risoluzione del device. La domanda che qualcuno potrebbe porsi è perchè andarli a posizionare così invece di inserirli in qualche modo dentro alla imageview. A parte il fatto che saremmo comunque costretti a specificare delle percentuali o delle dimensioni relative, questo sistema è molto più universale.

Infatti in assenza di supporto diretto dei layout, come nel caso di OpenGL o di una surfaceview, la logica di disegno rimane sempre la stessa.
Il tutorial è quindi non solo una possibile soluzione per un problema ricorrente, ma anche un primo passo verso la possibilità di disegnare interfacce complesse con tecnologie differenti quali appunto OpenGL.

Sorgenti:

activity_led.xml
Codice (XML): [Seleziona]
<RelativeLayout xmlns:android="[url]http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".LedActivity"
   android:background="#000000" >
<!-- qua sotto prova a mettere invece di fill_parent 50px o 250px... vedrai che il tutto si scala alla perfezione!!! -->

    <net.drkappa.tut.ledtutorial.LedBaseImageView
       android:id="@+id/LedBase"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:adjustViewBounds="true"
       android:src="@drawable/base" />

</RelativeLayout>

AndroidManifest.xml
Codice (XML): [Seleziona]
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="net.drkappa.tut.ledtutorial"
   android:versionCode="1"
   android:versionName="1.0" >
<!-- prova a usare pure landscape... va ugualmente (ma lo taglia) -->
    <uses-sdk
       android:minSdkVersion="10"
       android:targetSdkVersion="15" />

    <application
       android:allowBackup="true"
       android:icon="@drawable/ic_launcher"
       android:label="@string/app_name"
       android:theme="@style/AppTheme" >
        <activity
           android:name="net.drkappa.tut.ledtutorial.LedActivity"
           android:label="@string/app_name"
           android:screenOrientation="portrait"
           android:configChanges="keyboardHidden|orientation" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


LedActivity.java
Codice (Java): [Seleziona]
package net.drkappa.tut.ledtutorial;

import android.os.Bundle;
import android.app.Activity;


public class LedActivity extends Activity implements Runnable{

        protected LedBaseImageView m_LedImageView=null;
        protected int m_nLed = -1;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_led);
                m_LedImageView = (LedBaseImageView) findViewById(R.id.LedBase);
                m_LedImageView.setLed(m_nLed);
                m_LedImageView.init();
                Thread currentThread = new Thread(this);        
                currentThread.start();
        }

        @Override
        public void run() {
       
               
                try {      
                        while(true){
                       
                        m_nLed = (m_nLed+1)%5;
                        m_LedImageView.setLed(m_nLed);
                        m_LedImageView.postInvalidate();
                               
                        Thread.sleep(1000);
                       
                }
        }
                catch (InterruptedException e)
                {            
                       
                }
        }

}

LedBaseImageView.java
Codice (Java): [Seleziona]
package net.drkappa.tut.ledtutorial;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.widget.ImageView;

public class LedBaseImageView extends ImageView {

        protected int m_nLed = -1;
        protected Bitmap m_LedImage = null;
        public LedBaseImageView(Context context, AttributeSet attrs) {
                super(context, attrs);
               
        }

        public void init(){
                m_nLed = -1;
                BitmapFactory.Options opt = new BitmapFactory.Options();
                opt.inScaled = false;
                opt.inPreferredConfig = Bitmap.Config.ARGB_8888;
                m_LedImage = BitmapFactory.decodeResource(getResources(),R.drawable.led,opt);
        }
       
        public void setLed(int i_nLed){
                m_nLed = i_nLed;
        }
       
        @Override  
        public void onDraw(Canvas canvas)
         {    
                super.onDraw(canvas);
                if( m_nLed == -1 || m_LedImage == null)
                        return;
               
                // Allora... questa è una cosa un pò complicata ma dipende dal tipo di scalatura dell'imageview che ho scelto.
                // Io ho scelto una view che si ridimensiona in modo generico cioè mi filla il parent così che
                // mi incontra il primo bordo che trova, preserva l'aspect ratio e si ferma.
                // Questo significa che la mia immagine non riempie tutto lo schermo ma ha dei bordi laterali a seconda che sia
                // in portrait o landscape. Purtroppo però se gli dico fill_parent il canvas riempie comunque l'area disponibile.
                // Io devo quindi tenere conto di questo bordo e calcolarmelo, vediamo in che modo.
               
               
                // Vedo delle due dimensioni quale è la maggiore.
                int maxDimension = canvas.getHeight();
                int minDimension = canvas.getHeight();
                if( maxDimension < canvas.getWidth())
                {
                        maxDimension = canvas.getWidth();
                }
                else
                        minDimension = canvas.getWidth();

                // Mi calcolo la differenza tra le due dimensioni (cioè la lunghezza dei due bordi) e divido per 2 (cioè la lunghezza di 1 bordo).
                int xoffset = (maxDimension-minDimension)/2;
                int yoffset = (maxDimension-minDimension)/2;
               
                // Se la dimensione più grande è la larghezza allora sono in landscape e quindi la banda sta sulla x... azzero l'offset y
                if( canvas.getWidth() == maxDimension )
                        yoffset = 0;
                else    // Altrimenti azzero l'offset x
                        xoffset = 0;
               
                // Adesso vado a disegnare al pixel...
                // L'immagine è 256x256. Ci sono 16 pixel di scostamento sinistro (0.0625) ogni led è 32 pixel ed è spaziato dal successivo di altri 16 pixel.
                // 32+16 = 48 che rispetto a 256 è 0.1875.
                // Sommo gli offset x e y in modo da saltare la banda, ovunque essa sia.
                float x = (0.0625f+(m_nLed%5)*0.1875f)*(float)(minDimension)+xoffset;
                // Qua faccio un posizionamento verticale volutamente spreciso... ho posizionato la fila a 71 pixel, cioè 0.2773 e spiccioli.         
                float y = (0.2773f)*(float)(minDimension)+yoffset;
               
                // Calcolo la matrice (scalo solo per la dimensione minima perchè so che la view mi mantiene l'aspect ratio dell'immagine).
                Matrix matrix = new Matrix();
                matrix.postScale(minDimension/256.0f,minDimension/256.0f);
                matrix.postTranslate(x,y);
               
                // Disegno.
                canvas.drawBitmap(m_LedImage,matrix,null);

         }
}

Bibliografia: