Aggiunta di una ProgressBar orizzontale sotto la ActionBar

Durante lo sviluppo di un'app che si connetteva ad un servizio remoto, ho avuto la necessità di visualizzare un elemento che indicasse all'utente l'attività in background che il dispositivo stava effettuando.

All'inizio ho optato per un ProgressDialog. E' una buona opzione, ma nel caso siano presenti delle animazioni a seguito di un invio, il dialog "blocca" la visualizzazione delle stesse e risulta antiestetico.

Ho quindi optato per un sistema meno "invasivo": una barra orizzontale per segnalare il lavoro in background.

L'oggetto ProgressBar è semplice da implementare, lo è meno l'integrazione con una ActionBar personalizzata e "importata" nel layout tramite un'inclusione, come in questo esempio:

1
2
3
4
5
6
7
8
9
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="it.example.test_progressbar.MainActivity">

    <include layout="@layout/toolbar"/>

Impostazioni di base ed elementi necessari

Dopo aver creato un nuovo progetto "base" con una Activity vuota, per implementare la ProgressBar sono necessari i seguenti file (da creare o modificare):
  • activity_main.xml
  • toolbar.xml
  • AndroidManifest.xml
  • styles.xml
  • MainActivity.java

Modifiche XML

Partiamo dalle modifiche al design XML, che poi saranno le più "corpose".

AndroidManifest.xml

Il problema iniziale da affrontare è che la struttura "base" creata da Android Studio inserisce una ActionBar "basilare" direttamente dall'AndroidManifest attraverso questa voce:

1
2
3
<application
    ...
    android:theme="@style/AppTheme">

Per eliminare l'ActionBar, bisogna caricare specificare queste righe nel file styles.xml:

1
2
3
4
<style name="AppTheme.NoActionBar">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
</style>

rimuovere la voce incriminata e sostituirla con questa:

1
2
3
<application
    ...
    android:theme="@style/AppTheme.NoActionBar">

activity_main.xml

L'XML dell'Activity principale richiede una sola modifica, l'aggiunta della toolbar contenuta nel file separato.

Basta aggiungere, sotto all'apertura del tag del ConstraintLayout inserito dal sistema, di questa riga:

1
<include layout="@layout/toolbar"/>

toolbar.xml

La toolbar di norma viene innestata all'interno di un CoordinatorLayout, che verrà mantenuto.
Il codice di partenza è questo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="it.tortellinux.diceroller.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <LinearLayout
        android:id="@+id/fragment_container"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
        >
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

Per gestire correttamente la ProgressBar vanno applicate alcune modifiche sostanziali.

Per prima cosa, va rimosso quello che Android "presenta" sotto la Toolbar, cioè un piccolo margine e l'ombra.
Questo perché la ProgressBar verrà visualizzata "sotto" l'ombra e l'effetto sarà... inguardabile.

Bisogna quindi impostare l'elevation ed il margine inferiore a 0dp:

1
2
3
4
5
<android.support.design.widget.AppBarLayout
    ...
    app:elevation="0dp"
    android:layout_marginBottom="0dp"
    />

Poi è necessario aggiungere un FrameLayout che conterrà la ProgressBar. La rappresentazione "base" della ProgressBar utilizza un bordo superiore ed uno, più piccolo, inferiore.

Questi verranno eliminati inserendo un altezza di 3dp al frame ed impostando un margine negativo (sì, si può) alla ProgressBar, oltre ad un'altezza fissa.

E' necessario, infine, impostare questo comportamento al frame:

1
2
3
4
<FrameLayout
    ...
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >

Questo impedisce alla Toolbar di "coprire" la ProgressBar.

Questo è il codice finale:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="it.tortellinux.test_progressbar.MainActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="3dp"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <ProgressBar
            android:id="@+id/progress_spinner"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="7dp"
            android:layout_gravity="top"
            android:layout_marginTop="-3dp"
            android:indeterminate="true"
            android:visibility="visible"
            />
    </FrameLayout>

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:elevation="0dp"
        android:layout_marginBottom="0dp">
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            />

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

MainActivity.java

Per l'implementazione della ProgressBar bastano le modifiche all'XML indicate qui sopra, solo che c'è bisogno di un sistema per renderla visibile o farla sparire.

Dopo aver recuperato l'oggetto attraverso la funzione findViewById, per impostare la visibilità della ProgressBar si utilizzano due semplici comandi.

Per visualizzarla:

1
progressBar.setVisibility(View.VISIBLE);

Per farla sparire:

1
progressBar.setVisibility(View.GONE);

L'effetto finale

Completati i vari passaggi, si ottiene questo effetto:

Come personalizzare il nome dell'APK prodotto da Android Studio

Di norma, quando viene generato un APK si ottiene un file dal nome standard, "app-release.apk" o "app-debug.apk", che poi di norma va modificato per evitare la sovrascrittura di altre app durante il caricamento nei dispositivi o anche solo per la conservazione.

La gestione della compilazione, nei progetti Android, è gestita dal gradle. Per modificare il nome del file generato, quindi, si deve aggiungere qualche riga al build.gradle dell'app (e non del progetto "generale").

Il file, di norma, appare così:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "it.test.example"
        minSdkVersion 23
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:design:25.3.1'
    testCompile 'junit:junit:4.12'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
}

la sezione importante che andrà modificata, buildTypes > release, è:


1
2
3
4
5
6
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

All'interno della sezione release bisogna inserire un nuovo "elemento":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
applicationVariants.all { variant ->
    variant.outputs.each { output ->
        project.ext { appName = 'nomeApplicazione' }
        def newName = output.outputFile.name
        newName = newName.replace("app-", "$project.ext.appName-")
        newName = newName.replace("-release", "")

        output.outputFile = new File(output.outputFile.parent, newName)
    }
}

Ovviamente dovete sostituire 'nomeApplicazione' con il nome dell'app!

In questo modo, quando l'APK verrà generato, verrà prodotto con il nome selezionato e non più come "app-release.apk".

Icone per menu e altro - SVG e PNG

Il design attuale dei menu, basato su Material, adotta uno stile particolare: il nome della voce affiancato ad un'icona.

Un ottimo repository per icone, contenente sia quelle fornite di base da Google sia quelle create e caricate dagli utenti, è:


Alcuni esempi:

Generazione di un ID valido per un oggetto definito da codice

Ogni oggetto, per poter essere richiamato, ha un ID associato. Definirlo manualmente è possibile, ma può causare problemi, per cui il framework mette a disposizione un metodo della View per adempiere allo scopo:

1
View.generateViewId();

Questo metodo restituisce un ID valido che può essere associato ad un elemento definito da codice.

Impostare l'altezza di un ToggleButton

Quando si utilizza un ToggleButton con un background drawable (o anche solo con la grafica standard del Material Design), il sistema imposta automaticamente un padding attorno al testo dell'elemento, la cui presenza impedisce di definire programmaticamente l'altezza.

Per ovviare a questo problema, è necessario impostare l'altezza minima di questo elemento a zero. In questo modo, il sistema elimina il padding ed utilizza solamente le impostazioni definite per l'oggetto.

Definizione da layout XML

E' possibile impostare questo parametro nel codice XML in questo modo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<ToggleButton
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/togglebutton_color"
    android:textColor="@color/togglebutton_text"
    android:layout_margin="5dp"
    android:text="10"
    android:minHeight="0dp"
    />

Oggetto generato da codice

Nel caso, invece, si voglia generare l'oggetto in modo dinamico direttamente da codice ed inserirlo in una View, bisogna aggiungere un'impostazione in più.

I due metodi da richiamare sono:

  • tb.setMinHeight(0);
  • tb.setMinimumHeight(0);

Questo è il codice per la creazione dell'oggetto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// nuova istanza ToggleButton
ToggleButton tb = new ToggleButton(this);

// generazione di ID e TAG
int generatedId = View.generateViewId();
tb.setId(generatedId);
tb.setTag("TAG");

// testo / impostazione iniziale
tb.setTextOff("Testo OFF");
tb.setTextOn("Testo ON");
tb.setChecked( i == 1 );

// parametri di layout di base
LinearLayout.LayoutParams lParam = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
tb.setLayoutParams(lParam);

// altezza minima
tb.setMinHeight(0);
tb.setMinimumHeight(0);

// background
tb.setBackgroundDrawable(this.getResources().getDrawable(R.drawable.togglebutton_color, this.getTheme()));

// colore del testo
tb.setTextColor(getResources().getColorStateList(R.color.togglebutton_text, this.getTheme()));

// listener (così ne seleziona uno solo tra tutti quelli della riga
tb.setOnClickListener(new View.OnClickListener()
{
    @Override
    public void onClick(View v)
    {
        // fai qualcosa
    }
});

Far "pulsare" un'immagine... o qualche altro elemento!

Per far "pulsare" qualche elemento nella propria app sono necessari 3 elementi:
  • l'oggetto da far "pulsare" (ad esempio, un ImageView)
  • l'XML con la configurazione dell'animazione
  • il codice per gestire l'animazione

Oggetto da far "pulsare"

L'oggetto da far pulsare può essere un semplice ImageView, come questo:

1
2
3
4
5
6
<ImageView
    android:id="@+id/ivIconOk"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    app:srcCompat="@drawable/ic_result_ok"
    />

Animare utilizzando un XML

L'XML per l'animazione è il seguente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
           android:duration="120"
           android:fromXScale="1"
           android:fromYScale="1"
           android:pivotX="50%"
           android:pivotY="50%"
           android:repeatCount="3"
           android:repeatMode="reverse"
           android:toXScale="0.8"
           android:toYScale="0.8"
           android:interpolator="@android:anim/bounce_interpolator"
        />
</set>

Quest'animazione fa pulsare 3 volte l'oggetto (riduce dal 100% al 50% di dimensione, fissando il punto centrale) e poi si ferma.

Alcune annotazioni

Per "pulsazione" si intende riduzione o ingrandimento. Impostarlo a 3 volte vuol dire che, partendo dalla dimensione minore, si ingrandirà, si rimpicciolirà e poi si reingrandirà di nuovo.
Il repeatMode "reverse" indica che, concluso un ciclo, riprende dalla forma corrente (ingrandimento-riduzione-ingrandimento-riduzione...)

Codice Java

Questo invece è il codice per attivare la "pulsazione":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final ImageView iv = (ImageView) findViewById(R.id.ivIcon);
Animation pulse = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.pulse);

pulse.setAnimationListener(new Animation.AnimationListener()
{
    @Override
    public void onAnimationStart(Animation animation)
    {
        // avvio della pulsazione
    }

    @Override
    public void onAnimationEnd(Animation animation)
    {
        // conclusione dell'animazione
    }

    @Override
    public void onAnimationRepeat(Animation animation)
    {
        // ad ogni ripetizione dell'animazione
    }
});

iv.startAnimation(pulse);

Posticipare l'esecuzione di un pezzo di codice

Per posticipare l'esecuzione di un pezzo di codice, non è possibile utilizzare il Thread.sleep, in quanto blocca l'attività dell'interfaccia grafica.

Bisogna invece utilizzare questo pezzo di codice:

1
2
3
4
5
6
7
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
  @Override
  public void run() {
    //Do something after 100ms
  }
}, 100);

Utilizzando il postDelayed e l'oggetto Runnable, il sistema attende in un thread separato 100 millisecondi e poi esegue il corpo della funzione "run".

Gestire le animazioni su elementi dentro un ConstraintLayout

Il problema, quando si utilizza un ConstraintLayout, è che il sistema dà la priorità ai "constraint" più che alle specifiche altezze (che non vengono prese in considerazione).

Per gestire delle animazioni (o anche solo per gestire cambi di dimensioni), è necessario utilizzare di ConstraintSet e reimpostarli al bisogno.

Ecco un codice di esempio che gestisce la procedura:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
// button che avvia l'animazione
Button btn = (Button) findViewById(R.id.btnRileva);

// listener per l'attività
btn.setOnClickListener(new View.OnClickListener()
{
    @Override
    public void onClick(final View v)
    {
        // contenitore RelativeLayout
        final RelativeLayout rlContainer = (RelativeLayout) findViewById(R.id.rlContainer);

        // parametri di un ConstraintLayout
        final ConstraintLayout clMain = (ConstraintLayout) findViewById(R.id.ccMainLayout);

        // set di "constraint" (da cambiare o da ripristinare)
        final ConstraintSet set = new ConstraintSet();
        final ConstraintSet original = new ConstraintSet();

        // per avere una copia degli originali, bisogna clonarli
        set.clone(clMain);
        original.clone(clMain);

        // altezza iniziale dell'elemento
        final int initialHeight = rlContainer.getMeasuredHeight();

        // durata dell'effetto
        final int duration = (int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density);

        Animation a = new Animation()
        {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t)
            {
                // inizio dell'attività di animazione
                if (interpolatedTime == 0)
                {
                    // rottura della connessione del vincolo del contenitore da "animare"
                    set.clear(R.id.rlContainer, ConstraintSet.BOTTOM);

                    // applicazione del set al contenitore "padre" (la root del layout)
                    set.applyTo(clMain);

                    // impostazione dell'altezza iniziale, in modo da non presentare "strane variazioni"
                    rlContainer.getLayoutParams().height = initialHeight;

                    // avverto il sistema che il layout è cambiato, così viene ridisegnato
                    rlContainer.requestLayout();
                }
                // conclusione dell'attività di animazione
                else if(interpolatedTime == 1)
                {
                    // faccio sparire il contenitore
                    rlContainer.setVisibility(View.GONE);
                }
                else
                {
                    // calcolo dell'altezza in base al tempo di interpolazione
                    int height = initialHeight - ((int)(initialHeight * interpolatedTime));

                    // imposto l'altezza
                    rlContainer.getLayoutParams().height = height;

                    // avverto il sistema che il layout è cambiato, così viene ridisegnato
                    rlContainer.requestLayout();
                }
            }

            @Override
            public boolean willChangeBounds()
            {
                return true;
            }
        };

        // imposta la durata dell'animazione
        a.setDuration(duration);

        // imposto un listener per gestire avvio e fine dell'animazione
        a.setAnimationListener(new Animation.AnimationListener()
        {
            @Override
            public void onAnimationStart(Animation animation) { }

            @Override
            public void onAnimationEnd(Animation animation)
            {
                Toast.makeText(getApplicationContext(), "fine dell'animazione", Toast.LENGTH_LONG).show();

                // reimposto il ConstraintSet originale
                original.applyTo(clMain);
                rlContainer.getLayoutParams().height = initialHeight;
                rlContainer.requestLayout();
            }

            @Override
            public void onAnimationRepeat(Animation animation) { }
        });

        // avvio l'animazione
        rlContainer.startAnimation(a);
    }
});

Disabilitare "effetto" click in un button, mantenendo e gestendo il click

Per rimuovere l'effetto del "click" (cioè l'ombreggiatura al momento del tocco) in un bottone, bisogna definire il background del bottone con un elemento drawable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<Button
    android:id="@+id/btnVisualizzaMenuPeso"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/button_peso"
    app:layout_constraintBottom_toTopOf="@+id/llListaPeso"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:background="@drawable/togglebutton_peso_color_background"
    android:textColor="@color/colorTextWhite"
    />

A questo punto si definisce l'elemento @drawable/togglebutton_peso_color_background:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true">
        <shape android:shape="rectangle" >
            <corners android:radius="3dip" />
            <stroke
                android:width="1dip"
                android:color="@android:color/transparent"
                />
            <solid android:color="@color/colorTextWhite" />
            <padding
                android:bottom="10dp"
                android:left="10dp"
                android:right="10dp"
                android:top="10dp"
                />
        </shape>
    </item>
    <item>
        <shape android:shape="rectangle">
            <padding
                android:bottom="10dp"
                android:left="10dp"
                android:right="10dp"
                android:top="10dp"
                />
        </shape>
    </item>
</selector>

In questo modo, quando l'elemento viene "toccato", non visualizza l'ombra o altro.

Codice per lo "swipe" di elementi di una ListView

Link ad un sito con la spiegazione su come utilizzare un RecyclerView per gestire lo "swipe" (scorrimento) per eliminare elementi da una lista.

Disegnare un rettangolo in XML

Questa è la guida per disegnare un rettangolo in XML.

Crea un file XML nella directory drawable e aggiungi il codice seguente:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listview_background_shape">
    <stroke android:width="2dp" android:color="#ff207d94" />
    <padding android:left="2dp"
        android:top="2dp"
        android:right="2dp"
        android:bottom="2dp" />
    <corners android:radius="5dp" />
    <solid android:color="#ffffffff" />
</shape>

Questo invece è il codice per inserirlo come background in una View:


1
2
3
4
5
<View
    android:id="@+id/myRectangleView"
    android:layout_width="200dp"
    android:layout_height="50dp"
    android:background="@drawable/rectangle"/>

A questo punto dovrebbe essere tutto a posto.

Una nota: per le ImageViews, bisogna utilizzare l'attributo android:src!

Generare un colore randomico di tonalità "pastello"

Questo script restituisce il codice di un colore generato a caso nelle tonalità pastello. La generazione viene definita attraverso le 3 ca...