Sunday, May 1, 2011

Выбор элементов в кастомном ArrayAdapter

Недавно попался баг, из-за которого не происходил выбор айтема в кастомном ArrayAdapter.

Актовность ListActivity вместе со всем своим волшебством в большинстве случаев хорошо справляется с задачей представления списков меню:


Однако временами возникает потребность показать пользователю более информативный список, чем показан на картинке выше. В таком случае нужно немного модернизировать магию построения списка.

В нашем случае программа должна была в списке показывать не только название каждого элемента, но и дополнительную информацию, а также предоставлять пользователю без лишних телодвижений включить-выключить любой пункт меню посредством стандартного чекбокса. На экране все это колдунство выглядело приблезительно так:


Т.е. вместо стандартного текста каждый элемент этого кастомного меню содержит а) текстбокс для названия (большой шрифт), б) текстбокс для описания (мелкий щрифт) и ц) чекбокс для увлекательного чеканья.

В коде эта картинка звучит следующим образом:
public class MyAdapter extends ArrayAdapter<MyDataClass>
{
    public MyAdapter(final Context context, final List<MyDataClass> items)
    {
        // Конструктор
        super(context, R.layout.sensorslist, items);
    }

   @Override
    public View getView(final int position,
                final View convertView,
                final ViewGroup parent)
    {
        // Инициализация пунктов меню, каждого в отдельности
        TextView title = (TextView) v.findViewById(R.id.sensor_text);
        TextView details = (TextView) v.findViewById(R.id.sensor_comments);
        CheckBox enabled = (CheckBox) v.findViewById(R.id.sensor_enabled);
        // И потом делаем с ними что хотим...
    }
}
NB. Метод getView вызывается автоматически классом-родителем при создании пунктов меню, причем вызывается для каждого пункта, а не для всего списка сразу.

В конструктор помимо контекста передается список с кастомным классом данных (о нем еще ниже будет). К созданному классу MyAdapter применяется свой лэйаут, который определяет расположение элементов внутри каждого пункта меню:
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView android:id="@+id/sensor_text"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:lines="1"
            android:scrollHorizontally="true"
            android:ellipsize="end"
            android:paddingLeft="2sp"
            android:paddingTop="2sp"
            android:textSize="18sp"
            android:textStyle="bold"
            android:shadowColor="#90909090"
            android:shadowDx="1.0"
            android:shadowDy="1.0"
            android:shadowRadius="1.0"/>

        <TextView android:id="@+id/sensor_comments"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="12sp"
            android:textColor="#FF808080"
            android:paddingLeft="2sp"
            android:paddingTop="2sp"/>
    </LinearLayout>

    <CheckBox android:id="@+id/sensor_enabled"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true"
        android:paddingRight="5sp"/>
</RelativeLayout>

И, наконец, вызывается все это волшебство в нужной активности с помощью нехитрых махинаций волшебным посохом:
public class ExtendedListActivity extends ListActivity
{
    @Override
    public void onCreate(final Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        
        List<MyDataClass> datas = new List<MyDataClass>();
        
        // Здесь мы совершаем необходимые нам махинации с классами данных,
        // добавляя созданные инстансы в список
        for (int i = 0; i < 10; i++)
        {
            MyDataClass cl = new MyDataClass();
            datas.add(cl)
        }

        MyAdapter adapter = new MyAdapter(this.getApplicationContext(), datas);

        setListAdapter(adapter);
        
        // А теперь создадим слушателя для тыков в меню
        ListView v = this.getListView();
        v.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(final AdapterView<?> arg0,
                    final View view,
                    final int clicked,
                    final long position)
            {
                // Получаем номер пункта меню и исполняем
                // свой танец
            }
        });
    }    
}

Все, вроде, в порядке вещей, однако не слишком подкованные гипнотезеры часто упускают из вида один совсем неочевидный (к сожалению, довольно обычное дело для дядьки Андроида) момент, и в итоге это может приводить к непонятному хаосу в погоне за невидимым багом. Дело в том, что активность ListActivity, которая ответственна за показ элементов в виде списка, напрочь отказывается запускать OnItemClickListener (отросток, который помогает отлавливать события нажатий на пункты меню) ни за какие деньги. Принципиальная.

Чтобы нерадивые гипнотезеры и дальше паниковали под действием своего же хаоса, незаметно добавляем строчку в лэйаут, описывающий чекбокс:
<CheckBox android:id="@+id/sensor_enabled"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true"
        android:focusable="false"
        android:paddingRight="5sp"/>

И молча хихикаем в сторонке, зная, что Андроид не позволяет выбирать из списка айтемы, элементы которых могут быть фокусируемыми (focusable).