niedziela, 3 czerwca 2012

Android custom spinner

Przygody z Androidem doprowadziły mnie do momentu, w którym zmuszony zostałem zabawić się w tworzenie własnych elementów wyświetlanej listy. Oczywiście ani stworzenie własnego layoutu, ani adaptera nie jest czymś wielce trudnym... ale potrafi dostarczyć pewnych niemiłych doznań podczas procesu tworzenia. Generalnie problemem, który sprawił, że ten wpis powstał było zadanie generowania innego layoutu (w widoku dropdown) dla elementu obecnie zaznaczonego przez spinner.

Przykład, który tu zostanie przedstawiony można pobrać z chomika: (link)


Layouty

Na potrzeby tego przykładu przygotowałem 3 layouty - jeden główny, dla aplikacji, oraz 2 używane przez spinnera:

main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello"
        android:gravity="center_horizontal"
        android:layout_marginBottom="20dip" />
    <Spinner
        android:id="@+id/spinner"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
spinner_view1.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:id="@+id/spinner_title"
        android:text="EXAMPLE TEXT"
        android:gravity="center_horizontal"/>
</LinearLayout>
spinner_view2.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" >

    <ImageView
        android:layout_weight="1"
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:id="@+id/spinner_icon"
        android:src="@drawable/ic_launcher"/>
    <TextView 
        android:layout_weight="1"
        android:layout_width="0dip"
        android:layout_height="fill_parent"
        android:id="@+id/spinner_title"
        android:text="EXAMPLE TEXT"
        android:gravity="center"/>
</LinearLayout>
Jak widać nie ma tu wielkich szaleństw, ot Spinner, TextView i ImageView :)

 Activity

Po stworzeniu layoutów i generalnie wiedząc co chcemy wyświetlać czas przenieść się do aktywności, która tym wszystkim będzie zarządzać.
package runaurufu.example.spinner;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;

public class AndroidSpinnerActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // list of elements displayed in spinner
        String[] objects = new String[]{ "Object 1", "Object 2", "Object 3", "Object 4"};
        
        Spinner spin = (Spinner)findViewById(R.id.spinner);
        // here we use default android simple_spinner_item so we do not need to worry about
        // display while in normal (not in dropdown) view
        spin.setAdapter(new CustomAdapter(spin, this, android.R.layout.simple_spinner_item, objects));

    }
    
    
    public class CustomAdapter extends ArrayAdapter<String>
    {
  Spinner spinner = null;
  String[] headers = null;
  
     public CustomAdapter(Spinner spinner, Context context, int textViewResourceId, String[] objects) {
      super(context, textViewResourceId, objects);
      // this will allow us to use spinner view inside of our code
      this.spinner = spinner;
      // this will allow us to set proper values for views on specified positions
      headers = objects;
     }

     @Override
     public View getDropDownView(int position, View convertView,ViewGroup parent) {
      LayoutInflater inflater=getLayoutInflater();
      
      View row;
      
      // here we check if currently selected position match currently rendered position
      if(spinner.getSelectedItemPosition() == position)
      {
       row=inflater.inflate(R.layout.spinner_view2, parent, false);
             
       // be sure to find views using inflated view and not default Activity view
             TextView tv = (TextView)row.findViewById(R.id.spinner_title);
             tv.setText(headers[position]);
             
             ImageView iv = (ImageView)row.findViewById(R.id.spinner_icon);
             iv.setImageResource(R.drawable.ic_launcher);
      }
      else
      {
       row=inflater.inflate(R.layout.spinner_view1, parent, false);
       
       TextView tv = (TextView)row.findViewById(R.id.spinner_title);
             tv.setText(headers[position]);
      }

         return row;
     }

     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
      /* this makes adapter to use default layout posted to it in constructor 
       * in that example it will use android.R.layout.simple_spinner_item
       */
      return super.getView(position, convertView, parent);
     }
    }
}

Jak widać cudów niewidów nie ma, a sam kod jest raczej krótszy niż dłuższy. Z ciekawostek warto wspomnieć o tym, że generalnie stworzony spinner używa 3 layouty dla generowania obiektów - standardowy android.R.layout.simple_spinner_item dla elementów wyświetlanych na kontrolce oraz spinner_view1/spinner_view2 dla elementów renderowanych wewnątrz dropView. Takie wykorzystanie adapterów może nie jest jakimś wielkim popisem umiejętności, ale wbrew pozorom nie jest oczywiste.

Drugą istotną rzeczą, która w sumie stanowi clue tego wpisu jest sam warunek określający czy generujemy widok zaznaczony, czy też nie.
if(spinner.getSelectedItemPosition() == position)
Tutaj przyznam szczerze, że mimo wielu prób nie udało mi się tego rozwiązać w sposób inny niż poprzez przesłanie do adaptera obiektu, na którym będzie on pracował. Jak znajdę lepszy sposób to z pewnością wpis ten poprawię, a póki co zostaje mi się cieszyć z tego co mam :)

Rezultat

Aplikacja po uruchomieniu i kliknięciu w spinnera na emulatorze prezentuje się nadzwyczaj dobrze. Oczywiście można tutaj o wiele więcej ostylować, dodać kolorowe tła, różne radiobuttony, checkboxy i inne cuda nieznane jaskiniowcom - jednak generalna zasada działania pozostaje taka sama, a cel zrealizowany - element obecnie wybrany jest wyróżniony.



Plik projektu jak zawsze można pobrać z mojego chomika: (link)

Brak komentarzy:

Prześlij komentarz