Освой программирование играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

ListView. Списки со значками

Расширим возможности списка, добавив в него значки. Такой список будет выглядеть гораздо интереснее для пользователя. За основу возьмём пример обычного списка с именами котов.

Подготовим разметку для отдельного элемента списка, который должен уметь отображать текст и значок (файл res/layout/list_item.xml):


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

    <ImageView
        android:id="@+id/image_view_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/text_view_cat_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

Теперь переходим к java-коду. Мы воспользуемся ListActivity и код станет очень коротким.


package ru.alexanderklimov.listviewicon;

import ...

public class ListViewWithIconsActivity extends ListActivity {

    // определяем массив типа String
    private final String[] mCatNames = new String[] { "Рыжик", "Барсик", "Мурзик",
            "Мурка", "Васька", "Томасина", "Кристина", "Пушок", "Дымка",
            "Кузя", "Китти", "Масяня", "Симба" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setListAdapter(new ArrayAdapter<>(this, R.layout.list_item,
                R.id.textViewCatName, mCatNames));
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        // super.onListItemClick(l, v, position, id);
        String selection = l.getItemAtPosition(position).toString();
        Toast.makeText(this, selection, Toast.LENGTH_LONG).show();
    }
}

В методе setListAdapter() мы сообщаем, что разметку для отдельного элемента списка надо брать из R.layout.list_item, текст размещать в компоненте с идентификатором R.id.text_view_cat_name, а массив текстов брать из mCatNames.

Запустив пример, мы получим готовое приложение, состоящее из списка с одинаковыми значками. Вы можете, как и прежде, щёлкать по пунктам и прокручивать список.

Список ListView со значками

В некоторых случаях этого вполне достаточно. А в некоторых не совсем. Представьте себе, что мы хотим создать список, состоящий из знаков зодиака. Естественно, у каждого знака зодиака должнен быть свой значок.

Вся магия содержится в методе адаптера getView(). Мы можем задать для каждого элемента списка свой значок. Добавим в проект значки для знаков зодиака. Для простоты в этом примере я покажу изменение одного знака зодиака Лев, который относится к семейству кошачьих. Разметка может оставаться прежней, только поменяйте идентификатор у TextView (по коду вы должны сами понять, на какой именно).


package ru.alexanderklimov.listviewicon;

import android.app.ListActivity;
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.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class ListViewWithIconsActivity extends ListActivity {

    // Строка, которую мы выводим в список
    private String[] mZodiacSigns = { "Овен", "Телец", "Близнецы", "Рак", "Лев", "Дева",
            "Весы", "Скорпион", "Стрелец", "Козерог", "Водолей", "Рыбы" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // меняем на свой адаптер
        setListAdapter(new ZodiacAdapter(this, R.layout.list_item, mZodiacSigns));
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        // super.onListItemClick(l, v, position, id);
        String selection = l.getItemAtPosition(position).toString();
        Toast.makeText(this, selection, Toast.LENGTH_LONG).show();
    }

    // Пишем свой класс-адаптер
    private class ZodiacAdapter extends ArrayAdapter<String> {

        ZodiacAdapter(Context context, int textViewResourceId,
                      String[] objects) {
            super(context, textViewResourceId, objects);

        }

        @NonNull
        @Override
        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
            LayoutInflater inflater = getLayoutInflater();
            View row = inflater.inflate(R.layout.list_item, parent, false);
            TextView label = (TextView) row.findViewById(R.id.text_view_zodiac);
            label.setText(mZodiacSigns[position]);
            ImageView iconImageView = (ImageView) row.findViewById(R.id.image_view_icon);
            // Если текст содержит кота, то выводим значок Льва (лев - это кот!)
            if (mZodiacSigns[position].equalsIgnoreCase("Лев")) {
                iconImageView.setImageResource(R.drawable.lion);
            } else {
                iconImageView.setImageResource(R.mipmap.ic_launcher);
            }
            return row;
        }
    }
}

Список ListView со значками

По такому же принципу вы можете усложнять пример, например, вычислять чётные и нечётные элементы списка и выводить значки, раскрашивать зеброй и т.д.

Немного информации о классе LayoutInflater. Класс берёт данные из XML-файла разметки и превращает их в реальные компоненты, которые становятся частью списка. Вам надо только указать источник для конвертации через метод inflate().

После этого, система понимает, где искать отдельные компоненты разметки. Мы вызываем знакомый нам метод findViewById(), только он уже относится не к активности, а к корневому элементу разметки. Поэтому мы вызываем метод из row.

Но чего-то не хватает в этом примере. Давайте под каждым названием будем выводить еще дату, которая относится к знаку зодиака. Стандартный подход здесь не сработает. Во-первых, у нас будет два отдельных массива строк (названия и даты), а стандартный адаптер работает с одним массивом. Выход - опять написать собственный адаптер под нашу задачу. Заодно подключим и остальные значки.

Сначала изменим макет для списка.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="5dp" >

    <ImageView
        android:id="@+id/imageViewIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

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

        <TextView
            android:id="@+id/textViewSign"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FF0000"
            android:textSize="20sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/textViewDate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:textStyle="italic" />
    </LinearLayout>

</LinearLayout>

Теперь напишем свой адаптер и внесём изменения в код


package ru.alexanderklimov.listview;

import android.app.ListActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends ListActivity {

    private String[] mZodiacSigns = {"Овен", "Телец", "Близнецы", "Рак", "Лев", "Дева",
            "Весы", "Скорпион", "Стрелец", "Козерог", "Водолей", "Рыбы"};

    private String[] mDates = {"21 марта - 20 апреля", "21 апреля - 20 мая",
            "21 мая - 21 июня", "22 июня - 22 июля", "23 июля - 23 августа",
            "24 августа - 23 сентября", "24 сентября - 23 октября",
            "24 октября - 22 ноября", "23 ноября - 21 декабря",
            "22 декабря - 20 января", "21 января - 20 февраля",
            "21 февраля - 20 марта"};

    int[] mImageIds = {R.drawable.aries, R.drawable.taurus,
            R.drawable.gemini, R.drawable.cancer, R.drawable.lion,
            R.drawable.virgo, R.drawable.libra, R.drawable.scorpio,
            R.drawable.sagittarius, R.drawable.capricorn, R.drawable.aquarius,
            R.drawable.pisces};

    private ZodiacAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mAdapter = new ZodiacAdapter(this);
        setListAdapter(mAdapter);
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        String selection = mAdapter.getString(position);
        Toast.makeText(this, selection, Toast.LENGTH_LONG).show();
    }

    private class ZodiacAdapter extends BaseAdapter {
        private LayoutInflater mLayoutInflater;

        ZodiacAdapter(Context context) {
            mLayoutInflater = LayoutInflater.from(context);
        }

        @Override
        public int getCount() {
            return mZodiacSigns.length;
        }

        @Override
        public Object getItem(int position) {
            return position;
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null)
                convertView = mLayoutInflater.inflate(R.layout.list_item, null);

            ImageView image = (ImageView) convertView.findViewById(R.id.image_view_icon);
            image.setImageResource(mImageIds[position]);

            TextView signTextView = (TextView) convertView.findViewById(R.id.text_view_zodiac);
            signTextView.setText(mZodiacSigns[position]);

            TextView dateTextView = (TextView) convertView.findViewById(R.id.text_view_date);
            dateTextView.setText(mDates[position]);

            return convertView;
        }

        String getString(int position) {
            return mZodiacSigns[position] + " (" + mDates[position] + ")";
        }
    }
}

Готовый результат выглядит гораздо лучше предыдущих примеров.

Список ListView со значками

Здесь сразу хочу обратить внимание на сравнение if (convertView == null). Для данного примера это не принципиально, так как у нас список состоит всего из 12 элементов. Если счёт элементов пойдёт на сотни, то это уже вызовет проблемы с памятью. Вы просите систему создать более сотни новых компонентов, да ещё с картинками. Но в реальности вы на экране не видите сто элементов списка одновременно, а только часть из них. И был предложен хитрый механизм - не создавать новую строку списка при прокрутке, а использовать уже созданный ранее элемент и подгружать в него данные. Таким образом мы создаём элементы только для первых строк списка, видимых на экране. Допустим, экран вмещает десять строк списка, а остальные 90 с лишним элементов подгрузятся динамически. Это самый простой, но эффективный способ повышения производительности. Более продвинутые способы повышения производительности смотрите в статье Производительность при работе с ListView.

Полосатый ListView

Коты бывают полосатыми. Давайте и ListView сделаем полосатым. Делается элементарно, всего одной строчкой кода.

В метод getView() из предыдущего примера вставляем:


convertView.setBackgroundColor((position & 1) == 1 ? Color.WHITE : Color.LTGRAY);

Список ListView со значками

При использовании этого приёма не будет срабатывать фокус на элементах списка.

Upd: Евгений Галкин прислал ссылку на пример, где проблема с фокусом решена - android listview alternate row color BUT with default cursor selection - Stack Overflow

В этом уроке рассматривается более продвинутый вариант полосатой разметки.

При подготовке материала использовалась статья Программирование для android - Списки

Напоследок советую вам использовать в качестве корневого элемента RelativeLayout вместо LinearLayout - первый вариант использует намного меньше ресурсов и постоянно пропагандируется в документации.

Реклама