mercoledì 11 novembre 2015

Applicare il metodo tint() usando la Support Library

Applicare una tinta programmaticamente è possibile soltanto se si usano API > 21 (cioè da Lollipop in poi). Però mediante l'Android Support Library v4 è possibile utilizzare il metodo setTint() su un Drawable anche a quei dispositivi che montano una versione precedente di Android.

Esempio:

Supponiamo di voler applicare la tinta ad un drawable in base ad una condizione. Se quella condizione si verifica, usiamo il drawable "tinteggiato", altrimenti usiamo il drawable originale.

Realizzazione:

Prima di tutto importiamo la libreria.

import android.support.v4.graphics.drawable.DrawableCompat;

A questo punto invochiamo il metodo in base al verificarsi o meno della condizione.

Drawable myDrawable = ContextCompat.getDrawable(getContext(), R.drawable.my_icon);
ImageView myImageView = (ImageView) findViewById(R.id.my_image_view);

if (condition)
    tintDrawable(myDrawable, myImageView, false);
else
    tintDrawable(myDrawable, myImageView, true);

Vediamo ora il metodo tintDrawable() a cui vengono passati come parametri il Drawable che si vuole tinteggiare, l'ImageView dove mostrarlo e un booleano.

private void tintDrawable(Drawable drawable, ImageView imageView, boolean original) {

    // false = disabled icon
    if (!original) {  
        drawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(drawable, ContextCompat.getColor(getContext(), R.color.grey800));
        DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SCREEN);
        imageView.setImageDrawable(drawable);
        
    // true = "original" icon
    } else {          
        drawable = DrawableCompat.unwrap(drawable);
        DrawableCompat.setTintList(drawable, null);
        imageView.setImageDrawable(drawable);
    }

}

Bisogna notare come venga effettuato il wrapping del drawable che si vuole tinteggiare. Però, per riottenere il drawable originale non basta soltanto effettuare l'operazione inversa, cioè l'unwrap, ma occorre anche annullare l'operazione di tinta invocando il metodo setTintList() con null come parametro.

lunedì 9 novembre 2015

Multiple Screen

La risoluzione di un dispositivo è il numero di pixel presenti in uno schermo.
La densità (dpi) è il numero di pixel presenti all'interno di un pollice della diagonale dello schermo.

ppi 1

La risoluzione non è molto utile nel design di un app Android in quanto non tiene conto della dimensione fisica del dispositivo. Ecco perchè Android lavora in termini di Density-Independent Pixel (dp) che permettono al designer di creare elementi indipendenti dalla risoluzione e della densità dello schermo del dispositivo.


Un dp è equivalente ad 1px su uno schermo da 160dpi, ovvero la densità base riferita ad uno schermo a densità media. In pratica 1 pixel = DP * (DP / 160).
Ad esempio:
  • a 160dpi, un dp corrisponde ad un pixel
  • a 320dpi, un dp corrisponde a due pixel
Tutto questo per dire che ci sono dispositivi con la stessa risoluzione, cioè con lo stesso numero di pixel, ma con una quantità di spazio differente.


Density buckets

Quando si sviluppa un app, una delle possibilità è quella di fornire come risorsa l'immagine con la densità più alta possibile, in modo che sia poi Android ad effettuare lo scaling (down-sampling) della stessa per i dispositivi a densità minore.
La soluzione migliore, in ogni caso, consiste nel fornire un'immagine per ogni densità disponibile. Ci sono molti tools per creare automaticamente le immagini per ogni density bucket. Uno di questi è l'Android Asset Studio.

Project folders

E' necessario creare una struttura di cartelle con dei qualificatori all'interno della cartella res per il salvataggio delle risorse relative a ciascuna tipologia di dispositivi.

State List Drawables

Gli elementi grafici della UI dell'app possono cambiare aspetto per indicare lo stato in cui si trovano.
Questo genere di situazioni viene gestito in Android mediante gli state list drawables, che sono dei file XML che elencano le risorse da usare per un particolare stato dell'elemento grafico.

Quando si usano questi particolari drawable, Android analizza l'XML partendo dall'alto fino a trovare il primo item che soddisfa lo stato indicato. In pratica, dà la precedenza agli item posizionati in alto rispetto a quelli posizionati in basso. Per questo motivo, se volete usare un'immagine che rappresenta stati multipli (ad esempio, checked e pressed contemporaneamente) occorre riportare in alto questa combinazione.

domenica 8 novembre 2015

Impostare uno "branded launch" (screen iniziale col logo dell'app)

Quando la vostra app non è in memoria e viene lanciata, questa "esecuzione a freddo" può richiedere più tempo rispetto all'esecuzione della stessa app quando è già in memoria. Ovviamente, il tempo che impiega dipende da una serie di fattori come la dimensione dell'app e quali sono le operazioni che vengono svolte all'interno del metodo onCreate() (che spero per voi siano più poche possibili). Durante questo tempo, il window manager fa del suo meglio mostrando sul display una UI temporanea - un placeholder - usando elementi presi dal vostro tema come lo sfondo e il colore della status bar.

                 

Possiamo andare a modificare opportunamente questi elementi in modo da rendere più user-friendly questo "tempo morto" di avvio. Un esempio è quello di impostare il logo dell'app come sfondo del nostro tema invece di un colore a tinta unita.

Vediamo come impostare qesto branded launch.

Occorre creare un tema custom che modifichi android:windowBackground per poi impostarlo come tema dell'app prima della chiamata a super.onCreate().

Supponiamo di avere un tema chiamato AppTheme, il launcher dovrebbe essere:

In questo modo facciamo sì che il nostro launcher theme erediti le sue proprietà dal tema principale, modificando soltanto due attributi:
  • il windowBackground, cioè lo sfondo
  • il colorPrimaryDark, ovvero il colore della status bar
Sfortunatamente, la risorsa drawable/launch_screen impostata come sfondo, non può essere solo una semplice immagine, ma deve essere strutturata in questo modo:

A questo punto non ci resta che impostare il nostro branded launch all'interno dell'AndroidManifest.xml usando

android:theme="@style/AppTheme.Launcher"

Il modo migliore per ritornare al tema di default è quello di invocare setTheme(R.style.AppTheme) prima di super.onCreate() e setContentView() all'interno dell'activity.

Ulteriori informazioni: link

sabato 7 novembre 2015

Selezionare una foto dalla Galleria

Vediamo come selezionare un'immagine dalla Galleria usando gli intent.

Innanzitutto, occorre creare l'Intent.

public class MyFragment extends Fragment
    private static final int REQUEST_GALLERY = 100;
    ...
    private void openGallery() {    
        Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
        galleryIntent.setType("image/*");
        startActivityForResult(galleryIntent, REQUEST_GALLERY);
    }

N.B. Alcuni usano Intent.ACTION_PICK ma è bene utilizzare ACTION_GET_CONTENT che è più universale (Fonte: link)
Dopodichè occorre fare l'override dell' OnActivityResult() in modo da gestire il risultato restituito dalla Galleria, cioè l'immagine.

private String mCurrentPhotoPath;
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    switch (requestCode) {
        case REQUEST_GALLERY:
            if (resultCode == Activity.RESULT_OK) {
                Uri selectedImageUri = data.getData();
                mCurrentPhotoPath = getPathFromUri(selectedImageUri);

                if (mCurrentPhotoPath != null) {
                    setPicInImageView();      /* Show the pic in the ImageView */
                    mCurrentPhotoPath = null;
                }
            }
            break;

    }
}

In pratica, in questo metodo si verifica se la Galleria ha ritornato l'immagine, in caso positivo si estrae l'uri della stessa in modo da poter poi ricavare il path completo (absolute path) della risorsa attraverso il metodo getPathFromUri(). Questo metodo è molto importante in quanto si basa su una classe custom che dovremo implementare, all'interno della quale dovremo specificare come ottenere il path in base alla versione di Android sulla quale sta girando l'applicazione.

Vediamo prima il codice sorgente del metodo getPathFromUri():

private String getPathFromUri(Uri uri) {
    if (Build.VERSION.SDK_INT < 11)
        return RealPathUtil.getRealPathFromURI_BelowAPI11(getContext(), uri);
    else if (Build.VERSION.SDK_INT < 19)
        return RealPathUtil.getRealPathFromURI_API11to18(getContext(), uri);
    else
        return RealPathUtil.getRealPathFromURI_API19(getContext(), uri);
}

Vediamo ora, invece, il codice sorgente della classe Util menzionata in precedenza, denominata RealPathUtil.

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.MediaStore;

public class RealPathUtil {

    @SuppressLint("NewApi")
    public static String getRealPathFromURI_API19(Context context, Uri uri) {
        String filePath = "";
        String wholeID = DocumentsContract.getDocumentId(uri);

        // Split at colon, use second item in the array
        String id = wholeID.split(":")[1];
        String[] column = { MediaStore.Images.Media.DATA };

        // where id is equal to
        String sel = MediaStore.Images.Media._ID + "=?";

        Cursor cursor = context.getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, column, sel, new String[]{ id }, null);

        assert cursor != null;
        int columnIndex = cursor.getColumnIndex(column[0]);

        if (cursor.moveToFirst()) {
            filePath = cursor.getString(columnIndex);
        }
        cursor.close();
        return filePath;
    }

    @SuppressLint("NewApi")
    public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) {
        String[] proj = { MediaStore.Images.Media.DATA };
        String result = null;

        CursorLoader cursorLoader = new CursorLoader(context, contentUri, proj, null, null, null);
        Cursor cursor = cursorLoader.loadInBackground();

        if (cursor != null) {
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            cursor.moveToFirst();
            result = cursor.getString(column_index);
        }
        return result;
    }

    public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) {
        Cursor cursor = null;

        try {
            String[] proj = { MediaStore.Images.Media.DATA };
            cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
            assert cursor != null;
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            cursor.moveToFirst();
            return cursor.getString(column_index);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

}

Infine, è giusto osservare il metodo setPicInImageView() che consente di visualizzare l'immagine all'interno della ImageView desiderata. E' bene sottolineare. però, che viene effettuato uno scaling dell'immagine in modo da evitare che l'app crashi producendo l'errore:

java.lang.OutofMemoryError: bitmap size exceeds VM budget

Il codice sorgente del metodo è il seguente:

private void setPicInImageView() {
    // Get the dimensions of the ImageView
    int targetW = mPicImageView.getWidth();
    int targetH = mPicImageView.getHeight();

    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    mPicImageView.setImageBitmap(bitmap);
}

Per saperne di più: link (sezione: Loading Large Bitmaps Efficiently)