JavascriptProva

venerdì 23 settembre 2016

Esercizio di traslazione e scaling di immagini.

Avendo ben compreso il funzionamento di ImagePicker, quasi quasi lo lascerei così com'è.

Andiamo per ordine: sarei contento se rifacessi la procedura di ImagePicker di sana pianta.
Sì, ma ho serie difficoltà per coniugare scaling e traslazione.
Bene, devi solo rivedere quelle due parole chiave del multitouch, allora. Ma prima io ricomincerei con un po' di sano esercizio sulla traslazione e lo scaling separati, giusto per rinfrescare la memoria.
Okay!

Traslazione:

Immagine:
public class Immagine extends View{
    float lastX,lastY;
    float posX, posY;
    Drawable mImage;
    Context context;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.facciadaculo);
        mImage.setBounds(0,0,mImage.getIntrinsicWidth(),mImage.getIntrinsicHeight());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){

        int action=event.getAction() & MotionEvent.ACTION_MASK;
        switch(action){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;
        }
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(posX, posY);
        mImage.draw(canvas);
        canvas.restore();

    }

}


MainLayout:
public class MainActivity extends AppCompatActivity {

    Immagine immagine;
    RelativeLayout mainLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        mainLayout.addView(immagine);
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)immagine.getLayoutParams();
        params.width=300;
        params.height=300;
    }

}
E funziona!

Esercizio di scaling:
public class Immagine extends View{

    float scaleFactor=1.f;
    ScaleGestureDetector mDetector;
    Drawable mImage;
    Context context;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.facciadaculo);
        mImage.setBounds(0,0,mImage.getIntrinsicWidth(),mImage.getIntrinsicHeight());
        mDetector=new ScaleGestureDetector(context,new ScaleDetector());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){

        mDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.scale(scaleFactor,scaleFactor);
        mImage.draw(canvas);
        canvas.restore();

    }

    class ScaleDetector extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            scaleFactor*=detector.getScaleFactor();
            invalidate();
            return true;
        }
    }

}
Perfetto! (MainActivity è sempre uguale).

giovedì 22 settembre 2016

Compilazione del database da inserire in J-Speaker

Adesso dobbiamo scrivere il codice per un "visore" del database.

Ho preparato row con due textViews, che spero siano posizionate adeguatamente.
Nel caso ce lo vediamo poi...

> Ecco, hai un modulo di Activity completamente vuoto. Inventa.

Innanzitutto devo istanziare la ListView.
I dati li prendo dal database.

    ListView listView;  
    Helper helper;
Ho dichiarato la listView e il database.
Qual è il passo successivo?

Mettere in un cursor tutti gli elementi della tabella del database, e passarli in una ArrayList.
Ma se ogni elemento contiene due voci, come fare? Metterli in un ArrayList non sotto forma di stringa, ma di elementi che istanziano una classe. Creiamo dunque una classe Record, che in due suoi membri possa accogliere i due campi di cui è formato ogni record del database.

Creo la classe.
    class Record{
        public String parola;
        public int numero;
        public Record(String parola, int numero){
            this.parola=parola;
            this.numero=numero;
        }
    }
Ora si prendono tutti i record del database e si pongono in oggetti di questa classe.
Ho a disposizione il metodo query() del database.

Un momento! Poni i campi di un record nei membri di un oggetto di tipo Record, ma poi devi aggiungere l'oggetto a una Lista, da presentare a un adapter.

Prima dunque creo la lista.
Riassumi
Creo una classe per contenere un record, creo una lista in cui archiviare tutti gli oggetti del tipo di questa classe, e man mano che scorre il Cursor creo un oggetto e lo archivio nella lista.

Bene, procedi.
Creo la lista: la lista deve avere come Tipo (fra parentesi angolari) Record.
    ArrayList lista;

.....

        lista=new ArrayList();
Ora, man mano che si creano gli elementi di tipo Record li appongo alla lista.

Ecco il codice completo per ottenere la lista di elementi del Cursor:
        lista=new ArrayList();
        
        Cursor crs=helper.query();
        crs.moveToFirst();
        do{
            lista.add(new Record(crs.getString(1),crs.getInt(2)));
        } while(crs.moveToNext());

Bene.
Ora si dovrà creare il ListView e l'adapter.

Istanziamo ListView.
listView=(ListView) findViewById(R.id.listView);
E ora dobbiamo estendere la classe ArrayAdapter per creare un CustomAdapter adatto alle nostre esigenze (due textViews).
    class CustomAdapter extends ArrayAdapter{

        public CustomAdapter(Context context, int resource, ArrayList arrayList) {
            super(context, resource, arrayList);
        }
    }
Ora ne overridiamo il metodo getView:
        @Override
        public View getView(int posizione, View convertView, ViewGroup viewGroup){
            LayoutInflater layoutInflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView=layoutInflater.inflate(R.layout.row,null);

            return convertView;
        }
Questo inflata il layout row.
Però ora bisogna ricercare le due textViews e porvi i valori.
        @Override
        public View getView(int position, View convertView, ViewGroup viewGroup){
            LayoutInflater layoutInflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView=layoutInflater.inflate(R.layout.row,null);
            TextView txtParola=(TextView)convertView.findViewById(R.id.textView3);
            TextView txtNumero=(TextView)convertView.findViewById((R.id.textView4));
            Record r=getItem(position); 
            txtParola.setText(r.parola);
            txtNumero.setText(""+r.numero);
            return convertView;
        }
    }
Ecco, l'ho fatta completa.
Adesso resta da attribuire al listView l'adapter e vediamo come va...

        CustomAdapter customAdapter=new CustomAdapter(this,R.layout.row,lista);
        listView.setAdapter(customAdapter);
Bene. Alla fine funziona, ma ci sono ancora dei dettagli: non vedo la textView con il numero.

Torniamo al Layout...

Semplicemente una non perfetta reminiscenza degli attributi da dare nel RelativeLayout.

Ho sistemato tutto. Sulla seconda activity ho una visione soddisfacente della tabella del database.

Ho messo a punto anche il meccanismo dell'immissione mediante un editText e un button, che inseriscono nuove parole nella tabella del database.

Ora devo mettere in opera il mio meccanismo secondo il quale vengono aggiunte soltanto parole che non figurano nel database.
Ecco risolto:
In MainActivity ho questo codice:
            @Override
            public void onClick(View view) {
                if (editText.getText()!=null){
                    Cursor crs=helper.query(editText.getText().toString());
                    crs.moveToFirst();
                    if(crs.getCount()==0){
                        helper.save(editText.getText().toString());
                    }
                    else{
                        helper.incrementa(editText.getText().toString());
                    }

                    editText.setText(null);
                }
            }
che in risposta al click del button mi seleziona un recordset dalla tabella del database.
Se questo recordset ha dimensioni zero, ossia se la parola non è presente in tabella, allora mette in opera il semplice metodo save() del database e salva la parola (ponendo nel campo NUMERO il valore di 1).
Altrimenti se le dimensioni del recordset sono di 1 (e non possono essere altre perché se la parola è già presente non viene mai aggiunta una seconda volta) esegue il metodo incrementa, che ha per parametro la parola stessa trovata nell'editText, del database.

Ed ecco il metodo del database che incrementa il campo NUMERO di una parola già presente.
    public void incrementa(String parola){
        SQLiteDatabase db=this.getWritableDatabase();
        db.execSQL("UPDATE TABELLA SET NUMERO = NUMERO +1 WHERE TESTO = '" + parola + "'");

    }
Qui ho avuto qualche difficoltà dovuta al fatto che non padroneggio alla perfezione l'SQL.
Praticamente il significato è "Aggiorna, nella tabella TABELLA, il campo numero mettendo il suo stesso valore incrementato di uno, là dove il campo TESTO contiene la parola portata come parametro".
E funziona.

Cercare un elemento in un recordset, e ripasso di ArrayAdapter e CustomAdapter.

Che dobbiamo fare con quel database?

Dobbiamo archiviare le parole comprese dal sistema in un database, e a ogni parola pronunciata la parola compresa va confrontata con quelle presenti: se assenta va aggiunta.
Proviamo quindi inventandoci le parole...

Saggiare la lunghezza di un recordset avente come clausola WHERE la ricerca della parola.
Per fare questo devo creare un metodo del database di tipo query() avente per parametro la parola stessa, quindi se la lunghezza del recordset è pari a zero questo significa che non c'è, se è pari a 1, vuol dire che c'è.
Ho in mente il nucleo del progetto.
Ci provo...

    public Cursor query(String string){
        SQLiteDatabase db=this.getWritableDatabase();
        Cursor crs=db.rawQuery("SELECT * FROM TABELLA WHERE TEXT ="+string, null);
        return crs;
    }
Questo è il metodo.
Ora provo ad applicarlo. Prima però mi ricostruisco il "visore" del database, per il quale mi serve anche il metodo query senza parametri:
    public Cursor query(){
        SQLiteDatabase db=this.getWritableDatabase();
        Cursor crs=db.rawQuery("SELECT * FROM TABELLA", null);
        return crs;
    }
Ora li applichiamo...

Siamo giunti all'applicazione finale. Ecco...

        Cursor crsSelect=helper.query("fanculo");
        int numero=crsSelect.getCount();
        System.out.println(numero);
        if(numero==0) helper.save("fanculo");7
Questo codice (esclusa la terza riga) mi cerca un record in una tabella, e se non ce lo trova lo aggiunge.
Credo che si possa anche sintetizzare in un solo comando. Ci provo:
    public void addIfNotPresent(String string){
        Cursor crsSelect=helper.query(string);
        if(crsSelect.getCount()==0) helper.save(string);
    }
E la provo così:
addIfNotPresent("ciccia");
Vediamo...

Ottimo! L'ho sperimentato: aggiunge una voce se non è contenuta nella tabella.
Ma mi piacerebbe adesso fare una rilevazione statistica della frequenza con cui una voce, se presente, figura, e aumentare il numero.

A tale scopo rimetto le mani sul database.

    public Helper(Context context) {
        super(context, "mioDatabase.db", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE TABELLA(_ID INTEGER PRIMARY KEY, TESTO TEXT, NUMERO INTEGER)");
    }

    public Cursor query(String string){
        SQLiteDatabase db=this.getWritableDatabase();
        Cursor crs=db.rawQuery("SELECT * FROM TABELLA WHERE TESTO = '"+string+"'", null);
        return crs;
    }

    public Cursor query(){
        SQLiteDatabase db=this.getWritableDatabase();
        Cursor crs=db.rawQuery("SELECT * FROM TABELLA", null);
        return crs;
    }

    public void save(String parola){
        SQLiteDatabase db=this.getWritableDatabase();
        ContentValues values=new ContentValues();
        values.put("TESTO",parola);
        values.put("NUMERO",0);
        db.insert("TABELLA",null,values);
Ma ora devo visualizzare su una GridView, non più su una ListView.
Cosa cambia?

Cambia che la visualizzazione su GridView non mi sembra adeguata in quanto con l'ArrayAdapter resta sempre una visualizzazione sequenziale.
Forse meglio inflatare row con due textView in una delle quali mettere il campo testo mentre nell'altra i campo numero.

E come si inflata in questo modo più complesso?
Ed ecco il CustomAdapter!


Mentre ArrayAdapter è già una classe definita, per il CustomAdapter dobbiamo crearcela noi, una classe, quindi possiamo definire il costruttore.

Ma CustomAdapter estende ArrayAdapter? Non lo avrei mai pensato.
Dunque, che costruttore ha ArrayAdapter?
ArrayAdapter<String>(this,R.layout.row,R.id.textView,String[]). Giusto?
Sì, ma qui c'è la sintassi:
    public CustomAdapter(Context context, int textViewResourceId,
            Contatto [] objects) {
        super(context, textViewResourceId, objects);
    }
Sono solo tre parametri, che vengono rimandati al super, ossia alla classe ArrayAdapter.
Perché?
Rivediamo i costruttori di ArrayAdapter...

Credo che il costruttore che avevamo usato prima sia questo:
ArrayAdapter(Context context, int resource, int textViewResourceId, List objects)
in cui, a parte il context, vengono forniti un file XML di risorse, una TextView, un oggetto List.
Il costruttore usato in questa nuova sintassi, invece, è un Context, int, T[]
Faccio un elenco dei vari costruttori rimarcando i tipi dei parametri...
Context, int
Context, int, int
Context, int, T[]
Context, int, int, T[]
Context, int, List
Context, int, int, List
Quello di prima era a quattro parametri, di cui uno era un ArrayList, per cui apparteneva all'ultimo tipo; questo è a tre parametri di cui uno è un array, per cui appartiene al tipo del terzo.
Quando gli int sono due, il primo è relativo a un file XML, l'altro a una TextView.
Quando invece l'int è uno, è relativo soltanto al file XML, mentre quello alla TextView non è presente.
Dunque in questo caso si fornisce al costruttore della classe solo il layout e l'array di oggetti, o stringhe, o quello che sia.

Quindi viene overridato il metodo getView di ArrayAdapter.
Questo ha dei parametri fra cui position, che esprimerebbe la posizione nell'ambito dell'array di ogni elemento.
Quindi credo che debba venir chiamato a ogni elemento trovato nell'array che è stato fornito come parametro al costruttore.

Mi sembra che questo metodo, dati la posizione e una view già fornita dal sistema, trasformi questa view nella view da posizionare poi nella ListView/GridView. Normalmente, inflaterebbe il layout già fornito come parametro al costruttore e la textView, sempre fornita come parametro, quindi vi posizionerebbe l'elemento preso dall'array fornito come parametro.
Invece noi la manipoliamo. Facciamo ugualmente inflatare il layout, di questo individuiamo le textViews, e ci posizioniamo quello che riteniamo dell'elemento fornito nell'array.

Si può anche manipolare ArrayAdapter overridando in vario modo il suo metodo getView.


Ho molta confusione in testa.
Per prima cosa, ho poca esperienza circa gli array, le ArrayList, le LinkedList eccetera, e credo che sarà meglio dissipare i dubbi in materia.
Quelle maledette parentesi angolari mi creano un bel po' di confusione.

Fammi vedere se un oggetto può essere associato a un'ArrayList.
Ho una minuscola classe:
    class Elemento{
        public String parola;
        public int numero;
        public Elemento(String parola, int numero){
            this.parola=parola;
            this.numero=numero;
        }

    }
(senza setter e senza getter, così, semplice...)

Voglio costruire un array di elementi di questa classe.
Elemento[] arrayElementi= {new Elemento("Mario",3),new Elemento("Giuseppe",6)};
Ecco: ora posso usare un'ArrayList?
Vediamo:
        ArrayList<Elemento> lista=new ArrayList<Elemento>();
        lista.add(new Elemento("Mario",3));
        lista.add(new Elemento("Giuseppe",6)); 
Ecco: dunque io ho una duplice possibilità di costruire un array di elementi istanziati da una classe: mediante un semplice Array o mediante un'ArrayList.


Dunque posso usare ambedue i tipi di costruttore di ArrayAdapter, l'uno che impiega arrays e l'uno che impiega ArrayLists.

mercoledì 21 settembre 2016

Ripasso dei Menu e di ArrayAdapter.

Un po' di rallentamenti? Niente paura, adesso si recupera.

Bene, ho costruito un database nel quale provare il matching.
Ora costruisciti una visualizzazione, magari con un ListView o un GridView.
Così con l'occasione ripassi anche l'uso di questi controlli che non pratichiamo da tempo.

Ecco, ho ripassato i menu.
O meglio li sto ripassando.
Ho creato un file xml di nome "menu", mettendolo in una sottocartella di risorse che ho creato io, chiamata tanto per cambiare "menu".
La struttura non è poi così difficile: si tratta di mettervi semplicemente un item con due attributi: un id e un titolo. Il titolo è quello che apparirà nell'activity

Ecco la mia creazione:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu"
        android:title="Menu">

    </item>
</menu> 
Ora devo gestirlo in Java: creati sempre uno mnemonico, per ricordare da dove iniziare.
OnCreateOptionsMenu: si tratta della creazione di un menu di opzioni, e questo è sintetizzato nel nome del metodo.
Ricordiamo che è booleano.
Provo a riscriverlo:
    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        
        return true;
    }
Ecco. Non ho messaggi di errore.
Ora però devo aggiungerci l' "inflataggio" del menu a partire dalla risorsa.
Prima devo evocare un oggetto "menuInflater", quindi devo fargli inflatare la risorsa.
Provo:
    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        MenuInflater menuInflater=getMenuInflater();
        menuInflater.inflate(R.menu.menu,menu);
        return true;
    }
Una volta evocato, il menuInflater inflata, come è suo compito. E però devo specificare di inflatare la risorsa R.menu.menu dando la forma all'argomento di questo metodo.
Provo a cambiare i nomi per vedere se quel menu che devo inserire alla terza riga è inteso come quello che viene fornito come parametro.
    @Override
    public boolean onCreateOptionsMenu(Menu cicciobello){
        MenuInflater menuInflater=getMenuInflater();
        menuInflater.inflate(R.menu.menu,cicciobello);
        return true;
    }
Così funziona.
E se i due nomi sono diversi?
    @Override
    public boolean onCreateOptionsMenu(Menu cicciobello){
        MenuInflater menuInflater=getMenuInflater();
        menuInflater.inflate(R.menu.menu,cicciobrutto);
        return true;
    }
Ottengo un errore: cannot resolve symbol cicciobrutto.
Modifico il nome del parametro:
    @Override
    public boolean onCreateOptionsMenu(Menu cicciobrutto){
        MenuInflater menuInflater=getMenuInflater();
        menuInflater.inflate(R.menu.menu,cicciobrutto);
        return true;
    }
e sparisce ogni segnalazione di errore, segno che quel menu è riferito proprio al parametro!
Ora ho un menu che si vede, apparendo quello che ho designato come "title" del menu nel documento xml.
Passiamo a studiare come si utilizza un menu, ossia come si diano delle conseguenze al click sul menu stesso.

Inizia con onOptionsItemSelected, ossia "quando viene selezionato un item di opzioni".
La prima era onCreateOptionsMenu, la seconda onOptionsItemSelected. Il verbo va stavolta alla fine. E l'elemento viene chiamato "elemento di opzioni".
Il parametro è MenuItem, ossia l'elemento.
Di questo elemento va valutato l'Id.

Provo a metterlo per iscritto...

Lo scheletro va bene:
    @Override
    public  boolean onOptionsItemSelected(MenuItem item){
        
        return true;
    }
Ora dobbiamo selezionare l'Id dell'item fornito come parametro.
    @Override
    public  boolean onOptionsItemSelected(MenuItem item){

        switch(item.getItemId()){
            case R.id.menu:
                Toast.makeText(getApplicationContext(),"Menu",Toast.LENGTH_LONG).show();
        }
        return true;
    }
Sembra semplice.

E in effetti funziona.


Ora voglio costruirmi, comandato da Menu, una seconda activity in cui visualizzare il database.

Ecco: sull'adapter per le listView mi trovo sempre in difficoltà, ho studiato una forma complessa e ora non ricordo niente. Come fare?
Io suggerirei di andare a studiare intanto la forma più semplice, per poi successivamente approfondire.
Vai a cercare sulla documentazione ArrayAdapter o qualcosa di simile...

Ho provato la sintassi minimale dell'istanziazione, ma ottengo un messaggio di errore...
arrayAdapter =new ArrayAdapter<String>();
Cosa manca?

Ecco, vedi? Studiare prima i casi più semplici è più produttivo, perché per primi ti fa vedere i meccanismi fondamentali, quindi approfondendo con la loro conoscenza si può capire meglio le cose.
Nel costruttore di un ArrayAdapter, a parte il context, devi avere il layout nel quale ricercare la textview e la textview stessa.
A questi parametri fai seguire un array di stringhe che hai deciso di impiegare.
Con un database, sarà necessario mettere tutti gli elementi di un Cursor in un array di stringhe...

Proviamo con un array semplice.
Preparati pure il layout, velocemente.

        listView =(ListView) findViewById(R.id.listView);

        String[] array= {"lunedi","martedi","mercoledi","giovedi","venerdi","sabato","domenica"};
        
        arrayAdapter =new ArrayAdapter(getApplicationContext(),
                R.layout.row,R.id.textView3,array);
    }
Ecco: e ora, una volta preparato il layout, lo attribuiamo alla listView.

listView.setAdapter(arrayAdapter);
Proviamo...

dopo aver modificato la gestione dei menu in questo modo:
    @Override
    public  boolean onOptionsItemSelected(MenuItem item){

        switch(item.getItemId()){
            case R.id.menu:
                Intent intent=new Intent(MainActivity.this,Seconda.class);
                startActivity(intent);
        }
        return true;
    }
Vai!...

E infatti è riuscita.

Esercizio sullo scaling di un'immagine.

Adesso è il momento di ripetere lo zooming.
Purtroppo devo rivedermi il codice di sana pianta perché non me lo ricordo.

Quello che ti ricordi?
ScaleGestureDetector.simpleOn... Boh?

Ascoltami, Ciccio.
Innanzitutto devi definire una classe, che è una classe Listener.
Estende un altro listener.
E che cosa ascolta questo listener?

Ascolta onScale, ossia la scalatura.
Vediamo.
    private class QualunqueListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{

    }
Ho denominato a cazzo il listener per rimarcare che la parte fondamentale è ScaleGestureDetector.SimpleOnScaleGestureListener.

Non ha metodi da implementare, però.
Perlomeno non mi appare nulla da implementare, probabilmente perché non è un'interfaccia ma una classe.
Vediamo sulla documentazione ufficiale che accidenti è ScaleGestureDetector.SimpleOnScaleGestureListener.

Sì, ho detto giusto! Questo ScaleGestureDetector.SimpleOnScaleGestureListener è una classe, e non un'interfaccia come altri listeners.

Dunque il suo metodo onScale è già implementato, e non deve essere obbligatoriamente overridato, per questo non appare nessun avviso.
Dobbiamo overridarlo a memoria!

Ora, onScale ha come parametro un oggetto di tipo ScaleGestureDetector, ossia lo stesso oggetto di cui questo listener estende la classe SimpleOnScaleGestureListener.

Proviamo...

    private class QualunqueListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            
            return true;
        }

    }
E siamo qui.
Mandala!

    private class IlMioListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            
            return true;
        }
    }
Ecco. Ora ho una misura da rilevare da detector.
scaleFactor*=detector.getScaleFactor();
la misura getScaleFactor esprime il fattore scala, per il quale una grandezza va moltiplicata.
Cerchiamo di riscrivere il tutto anche se scaleFactor mi resta ancora non definito.
    private class Listener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            scaleFactor*=detector.getScaleFactor();
            return true;
        }
    }
Per il momento, ottimo.
Al posto del nome di fantasia, il programma originale cosa mette?
ScaleListener.
Bene. E' il nome della classe.


scaleFactor possiamo dichiararlo fra le variabili di istanza.
float scaleFactor=1.f;
E ora?
Una classe va istanziata. Dove si istanzia la classe ScaleListener o accidentichetipiglia... nomi di fantasia che le ho dato prima?
Vediamo...

In realtà si deve istanziare un oggetto ScaleGestureDetector.
Fra i parametri, esso porta un'istanza della classe ScaleListener o l'accidentechetipigliaListener.
Dichiarazione e istanziazione di ScaleGestureDetector.
ScaleGestureDetector mDetector;
e poi, nel costruttore:
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.facciadaculo);
        mImage.setBounds(0,0,300,300);
        mDetector=new ScaleGestureDetector(context,new Listener());
Ed ecco che all'evento onTouchEvent:
    @Override
    public boolean onTouchEvent(MotionEvent event){
        mDetector.onTouchEvent(event);


        return true;
    }
Dovrebbe funzionare...

Ovviamente, anche:
    @Override
    public void onDraw(Canvas canvas){
        canvas.save();
        canvas.scale(scaleFactor,scaleFactor);
        mImage.draw(canvas);
        canvas.restore();
    }
e vediamo...

Ho dimenticato invalidate():
    private class Listener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            scaleFactor*=detector.getScaleFactor();
            invalidate();
            return true;
        }
    }
che non sia questo il problema?

Sì, era quello: adesso funziona!

Adesso Mandala!

public class Immagine extends View{
    Context context;
    Drawable mImage;
    float scaleFactor=1.f;
    ScaleGestureDetector mDetector;
    float lastX, lastY, posX, posY;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.facciadaculo);
        mImage.setBounds(0,0,300,300);
        mDetector=new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        mDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.scale(scaleFactor,scaleFactor);
        mImage.draw(canvas);
        canvas.restore();
    }


    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            scaleFactor*=detector.getScaleFactor();
            invalidate();
            return true;
        }
    }

}


martedì 20 settembre 2016

Ragionamenti su J-Speaker

Per avere la certezza ragionevole che la parola detta da lui venga riconosciuta, bisogna proporre una rosa di parole fra le quali operare una scelta mediante il metodo contains di ArrayList.

Come costruire questa rosa di parole?
Attualmente lo sta usando in modo del tutto aspecifico, dicendo sempre "musica". Captando queste parole che lui dice, e memorizzando le parole capite dal sistema, si può creare una rosa di parole che il sistema capisce quando lui dice "musica" e dargliele per buone quando lui dice "musica". Raccolta una buona quantità di parole, si potrà restringere il campo delle specificità accettando per "musica" soltanto quando il sistema riconosce una di queste parole.

E se si procedesse in questo senso in modo del tutto automatico?

Esercitazione su immagine scrollabile.

Adesso, a titolo di esercitazione, scriviamo il più possibile, a mano libera, il codice di un'immagine scrollabile.

public class Immagine extends View{
    Context context;
    Drawable mImage;
    float lastX, lastY, posX, posY;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.facciadaculo);
        mImage.setBounds(0,0,300,300);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        int action=event.getAction() & MotionEvent.ACTION_MASK;

        switch(action){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;
        }
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        canvas.save();
        canvas.translate(posX,posY);
        mImage.draw(canvas);
        canvas.restore();
    }
}
Fatto praticamente di getto. E l'immagine si muove perfettamente!

Matching di un elemento in un ArrayList.

E adesso sarebbe il caso di completare l'opera e studiare come trovare un elemento in un ArrayList.
Meglio: come vedere se una parola si trova in una lista di tipo ArrayList.

Isoliamo il problema.
Creiamo un'ArrayList di stringhe con un elenco banale, i nomi dei sette nani.
        ArrayList lista=new ArrayList();
        lista.add("Gongolo");
        lista.add("Mammolo");
        lista.add("Pisolo");
        lista.add("Cucciolo");
        lista.add("Dotto");
        lista.add("Eolo");
        lista.add("Brontolo");
Ora ho cercato su Google le parole chiave "matches" e "ArrayList", e ho trovato alcuni costrutti, interessanti da ricordare, che metto in pratica subito. In particolare, il metodo contains di ArrayList è molto utile allo scopo.
public class MainActivity extends AppCompatActivity {

    Button button;
    ArrayList<String> lista;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button)findViewById(R.id.button);

        lista=new ArrayList<String>();
        lista.add("Gongolo");
        lista.add("Mammolo");
        lista.add("Pisolo");
        lista.add("Cucciolo");
        lista.add("Dotto");
        lista.add("Eolo");
        lista.add("Brontolo");

        button.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View view) {
                if(lista.contains("Cuccolo")){
                    Toast.makeText(getApplicationContext(),"CONTIENE!",Toast.LENGTH_LONG).show();
                }else{
                    Toast.makeText(getApplicationContext(),"NON CONTIENE!",Toast.LENGTH_LONG).show();
                }
            }
        });
    }
} 
Cambiando "Cucciolo" in "Cuccolo", la risposta cambia: nel primo caso è "CONTIENE" mentre nel secondo è "NON CONTIENE".
Un altro costrutto è questo:
public class MainActivity extends AppCompatActivity {

    Button button;
    ArrayList<String> lista;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button)findViewById(R.id.button);

        lista=new ArrayList<String>();
        lista.add("Gongolo");
        lista.add("Mammolo");
        lista.add("Pisolo");
        lista.add("Cucciolo");
        lista.add("Dotto");
        lista.add("Eolo");
        lista.add("Brontolo");

        button.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View view) {
                for(String s : lista){
                    if(s.equals("Cucciolo"))
                        Toast.makeText(getApplicationContext(),"CONTIENE!",Toast.LENGTH_LONG).show();
                    else
                        Toast.makeText(getApplicationContext(),"NON CONTIENE!",Toast.LENGTH_LONG).show();
                }
            }
        });
    }
} 
Con questo costrutto posso ottenere risultati ingannevoli: se predispongo, come in questo caso, una risposta anche in caso di esito negativo, ottengo il risultato dell'ultimo elemento della lista esaminato, quindi posso avere una segnalazione di "non presente" se solo l'ultimo elemento esaminato è diverso da quello che io sto cercando.
Predisponendo solo la risposta in caso positivo, invece, ottengo una risposta pertinente.

Ripasso del TTS ed esercitazione congiunta su riconoscimento e sintesi vocale.

Bene. Invece di rimandare, vieni a chiedere consiglio a me, e una volta che inizi a scrivere qui sopra, come stai facendo adesso, io ti riporterò alla razionalità conducendoti ad avere ottimi risultati.
L'ultima cosa in sospeso che avevamo, nello studiare lo Speech Recognizing, era il TTS, che credi di avere dimenticato, e adesso io ti propongo una sessione razionale sul TTS, che così, si spera, non dimenticherai mai più in un modo così importante.
Ricordo che la prima cosa nel TTS era una funzione chiamata onInit, ma non ne ricordo né lo scope né il tipo.
Prova a scriverla, magari tentando di dare voce al risultato della ArrayList che è ottenuta da un RecognizerIntent.

Bene, la scrivo nel programma.

No, nulla da fare. Sono costretto a rivederla. Mi sto sentendo male perché sento che il mio apprendimento sembra poco serio, e troppo mnemonico, senza rendermi conto dei costrutti fino in fondo.
Bene, allora partiremo da questa funzione per analizzare la cosa al meglio con gli strumenti della razionalità.

L'elemento fondamentale è istanziare un oggetto della classe TextToSpeech.
Distruggo tutto ciò che ho sul tavolo del laboratorio e costruisco.

public class MainActivity extends AppCompatActivity {

    Button button;
    TextToSpeech TTS;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button)findViewById(R.id.button);

    }

}
Questa è una dichiarazione.
Ora devo inizializzare o istanziare la variabile TTS di tipo TextToSpeech.
Lo schema è sempre quello:
TTS=new TextToSpeech();
Questo non basta, ottengo una segnalazione di errore.
Che errore?
Cannot resolve constructor.
Bene, a questo punto dobbiamo cercare i costruttori della classe TextToSpeech.
Ecco: il costruttore ha come parametri il context e un'istanza dell'interfaccia TextToSpeech.onInitListener.
Questa verrebbe chiamata quando il motore di sintesi vocale è stato inizializzato.
Ma si tratta di un'interfaccia che ha metodi che vanno implementati, per cui scrivere soltanto
TTS=new TextToSpeech(this,new TextToSpeech.OnInitListener());
dà un messaggio di errore che dice "OnInitListener è astratta, non può essere istanziata".
L'interfaccia ha metodi che devono essere implementati: con Alt e Invio in Android Studio si ottiene la guida all'implementazione del metodo onInit.
        TTS=new TextToSpeech(this, new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int i) {
                
            }
        });
Bene. Come si implementa il metodo?

Android Studio mi dà come parametro di onInit un int chiamato i, mentre Eclipse mi dava un int chiamato status.
Il concetto è lo stesso: bisogna in qualche modo vedere se questo status ha avuto successo, ossia se il motore è stato inizializzato correttamente, credo.
Esiste una costante di TextToSpeech che si chiama TextToSpeech.SUCCESS, e appunto la condizione essenziale per procedere a questo punto, trattandosi dell'avvio del motore del TextToSpeech, è che questo status, o "i" che dir si voglia, sia uguale a questa costante SUCCESS, ossia abbia avuto successo.
In tal caso si può procedere a settare il linguaggio come italiano.
La sintassi Locale.ITALIAN è la stessa che per il RecognizerIntent.
A questo punto forse dovrei essere in grado di fare il Mandala e riscrivere tutto.
        TTS=new TextToSpeech(this,new TextToSpeech.OnInitListener(){

            @Override
            public void onInit(int i) {
                if(i==TextToSpeech.SUCCESS){
                    TTS.setLanguage(Locale.ITALIAN);
                }
            }
        });
Esatto!
Saresti capace di implementare l'interfaccia in modo non anonimo e poi porre l'oggetto che implementa l'interfaccia come parametro nell'istanziazione di TTS?

Proviamo...

        TextToSpeech.OnInitListener onInitListener=new TextToSpeech.OnInitListener(){

            @Override
            public void onInit(int i) {
                if(i==TextToSpeech.SUCCESS){
                    TTS.setLanguage(Locale.ITALIAN);
                }
            }
        };
        
        TTS=new TextToSpeech(this,onInitListener);
Messaggi di errore non ne vedo. E' possibile, però, che vi sia qualche NullPointer dato che TTS non è ancora stato istanziato quando si setta il linguaggio? Non credo, però: è un listener non "gira" prima dell'istanziazione di TTS.
Proviamo.
Ci aggiungo il comando che lo fa parlare:
        TextToSpeech.OnInitListener onInitListener=new TextToSpeech.OnInitListener(){

            @Override
            public void onInit(int i) {
                if(i==TextToSpeech.SUCCESS){
                    TTS.setLanguage(Locale.ITALIAN);
                }
            }
        };

        TTS=new TextToSpeech(this,onInitListener);

        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                TTS.speak("Ciao, bello",TextToSpeech.QUEUE_FLUSH,null);
            }
        });
e vediamo.
(la sintassi del comando speak() va revisionata).

Okay, funziona anche con questa sintassi.

Cosa significa quel QUEUE_FLUSH, e quel null?
Non avere paura, si tratta solo di fare due excursus sulla documentazione ufficiale e, se non sufficiente, su altro materiale presente in rete.

Posso tornare a vederlo successivamente, fra qualche minuto?
E sia... hai trovato degli sviluppi da approfondire e hai paura. E se invece decidessimo di vincere la paura e farlo adesso? Se vuoi, accenditi la sigaretta adesso qui, dato che lo puoi fare, questo te lo concedo.
E sia...

Oltre al testo da dire, abbiamo due possibili modalità QUEUE MODE: una QUEUE_FLUSH, che, credo, significhi che le successive frasi vengono a sostituire quelle attuali nella fila per la sintesi e l'output vocale, mentre QUEUE_ADD significherebbe che vengono sommate a quelle attuali.
La cosa richiederebbe una verifica sperimentale per essere ben compresa.
La facciamo?
E dai, facciamola.

Sì! Premendo ripetutamente il tasto che "parla", se è attivo QUEUE_FLUSH la frase viene detta una volta sola, in quanto il nuovo testo rimpiazza il precedente, mentre se è attivo QUEUE_ADD la frase viene tenuta memorizzata insieme a tutte le successive, e vengono dette tutte una appresso all'altra.
Questa sintassi è deprecata nelle API più nuove.
La sintassi corretta sarebbe questa:
TTS.speak("Ciao, bello",TextToSpeech.QUEUE_FLUSH,null,null);
che però, dal momento in cui la mia applicazione si estende anche all'API 18, non mi viene concessa, in quanto la forma precedente risulta deprecata dall'API 21 in poi.

Possiamo accontentarci così. Non è bene poi lasciarsi prendere dalle smanie e voler approfondire eccessivamente e in modo convulso. E' un'altra faccia della paura, paura di considerare il proprio apprendimento poco valido.
Questa paura può condurti o ad evitare la programmazione per non incorrere in cose difficili che ti facciano autogiudicare male, o a voler approfondire tutto convulsamente e nevroticamente, per evitare di autogiudicarsi male.
Il necessario verrà.

Facciamo un Mandala finale.
public class MainActivity extends AppCompatActivity {

    Button button;
    TextToSpeech TTS;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button)findViewById(R.id.button);

        TTS=new TextToSpeech(this, new TextToSpeech.OnInitListener() {
            @Override
            public void onInit(int i) {
                if(i==TextToSpeech.SUCCESS){
                    TTS.setLanguage(Locale.ITALIAN);
                }
            }
        });

        button.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                TTS.speak("Questo è il testo",TextToSpeech.QUEUE_FLUSH,null);
            }
        });


    }

}
Funziona!
Ora un mega-Mandala che riconosce il testo parlato e lo replica con il sintetizzatore vocale.
public class MainActivity extends AppCompatActivity {

    Button button;
    TextToSpeech TTS;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button)findViewById(R.id.button);

        TTS=new TextToSpeech(this,new TextToSpeech.OnInitListener(){

            @Override
            public void onInit(int i) {
                if(i==TextToSpeech.SUCCESS){
                    TTS.setLanguage(Locale.ITALIAN);
                }
            }
        });

        button.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ITALIAN);
                intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"PARLA ORA");
                startActivityForResult(intent,0);
            }
        });


    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data){
        if(resultCode==RESULT_OK){
            ArrayList<String> lista=data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            String testo=lista.get(0);
            TTS.speak(testo,TextToSpeech.QUEUE_FLUSH,null);
        }
    }

}
Funziona alla perfezione!

Ripasso razionale e ponderato di Speech Recognition

Scriviamo un codice per il riconoscimento vocale.

Hai una paura terribile che, una volta ottenuti i successi che stai ottenendo, possa incontrare qualche grossa difficoltà che ti faccia rimangiare tutto e ti rimetta in crisi convincendoti che tu sia un cretino.
Per questo non riesci a programmare e rimandi sempre.
Razionalmente, ti posso dire la mia: è possibile, anzi probabile, che incontrerai difficoltà, ma in questo caso sezioneremo il problema in elementi sempre più piccoli e circostanziati e li analizzeremo, giungendo per vie razionali alla loro comprensione.
Dunque, inizia a scrivere il codice come lo ricordi, senza paura.
Ricordo che si iniziava con un intent.
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent
            }
        });
Dove si scrive il codice relativo al riconoscimento vocale?
Ecco: ti do un suggerimento: la classe chiave è RecognizerIntent. Su questa base puoi fare diversi tentativi.

Ecco: la più plausibile mi è sembrata questa, in quanto definisco l'azione in generale, ossia il riconoscimento vocale:
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
            }
        });
Confrontiamo con il codice già scritto:
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ITALIAN);
                intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"PARLA ORA");
                startActivityForResult(intent,0);
            }
        });
E infatti ci siamo! Questa l'hai indovinata sulla base di un procedimento logico.

Ora ci sono però dei putExtra al tuo intent dal quale parti.
Prova senza sbirciare.
Mi ricordavo vagamente un MODEL, e ho scelto questo:
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,)
            }
        });
che è incompleto, e non ricordo la successiva. Ma ha poco senso andare a memoria. Cerca adesso sulla documentazione una spiegazione razionale.

La riga completa è (sbirciamo):
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
            }
        });
Un suggerimento: la cosa più razionale è andare a vedere i metodi e le proprietà di RecognizerIntent.
La documentazione ufficiale non mi dice niente.
Vai a cercare altrove, sempre con le parole chiave EXTRA_LANGUAGE_MODEL e LANGUAGE_MODEL_FREE_FORM.

Suggerirei pure di rivedere la sintassi di intent.putExtra, che è fatta tipo chiave - contenuto.

Dunque EXTRA_LANGUAGE_MODEL sarebbe la chiave, mentre il contenuto sarebbe LANGUAGE_MODEL_FREE_FORM?
Possibile... Vediamo...

Ecco: quello che abbiamo capito è che l'elemento aggiunto all'Intent ha la chiava EXTRA_LANGUAGE_MODEL, e LANGUAGE_MODEL_FREE_FORM è solo uno dei valori possibili, mentre ce ne sarebbe un altro chiamato LANGUAGE_MODEL_WEB_SEARCH.
Cerchiamo di vedere quale sia la differenza fra i due, magari con una parola chiave "difference"?

Ecco, sembra che l'uno sia più efficace per la ricerca sul web ma che FREE_FORM sia adatto per la maggior parte dei casi.
Adesso facciamo un piccolo Mandala: distruggiamo tutto e ricominciamo a scrivere da capo.
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                
                    
                }
 
        });
Okay.


Ora proseguiamo: ci sono altri putExtra.
Buio totale!
Mi ricordo vagamente che come ultimo membro ci fosse la specificazione della lingua locale, ma ricordo solo quello...
Bene: allora vai a vedere, tenendo conto che se era l'ultimo membro, con tutta probabilità era il valore e non la chiave di qualcosa...

Ecco: la chiave era EXTRA_LANGUAGE, e il valore è specificato molto semplicemente come Locale.ITALIAN.
Cosa significa EXTRA_LANGUAGE?
Di nuovo alle proprietà e metodi di RecognizerIntent!

Ecco: EXTRA_LANGUAGE stabilisce una lingua extra rispetto a quella contemplata in getDefault() che sarà, credo un membro di Locale.
Praticamente, questa linea specifica alla mia App di recepire l'italiano.
E dunque la scrivo, per poi confrontare con quella già scritta.

intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ITALIAN);
Confrontiamola con questa, già scritta e funzionante:
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ITALIAN);
E' perfettamente uguale!!!
Ora veniamo all'ultimo putExtra: non ricordo assolutamente cosa fosse, se non qualcosa del tipo gestione del form che appare quando il programma invita a parlare, ma ne ho una reminiscenza molto vaga...

Bene, allora semplicemente vallo a vedere.

Ecco, ha a che fare con il PROMPT, e specifica la frase che appare sul prompt quando l'utente viene invitato a parlare.
E' EXTRA_PROMPT.
Provo dunque a scrivere:
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"Parla pure!");
Confrontiamo:
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"PARLA ORA");
E a parte la frase diversa, mi sembra perfettamente uguale!
Ora possiamo fare un Mandala e riscrivere il tutto a memoria.
                Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ITALIAN);
                intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"PARLA ORA");
Bene! Sembra che abbiamo gettato le basi per poterlo memorizzare!

Adesso vediamo i risultati.
Comincia ad inventare una risposta...
Dopo l'intent viene posto un startActivityForResult(intent,0), come per leggere le immagini dalla memoria.
Quello che avviene poi nel metodo onActivityResult è un po' più indaginoso.
Mi ricordo che veniva usata una ArrayList, e la cosa non mi è mai stata chiara. E' il momento di approfondire.
Mi rifaccio al codice già scritto in precedenza.
Questa, prima che memorizzata, va capita, per cui me la ricopio pedissequamente.
ArrayList<String> lista = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
C'è un'arrayList di stringhe, chiamata Lista. Questa viene ottenuta dal membro di RecognizerIntent chiamato EXTRA_RESULTS.
Andare a capire cosa sia mi sembra l'unica cosa razionale da fare.

Credo di aver capito che RecognitionIntent.EXTRA_RESULTS sia un array di risultati plausibili dal riconoscimento vocale.
Questo mi dà agio per una possibile modifica rivoluzionaria dell'applicazione, ma ci penseremo dopo.
E quel metodo getStringArrayListExtra?
Quello appartiene a Intent, e sarà saggio andarselo a guardare, con calma...

Ecco, in pratica getStringArrayListExtra sarebbe il retrievare di una lista di String dagli Extra di un intent.
Solo che in questo caso non ce l'ho messo io ma è arrivato dal riconoscimento vocale.
Se è vero che questi risultati sono elencati con intervallo di confidenza, allora il n. 0 è solo il più probabile, ma ce ne sono anche altri, che possono, come pensavo prima, essere usati per un riconoscimento vocale più elastico!
Bisogna fare una prova, non senza aver prima studiato cosa si fa di questa ArrayList.
Niente, nella mia App viene messa in una TextView, ma si può farsene ciò che si vuole.

domenica 18 settembre 2016

Zooming di un'immagine.

Questo codice identifica meglio le costanti per gli eventi su e giù del secondo puntatore:
    public boolean onTouchEvent(MotionEvent event){
        int action=event.getAction() & MotionEvent.ACTION_MASK;
        switch(action){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                System.out.println("SECONDO PUNTATORE GIU'");
                break;
            case MotionEvent.ACTION_POINTER_UP:
                System.out.println("SECONDO PUNTATORE SU");

        }

        return true;
    }

Zooming.

Questa è la costruzione di un listener:
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{

        @Override
        public boolean onScale(ScaleGestureDetector detector){
            mScaleFactor=detector.getScaleFactor();
            invalidate();
            return true;
        }
    }
E' da vedere come viene istanziata questa classe.
Viene dichiarata una variabile di tipo ScaleGestureDetector.
private ScaleGestureDetector mScaleDetector;
Ah, ecco: nel contesto del costruttore viene messa un'istanza anonima della classe ScaleListener.
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
Ossia si crea un nuovo ScaleGestureDetector con quel determinato listener.

Questo ScaleGestureDetector viene posto nell'evento onTouchEvent dove viene evocato il suo metodo onTouchEvent.

Ricominciamo...

No, ho dimenticato questi importanti dettagli:
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener{
        @Override
        public boolean onScale(ScaleGestureDetector detector){
            mScaleFactor*=detector.getScaleFactor();
            invalidate();
            return true;
        }
    }
Alla fine sono riuscito a zoomarla.

Mandala!
Cancello e riscrivo.

Ho fatto tre mandala.
Dovrei averla memorizzata abbastanza, ma sono necessari altri mandala successivi.

Mascheramento di MotionEvent.getAction() in funzione del secondo puntatore del touchscreen.

Iniziamo ora a trattare dei puntatori sul touchscreen.
Questo argomento l'ho trattato già, ma mi risulta ostico e credo di averlo dimenticato in gran parte.
Se io aggiungo al codice per il movimento delle immagini un comando che mi metta a video il codice del comando:
    @Override
    public boolean onTouchEvent(MotionEvent event){
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;
        }
        System.out.println(event.getAction());
        return true;
    }
All'evento Down ottengo il numero 0, al move il 2 e all'up (qui non contemplato) ottengo 1.

Ed ecco la novità: quando abbasso il secondo dito ottengo 261, mentre quando lo sollevo ottengo 262.

In binario:
ACTION_DOWN: 0000 0000 0000 0000
ACTION_UP: 0000 0000 0000 0001
ACTION_MOVE: 0000 0000 0000 0010

Secondo puntatore giù: 0000 0001 0000 0101
Secondo puntatore su: 0000 0001 0000 0110
Che cosa si ottiene col mascheramento?

La sintassi sarebbe event.getAction() & MotionEvent.ACTION_MASK.

Se MotionEvent.ACTION_MASK è 0xFF, ossia 255, questo mascheramento dovrebbe dare:
ACTION_DOWN: 0 & 0xFF ->0;
ACTION_UP: 1 & 0xFF ->1;
ACTION_MOVE: 2 & 0xFF->2;
fin qui non cambia niente.
Proviamo ora

Secondo puntatore giù: 0000 0001 0000 0101 & 0xFF= 0000 0000 0000 0101 = 5 Secondo puntatore su: 0000 0001 0000 0110 & 0xFF= 0000 0000 0000 0110 = 6

Praticamente il mascheramento elimina il byte superiore.

Verifichiamo con il codice:
    public boolean onTouchEvent(MotionEvent event){
        int action=event.getAction() & MotionEvent.ACTION_MASK;
        switch(action){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;
        }
        System.out.println(action);
        return true;
    }
Sì: adesso il codice del secondo puntatore giù e su diventa rispettivamente 5 e 6. Come da mia previsione.

Quindi, finché si resta con un solo puntatore non ha senso mascherare MotionEvent.getAction(), mentre la cosa diventa necessaria con due puntatori, perché il codice del secondo puntatore si estende, essendo superiore a 255, anche su un byte alto, che va eliminato mediante il mascheramento AND con 0xFF.

sabato 17 settembre 2016

Esercizi immagine scrollabile su View caricabile dalla memoria e con i tag Exif.

Esercizio:
Scrivere codice per un'immagine scrollabile su View con la possibilità di caricare un'immagine dalla memoria.
public class Immagine extends View {

    Context context;
    Drawable mImage;
    float lastX, lastY, posX, posY;
    public Immagine(Context context) {
        super(context);
        this.context=context;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
        }
        return true;
    }

    @Override
    public void onDraw(rCanvas canvas){
        super.onDraw(canvas);
        canvas.translate(posX,posY);
        canvas.save();
        try{
            mImage.draw(canvas);
        }catch(Exception e){}
        canvas.restore();
    }

    public void setImage(Bitmap bitmap){
        mImage=new BitmapDrawable(getResources(),bitmap);
        mImage.setBounds(0,0,bitmap.getWidth(),bitmap.getHeight());
        invalidate();
    }


Main:
public class MainActivity extends AppCompatActivity {
    RelativeLayout mainLayout;
    Immagine immagine;
    Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        button=(Button)findViewById(R.id.button);
        mainLayout.addView(immagine);
        button.setId(View.generateViewId());
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)immagine.getLayoutParams();
        params.addRule(RelativeLayout.BELOW,button.getId());
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent();
                intent.setAction(Intent.ACTION_PICK);
                intent.setData(Uri.parse("content://media/external/images/media"));
                startActivityForResult(intent,0);
            }
        });

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data){
        if(resultCode==RESULT_OK){
            Cursor crs=getContentResolver().query(data.getData(),null,null,null,null);
            crs.moveToFirst();
            int colonna=crs.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            String path=crs.getString(colonna);
            Bitmap bmp= BitmapFactory.decodeFile(path);
            immagine.setImage(bmp);
        }
    }
}
Buona l'idea di non caricare nessuna immagine precedente gestendo l'errore generato dalla sua mancanza.


Esercizio n.2: correggere l'orientamento sbagliato delle immagini per mezzo dei tag Exif.
public class MainActivity extends AppCompatActivity {
    RelativeLayout mainLayout;
    Immagine immagine;
    Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        immagine=new Immagine(this);
        button=(Button)findViewById(R.id.button);
        mainLayout.addView(immagine);
        button.setId(View.generateViewId());
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)immagine.getLayoutParams();
        params.addRule(RelativeLayout.BELOW,button.getId());
        button.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View view) {
                Intent intent=new Intent();
                intent.setAction(Intent.ACTION_PICK);
                intent.setData(Uri.parse("content://media/external/images/media"));
                startActivityForResult(intent,0);
            }
        });

    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data){
        if(resultCode==RESULT_OK){
            Cursor crs=getContentResolver().query(data.getData(),null,null,null,null);
            crs.moveToFirst();
            int colonna=crs.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
            String path=crs.getString(colonna);

            try {
                ExifInterface exifInterface =new ExifInterface(path);
                int orientation=exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL);
                int rotate=0;
                switch(orientation){
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        rotate=90;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        rotate=180;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        rotate=270;
                        break;
                }

                Matrix matrix=new Matrix();
                matrix.postRotate(rotate);

                Bitmap b= BitmapFactory.decodeFile(path);

                Bitmap bmp=Bitmap.createBitmap(b,0,0,b.getWidth(),b.getHeight(),matrix,true);
                immagine.setImage(bmp);

            } catch (IOException e) {
                e.printStackTrace();
            }


        }
    }
}
Perfetto! Qui si gioca tutto sulla classe che istanzia Immagine.

Esercizi di scrittura di codice per un'immagine scrollabile su view.

Esercizio:
Scrivere il codice di un'immagine scrollabile nell'ambito di una View.
public class Immagine extends View {
    Context context;
    Drawable mImage;
    float lastX,lastY,posX,posY;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.bona);
        mImage.setBounds(0,0,200,200);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){

        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
                break;

        }
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(posX,posY);
        mImage.draw(canvas);
        canvas.restore();
    }
}
Ho dimenticato di sottrarre a getX e getY dell'evento Move il valore di lastX e lastY, ossia delle coordinate dell'ultima posizione.

o Ripetere l'esercizio.
Voglio provare a lasciare il codice solo alla creazione dell'immagine, per vedere cosa ottengo. Secondo me la mancanza dell'evento onDraw dovrebbe lasciarmi la view vuota:
public class Immagine extends View {

    Context context;
    Drawable mImage;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.bona);
        mImage.setBounds(0,0,300,300);
    }
    
}
Sì, è così!

Ora aggiungo onDraw senza il codice per il movimento:
public class Immagine extends View {

    Context context;
    Drawable mImage;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.bona);
        mImage.setBounds(0,0,300,300);
    }
    
    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        mImage.draw(canvas);
        canvas.restore();
    }

}
Ecco, con questo codice l'immagine appare.
Però, ovviamente, non si sposta.

Ora riscrivo il codice completo.
public class Immagine extends View {

    Context context;
    Drawable mImage;
    float lastX,lastY,posX,posY;
    public Immagine(Context context) {
        super(context);
        this.context=context;
        mImage=getResources().getDrawable(R.drawable.bona);
        mImage.setBounds(0,0,300,300);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event){
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX=event.getX();
                lastY=event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                posX+=event.getX()-lastX;
                posY+=event.getY()-lastY;
                invalidate();
                lastX=event.getX();
                lastY=event.getY();
        }
        return true;
    }

    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(posX,posY);
        mImage.draw(canvas);
        canvas.restore();
    }
}
Ho dimenticato di aggiungere il metodo translate del canvas, subito ricordato e corretto.

giovedì 15 settembre 2016

ExifInterface

ExifInterface è la classe da cui partire.

Si fa qualcosa tipo ExifInterface exifInterface=new ExifInterface(path)? Sì. Perlomeno questo lo abbiamo capito.

Poi si mette in opera il metodo che restituisce un attributo, che dovrebbe essere l'orientamento dell'immagine.
L'orientamento è un tipo int.
Questo può servire come mnemonico per ricordare che il metodo di ExifInterface è ExifInterface.getAttributeInt.

I parametri di questo metodo mi risultano oscuri.
Ecco: la firma del metodo sarebbe questa:
int getAttributeInt (String tag, int defaultValue)
Praticamente rileva il valore del tag exif con un valore di default nel caso in cui quello non sia disponibile.
Cosa è un tag exif?

Exif sta per EXchangeable Image file Format.
Non sarebbe altro che un formato JPEG o altro con l'aggiunta di dati che sarebbero appunto i TAG.
Fra questi dati ci sono ANCHE quelli relativi all'orientamento dell'immagine.
Appunto, se un'immagine viene salvata come formato Exif, viene incorporata anche un'informazione sull'orientamento dell'immagine, che se non viene tenuta in considerazione può lasciare che l'immagine si presenti rovesciata a seconda di come è stata scattata.
Dunque si deve verificare il tag TAG_ORIENTATION, che è un dato numerico, lasciando che in assenza di questo TAG venga letto il TAG ORIENTATION_NORMAL.


Adesso ho le idee più chiare.
Adesso devo studiarmi ScaleType.
Non vorrei ricorrere alle opzioni di BitmapFactory per ridimensionare l'immagine alle dimensioni della ImageView relativa.
Un momento! Ho impiegato un po' di tempo per decifrare, e ho trovato degli spunti nel codice di J-Communicator.
Posso provare con setBounds, che sarebbe un metodo di Drawable.
Vediamo se è applicabile anche alla bitmap.
No, sembra solo di Drawable. Ho provato a farlo con una bitmap e non trovo questo metodo nell'intelliSense.

C'è modo di piazzare l'immagine nei limiti di una ImageView?
Vediamo: io ho una ImageView delle dimensioni di 200.


        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        imageView=new ImageView(this);
        imageView.setBackgroundColor(Color.WHITE);
        imageView.setImageResource(R.drawable.genio);
        button=new Button(this);
        mainLayout.addView(imageView);
        mainLayout.addView(button);
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)imageView.getLayoutParams();
        params.width=200;
        params.height=200;
        params.topMargin=100;
Ecco. Ora provo a piazzarci la bitmap...

Sembra che venga bene! Ben contenuta nell'immagine.

Ora cambio le dimensioni dell'immagine.

        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)imageView.getLayoutParams();
        params.width=300;
        params.height=300;
        params.topMargin=100;

Vediamo...

Viene ancora bene!
Quindi possiamo lasciare questo codice.

Ora provo con un'immagine verticale...
che viene ancora bene, ben posizionata nell'ImageView.


Dunque non c'è bisogno di ridimensionare l'immagine?

Ora però mi voglio occupare di nuovo di traslazione e zooming dell'immagine.
Ho già dimenticato questo argomento, che avevo peraltro visto qualche mese fa.


Per il momento riscriviamo il codice per leggere i tag Exif, a memoria.

            try {
                ExifInterface exifInterface=new ExifInterface(path);
                int orientation=exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,ExifInterface.ORIENTATION_NORMAL);
                int rotate=0;
                switch(orientation){
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        rotate=90;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        rotate=180;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        rotate=270;
                        break;
                }
                Matrix matrix=new Matrix();
                matrix.postRotate(rotate);
                Bitmap bmp=Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
                imageView.setImageBitmap(bmp);
            } catch (IOException e) {
                e.printStackTrace();
            }
Il risultato c'è ed è perfetto.

lunedì 12 settembre 2016

Salvataggio codice conversione dp pixel, layer-list, shapes.

Sono riuscito a realizzare delle shapes proporzionate con l'immagine definita programmaticamente, grazie alla formula che converte i dp in pixels.
Opto però per una costruzione programmatica dell'immagine anziché statica come nelle shapes, perché ci sono maggiori possibilità di customizzazione.
Per il momento, salvo il codice per eventuali successivi ripassi e approfondimenti:
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);



        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);

        LayoutInflater inflater=(LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View view=(View)inflater.inflate(R.layout.inflatabile,null);
        mainLayout.addView(view);
        RelativeLayout.LayoutParams viewParams=(RelativeLayout.LayoutParams)view.getLayoutParams();
        viewParams.height=170;
        viewParams.width=170;




    }

    float dpToPixels(float dp){
        DisplayMetrics metrics= Resources.getSystem().getDisplayMetrics();
        float px=dp*(metrics.densityDpi/160f);
        return Math.round(px);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:top="12dp">
        <shape
            android:shape="rectangle">

            <solid android:color="#fe888888" />
        </shape>
    </item>
    <item
        android:right="60dp">
        <shape
            android:shape="rectangle">
            <solid android:color="#fe888888" />
        </shape>
    </item>
    <item
        android:top="17dp"
        android:bottom="5dp"
        android:left="5dp"
        android:right="5dp">
        <shape
            android:shape="rectangle">

            <solid android:color="#feffff88" />
        </shape>
    </item>
    <item
        android:right="65dp"
        android:left="5dp"
        android:bottom="5dp"
        android:top="5dp">
        <shape
            android:shape="rectangle">
            <solid android:color="#feffff88" />
        </shape>
    </item>
</layer-list>
...e il layout che ha per sfondo la layer list:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/folder">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ETICHETTA"
        android:id="@+id/textView"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_marginTop="17dp"
        android:layout_marginLeft="5dp"/>
</RelativeLayout> 
E riprendo a disegnare a runtime col canvas...

venerdì 9 settembre 2016

Aggiunta di elementi al mio layout immagine di J-Communicator, con problemi di formato

Ho fatto un po' di progressi nel gestire il caricamento di cartelle.
Questo è il codice:
do{
            boolean isCategoria=false;
            if(crs.getString(4).equals("categoria")) isCategoria=true;

            if(isCategoria==false) layout=new NormalLayout(this);
            else layout=new CategoryLayout(this);

            RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
            params.topMargin=crs.getInt(6);
            params.leftMargin=crs.getInt(5);
            params.width=150;
            params.height=150;
            layout.setLayoutParams(params);


            //CODICE PER L'AGGIUNTA DELL'ETICHETTA:
            TextView etichView=new TextView(this);
            etichView.setText("ETICHETTA");
            layout.addView(etichView);
            RelativeLayout.LayoutParams etichParams=(RelativeLayout.LayoutParams)etichView.getLayoutParams();
            etichParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
            etichParams.topMargin=(int)(layout.getLayoutParams().height*Parametri.percentuale_altezza_linguetta);
            etichView.setTypeface(null, Typeface.BOLD);
            System.out.println(layout.getLayoutParams().height);
            etichView.setLayoutParams(etichParams);



            mainLayout.addView(layout);
        } while(crs.moveToNext());
Ho imparato anche il metodo addRule di LayoutParams.

Ho impostato un modulo Parametri per tenere sotto controllo le misure della cartella in un unico punto senza dover andare a spulciare in mezzo al codice.

L'aggiunta dell'immagine si rivela problematica.
Come fare ad accodare l'immagine all'etichetta?
Come fare se l'etichetta deve essere più lunga a causa di un testo più lungo?
Il problema non è facile affatto... Come gestire, in sostanza, lo spazio?
Trovo un attributo in XML. Potrà servire?

Allo stesso modo, l'immagine andrà costretta in uno spazio limitato.
Mi trovo piuttosto in difficoltà...
Mi isolo un esperimento sul mio tavolo da lavoro.
public class MainActivity extends AppCompatActivity {

    RelativeLayout mainLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);
        TextView textView=new TextView(this);
        textView.setText("PRIMA");
        mainLayout.addView(textView);


        TextView textView2=new TextView(this);
        textView2.setText("SECONDA");
        mainLayout.addView(textView2);
    }
}
Vediamola...

Ecco:



Le due TextView vengono sovrapposte.


Ora voglio trovare il modo di porle una sotto l'altra.
Forse sarà meglio isolarle dallo sfondo colorandole...

        TextView textView=new TextView(this);
        textView.setText("PRIMA");
        textView.setBackgroundColor(Color.YELLOW);
        mainLayout.addView(textView);


        TextView textView2=new TextView(this);
        textView2.setText("SECONDA");
        textView2.setBackgroundColor(Color.GREEN);
        mainLayout.addView(textView2);
Così facendo, si vede soltanto quella apposta per ultima.
Ora cominciamo a cercare di far qualcosa coi parametri.

        TextView textView=new TextView(this);
        textView.setText("PRIMA");
        textView.setBackgroundColor(Color.YELLOW);
        mainLayout.addView(textView);
        


        TextView textView2=new TextView(this);
        textView2.setText("SECONDA");
        textView2.setBackgroundColor(Color.GREEN);
        mainLayout.addView(textView2);
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)textView2.getLayoutParams();
        params.addRule(RelativeLayout.BELOW,textView.getId());
Vediamo se funziona ponendomi textView2 sotto textView.

Non funziona.
Invece questo codice...
        TextView textView2=new TextView(this);
        textView2.setText("SECONDA");
        textView2.setBackgroundColor(Color.GREEN);
        mainLayout.addView(textView2);
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)textView2.getLayoutParams();
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
funziona, mettendo la seconda textView all'estremità inferiore del layout.

Ho trovato la soluzione:
        TextView textView=new TextView(this);
        textView.setText("PRIMA");
        textView.setBackgroundColor(Color.YELLOW);
        mainLayout.addView(textView);
        textView.setId(View.generateViewId());



        TextView textView2=new TextView(this);
        textView2.setText("SECONDA");
        textView2.setBackgroundColor(Color.GREEN);
        mainLayout.addView(textView2);
        RelativeLayout.LayoutParams params=(RelativeLayout.LayoutParams)textView2.getLayoutParams();
        params.addRule(RelativeLayout.BELOW,textView.getId());
In questo modo il BELOW funziona.

Ora cerco di applicarlo al mio J-Communicator.
            //CODICE PER L'AGGIUNTA DELL'ETICHETTA:
            TextView etichView=new TextView(this);
            etichView.setText("ETICHETTA");
            layout.addView(etichView);
            RelativeLayout.LayoutParams etichParams=(RelativeLayout.LayoutParams)etichView.getLayoutParams();
            etichParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
            etichParams.topMargin=(int)(layout.getLayoutParams().height*Parametri.percentuale_altezza_linguetta);
            etichView.setTypeface(null, Typeface.BOLD);
            System.out.println(layout.getLayoutParams().height);
            etichView.setLayoutParams(etichParams);
            etichView.setId(View.generateViewId());

            //CODICE PER L'AGGIUNTA DELL'IMMAGINE
            ImageView imageView=new ImageView(this);
            Bitmap bitmap=Funzioni.StringToBitmap(crs.getString(3));

            layout.addView(imageView);
            RelativeLayout.LayoutParams imgParams=(RelativeLayout.LayoutParams) imageView.getLayoutParams();
            imgParams.addRule(RelativeLayout.BELOW,etichView.getId());
            imageView.setLayoutParams(imgParams);
E vediamo...

            ImageView imageView=new ImageView(this);
            Bitmap bitmap=Funzioni.StringToBitmap(crs.getString(3));
            imageView.setImageBitmap(bitmap);
            layout.addView(imageView);
            RelativeLayout.LayoutParams imgParams=(RelativeLayout.LayoutParams) imageView.getLayoutParams();
            imgParams.addRule(RelativeLayout.BELOW,etichView.getId());
r> Questo funziona.
Ora devo vedere di adattare le misure.

Non è facile.
In OnStart non riesco ad avere le misure della TextView, e nemmeno in OnResume.
Leggo che c'è qualche possibilità intercettando l'evento onWindowFocusChange ma mi chiedo se questo venga prima o dopo onStart e se una variabile modificata in questo evento mostri la sua modifica quindi anche in onStart.
Faccio una prova. Creo una variabile di istanza e la modifico per poi retrievarne il valore in onStart.
String variabileSpia;

.....

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        variabileSpia="PRIMO VALORE";
Ora monto su il metodo:
    @Override
    public void onWindowFocusChanged(boolean hasFocus){
        variabileSpia="SECONDO VALORE";
    }
e vado a leggere in onStart...
    @Override
    protected void onStart(){
        super.onStart();
        System.out.println("VARIABILE SPIA "+variabileSpia);
        mLucchettiChangeListener.onLucchettiChange(boolLucchettoSinistroAperto,boolLucchettoDestroAperto);
        refreshScreen();

    }
Vediamo...

No:
I/System.out: VARIABILE SPIA PRIMO VALORE
Vediamo in onResume...
    @Override
    protected void onResume(){
        super.onResume();
        System.out.println("VARIABILE SPIA "+variabileSpia);
    }
Nemmeno...
I/System.out: VARIABILE SPIA PRIMO VALORE
Bene... non so come fare.

giovedì 8 settembre 2016

J-Communicator: creazione di layout differenziati per le immagini normali e le immagini-categoria

Eravamo rimasti allo scaricamento del database per formare le immagini inserite nella schermata principale.
In questo metodo, LeggiDaDatabase, dopo il refresh della schermata ad opera di refreshScreen() (che potrei benissimo incorporare in questo metodo, e nel quale l'esecuzione di LeggiDaDatabase è circondata da un try...catch per motivi che ora mi sfuggono), usa in questa mia ricostruzione una view:
    private void leggiDaDatabase(String categoria){
        Cursor crs=helper.query(categoria);
        do{
            View view=new View(this);
            RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
            params.topMargin=crs.getInt(6);
            params.leftMargin=crs.getInt(5);
            view.setLayoutParams(params);
            view.setBackgroundResource(R.drawable.forma);
            mainLayout.addView(view);
        } while(crs.moveToNext());
    }
Bene, io adesso voglio usare una classe che estenda il LinearLayout. Nella versione originale era usato un LinearLayout, mentre adesso userei due classi derivate di LinearLayout, diverse a seconda che il record letto in questo momento riguardi un'immagine normale o un'immagine-categoria.
L'informazione sul tipo la ottengo dal campo TIPO, che è il numero 4 del record.

Ci provo...

Intanto mi devo preparare le due classi derivate da LinearLayout, come ho fatto precedentemente nel mio esperimento.
Questa è la creazione di NormalLayout, per le immagini semplici:
public class normalLayout extends LinearLayout {
    Paint paint;
    Context context;
    public normalLayout(Context context) {
        super(context);
        this.context=context;
        paint=new Paint();
        
    }
    
    @Override 
    protected void onDraw(Canvas canvas){
        int bordo=5;
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);
        canvas.drawRect(0,0,getWidth(),getHeight(),paint);
        paint.setColor(Color.WHITE);
        canvas.drawRect(bordo,bordo,getWidth()-bordo,getHeight()-bordo,paint);
    }
}
Vediamo se funziona o se ho trascurato qualcosa...

Avevo trascurato setWillNotDraw(false) oltre a non aver tolto qualche comando precedente un po' campato in aria nel metodo LeggiDaDatabase, e ora funziona.
Ecco il codice definitivo di NormalLayout:
public class NormalLayout extends LinearLayout {
    Paint paint;
    Context context;
    public NormalLayout(Context context) {
        super(context);
        setWillNotDraw(false);
        this.context=context;
        paint=new Paint();

    }

    @Override
    protected void onDraw(Canvas canvas){
        int bordo=5;
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);
        canvas.drawRect(0,0,getWidth(),getHeight(),paint);
        paint.setColor(Color.WHITE);
        canvas.drawRect(bordo,bordo,getWidth()-bordo,getHeight()-bordo,paint);
    }
}


Ora però devo impostare anche un codice per CategoryLayout.
Ci provo...

public class CategoryLayout extends LinearLayout{
    Paint paint;
    Context context;
    public CategoryLayout(Context context) {
        super(context);
        this.context = context;
        paint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        int bordo=5;
    
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);
        
        //rettangolo principale
        canvas.drawRect(0,30,getWidth(),getHeight(),paint);

        //linguetta
        canvas.drawRect(0,0,(float)(getWidth()*.4),100,paint);


        paint.setColor(Color.WHITE);

        //rettangolo principale interno
        canvas.drawRect(bordo,30+bordo,getWidth()-bordo,getHeight()-bordo,paint);

        //linguetta interna
        canvas.drawRect(bordo,bordo,(float)(getWidth()*.4-bordo),100,paint);

    }
}
Sperimentiamolo, attribuendolo senza alcun criterio...

Sì, mi sono venute fuori delle "cartelle", ma un po' sproporzionate.
Evidentemente non ho reso in percentuale le cose nel modo giusto.
Ci studio un po'...

Ecco, per il momento sono riuscito a creare i rettangoli esterni della cartella:
public class CategoryLayout extends LinearLayout{
    Paint paint;
    Context context;
    public CategoryLayout(Context context) {
        super(context);
        setWillNotDraw(false);
        this.context = context;
        paint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        int bordo=5;

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);

        //qui devo stabilire la percentuale delle misure della linguetta
        //rispetto a quelle della cartella.
        //altezza linguetta=20%, larghezza linguetta=30% mi sembra ragionevole.
        //iniziamo con le cartelle esterne.

        //cartella rettangolo esterno: le misure iniziali sono 0 e altezza 20%:
        float cartTop=(float)(getHeight()*.2);
        canvas.drawRect(0,cartTop,getWidth(),getHeight(),paint);

        //linguetta rettangolo esterno: le misure iniziali zono 0, 0
        //mentre quelle finali sono larghezza 30% e arbitraria.ma possiamo anche eguagliarla
        // a cartTop per evitare che con cartelle molto piccole possa sporgere dall'altra parte
        float lingRight=(float)(getWidth()*.5);
        float lingBottom=cartTop;
        canvas.drawRect(0,0,lingRight,lingBottom,paint);
    }
}
Adesso diamo fondo ai rettangoli interni:

Ecco il codice completo:
public class CategoryLayout extends LinearLayout{
    Paint paint;
    Context context;
    public CategoryLayout(Context context) {
        super(context);
        setWillNotDraw(false);
        this.context = context;
        paint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        int bordo=5;

        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);

        //rettangoli esterni
        float cartTop=(float)(getHeight()*.15);
        canvas.drawRect(0,cartTop,getWidth(),getHeight(),paint);
        float lingRight=(float)(getWidth()*.5);
        float lingBottom=cartTop;
        canvas.drawRect(0,0,lingRight,lingBottom,paint);

        //rettangoli interni
        paint.setColor(Color.WHITE);
        canvas.drawRect(bordo,cartTop+bordo,getWidth()-bordo,getHeight()-bordo,paint);
        canvas.drawRect(bordo,bordo,lingRight-bordo,lingBottom+bordo,paint);
    }
}
Abbiamo alcuni punti sui quali possiamo far leva per determinare l'aspetto delle nostre cartelle.
  • bordo, che sarebbe lo spessore del bordo desiderato;
  • cartTop, in cui variando il numero si agisce sulla percentuale in altezza della linguetta rispetto alla cartella;
  • lingRight, in cui variando il numero si agisce sulla percentuale in larghezza della linguetta rispetto alla cartella.
in questo modo sarà più facile rimettere le mani sul codice qualora non dovessi essere soddisfatto dell'aspetto e delle dimensioni della cartella.
Ecco il risultato grafico:



Ora ci giocherello un po': agendo su quei parametri, per assurdo, voglio una linguetta alta la metà dell'altezza totale del layout:



Ora riporto la linguetta al 15% di altezza, ma la voglio al 10% di larghezza rispetto alla cartella anziché al 50%:



Ci è rimasto solo il bordo!

Portiamole al 30%, una via di mezzo...

Ecco... Adesso dovremo usare l'uno o l'altro dei layout, NormalLayout o CategoryLayout, a seconda che il TIPO sia normale o categoria.

Realizzazione di un'immagine di "folder" su un LinearLayout (in funzione di J-Communicator)

Provo a disegnare un semplice rettangolo bordato, con la tecnica della figura più piccola sovrapposta a quella più grande.
Dunque quelle elucubrazioni geometriche che ho messo in opera prima possono essere eliminate. Ho scelto la soluzione più complicata credendo che con un'immagine più complessa di "cartella" e non di semplice rettangolo ci sarebbero state complicazioni, e anche perché ero indeciso se fare un FILL o uno STROKE.
Opto per il FILL.

Ecco, dopo varie prove sono riuscito a realizzare l'immagine di una cartella con bordo grigio su sfondo nero:

MyLayout:
public class MyLayout extends LinearLayout {

    Paint paint;
    Context context;

    public MyLayout(Context context) {
        super(context);
        this.context=context;
        setWillNotDraw(false);
        paint = new Paint();
    }

    @Override
    protected void onDraw(Canvas canvas){
        int bw=10;
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GRAY);

        //rettangolo principale
        canvas.drawRect(0,30,getWidth(),getHeight(),paint);

        //linguetta
        canvas.drawRect(0,0,90,100,paint);


        paint.setColor(Color.WHITE);

        //rettangolo principale interno
        canvas.drawRect(0+bw,30+bw,getWidth()-bw,getHeight()-bw,paint);

        //linguetta interna
        canvas.drawRect(0+bw,0+bw,90-bw,100,paint);
    }

}


MainActivity:
public class MainActivity extends AppCompatActivity {


    RelativeLayout mainLayout;
    MyLayout myLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainLayout=(RelativeLayout)findViewById(R.id.mainLayout);

        myLayout=new MyLayout(this);
        mainLayout.addView(myLayout);

        RelativeLayout.LayoutParams params=
                new RelativeLayout.LayoutParams(
                        RelativeLayout.LayoutParams.WRAP_CONTENT,
                        RelativeLayout.LayoutParams.WRAP_CONTENT);

        params.width=200;
        params.height=200;
        params.leftMargin=100;
        params.topMargin=100;
        myLayout.setLayoutParams(params);
    }

}
Sarebbe il caso di usare, nel codice di MyLayout, valori delle dimensioni dei rettangoli in percentuale, in modo da adattarsi automaticamente a ogni dimensione data al layout.
Si può consentire la scelta delle dimensioni del bordo volta per volta (e sarà meglio metterlo come parametro da passare al costruttore), mentre per le dimensioni relative della linguetta rispetto alla larghezza della pagina, andrebbe posto un valore percentuale.