Pull to refresh

Создаем ListView с Context Action Bar как в новом Gmail

Reading time 7 min
Views 23K


Что хотим получить


Сделать плавно работающий список с возможностью выделения рядов как кликом на иконку ряда, так и долгим нажатием на него. Также, дабы выделение не пропало даром, мы должны дать возможность пользователю производить некие действия с выделенными объектами.



Создание разметки для списка


Итак, в первую очередь нам потребуется создать layout, в котором будет находиться список, выглядит он так:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context=".MainActivity">

    <ListView
            android:id="@android:id/list"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"/>

</RelativeLayout>

Кроме множества падингов, любезно созданных для меня android developer studio, здесь ничего интересного нет. Разве что напомню: android:id/list — это специально выделенный ID, который знают ListActivity и ListFragment.

Далее создадим layout, который будет являться каждым рядом в нашем ListView:
<?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:orientation="vertical"
        android:background="?android:attr/activatedBackgroundIndicator">

    <View
            android:id="@+id/item_image"
            android:layout_width="45dp"
            android:layout_height="45dp"
            android:layout_margin="5dp"
            android:padding="10dp"/>

    <TextView
            android:id="@+id/item_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/item_image"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="10dp"
            android:text="TextView"
            android:layout_gravity="center_vertical|left"
            android:textAppearance="?android:textAppearanceListItem">
    </TextView>
</RelativeLayout>

Здесь у нас TextView, расположенный справа от View. На месте View обычно картинка, но в данном примере мы будем просто отображать случайно сгенерированный цвет.
Также обратите внимание на android:background="?android:attr/activatedBackgroundIndicator" в свойствах layout. Без этого атрибута не будет виден визуальный эффект выделения.

Создаем ListView и заполняем его


Сразу приведу код activity, а затем поясню его:
public class MainActivity extends ListActivity {
    public static final String TAG = "FOR_HABR";
    private Random randomGenerator = new Random();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //генерируем размер нашего листа
        int size = getRandomNumber(200);

        ListView listView = getListView();
        //Создаем инстанс нашего кастомного адаптера
        Integer[] colors = generateListOfColors(size).toArray(new Integer[0]);
        ArrayAdapter<Integer> customAdapter = new CustomAdapter(this, R.layout.list_view_row, colors, listView);
        listView.setAdapter(customAdapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        Log.d(TAG, "onCreateOptionsMenu");
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    //Генерируем список из случайных цветов
    private List<Integer> generateListOfColors(int size) {
        List<Integer> result = new ArrayList<Integer>();
        for (int i = 0; i < size; i++) {
            result.add(generateRandomColor());
        }
        return result;
    }

    //Генерируем случайный цвет
    private int generateRandomColor() {
        return Color.rgb(getRandomNumber(256), getRandomNumber(256), getRandomNumber(256));
    }

    private int getRandomNumber(int maxValue) {
        return randomGenerator.nextInt(maxValue);
    }

}

Здесь мы первым делом находим по ID layout, в котором будет размещен наш лист, и назначаем его контентом этого activity. Для того чтобы заполнить ListView информацией, мы в начале генерируем список из чисел и передаем его в конструктор нашего кастомного адаптера.
Адаптер — это мост между данными и отображением, в нем мы подсказываем системе, где и какой компонент каждого ряда списка мы хотели бы видеть. Вот код нашего адаптера:
public class CustomAdapter extends ArrayAdapter<Integer> {
    private ListView listView;

    public CustomAdapter(Context context, int textViewResourceId, Integer[] objects, ListView listView) {
        super(context, textViewResourceId, objects);
        this.listView = listView;
    }

    static class ViewHolder {
        TextView text;
        View indicator;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        Integer color = getItem(position);

        View rowView = convertView;

        //Небольшая оптимизация, которая позволяет повторно использовать объекты
        if (rowView == null) {
            LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater();
            rowView = inflater.inflate(R.layout.list_view_row, parent, false);
            ViewHolder h = new ViewHolder();
            h.text = (TextView) rowView.findViewById(R.id.item_text);
            h.indicator = rowView.findViewById(R.id.item_image);
            rowView.setTag(h);
        }

        ViewHolder h = (ViewHolder) rowView.getTag();

        h.text.setText("#" + Integer.toHexString(color).replaceFirst("ff", ""));
        h.indicator.setBackgroundColor(color);

        return rowView;
    }
}

Мы переписываем всего один метод из родительского класса — метод getView. Этот метод вызывается каждый раз, когда в поле зрения пользователя появляется новый ряд списка. Соответственно, из него мы должны вернуть объект View именно в том виде, в котором желаем отобразить его пользователю.
Здесь мы применяем популярный шаблон, который позволяет нам немного (до 15%) увеличить производительность ListView за счет повторного использования объектов. Более подробно прочитать про этот шаблон можно здесь.

На этом этапе можно запустить приложении, и мы увидим список с цветами, но, конечно, без какого-либо интерактива.

Добавляем возможность выбора ряда


Для этого требуется сделать следующие:
//Указываем ListView, что мы хотим режим с мультивыделением
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
//Указываем обработчик такого режима
listView.setMultiChoiceModeListener(new MultiChoiceImpl(listView));


Обработчик выглядит так:
public class MultiChoiceImpl implements AbsListView.MultiChoiceModeListener {
    private AbsListView listView;

    public MultiChoiceImpl(AbsListView listView) {
        this.listView = listView;
    }

    @Override
    //Метод вызывается при любом изменении состояния выделения рядов
    public void onItemCheckedStateChanged(ActionMode actionMode, int i, long l, boolean b) {
        Log.d(MainActivity.TAG, "onItemCheckedStateChanged");
        int selectedCount = listView.getCheckedItemCount();
        //Добавим количество выделенных рядов в Context Action Bar
        setSubtitle(actionMode, selectedCount);
    }

    @Override
    //Здесь надуваем CAB из xml
    public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
        Log.d(MainActivity.TAG, "onCreateActionMode");
        MenuInflater inflater = actionMode.getMenuInflater();
        inflater.inflate(R.menu.context_menu, menu);
        return true;
    }

    @Override
    public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
        Log.d(MainActivity.TAG, "onPrepareActionMode");
        return false;
    }

    @Override
   //Вызывается при клике на любой Item из СAB
    public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
        String text = "Action - " + menuItem.getTitle() + " ; Selected items: " + getSelectedFiles();
        Toast.makeText(listView.getContext(), text , Toast.LENGTH_LONG).show();
        return false;
    }

    @Override
    public void onDestroyActionMode(ActionMode actionMode) {
        Log.d(MainActivity.TAG, "onDestroyActionMode");
    }

    private void setSubtitle(ActionMode mode, int selectedCount) {
        switch (selectedCount) {
            case 0:
                mode.setSubtitle(null);
                break;
            default:
                mode.setTitle(String.valueOf(selectedCount));
                break;
        }
    }

    private List<String> getSelectedFiles() {
        List<String> selectedFiles = new ArrayList<String>();

        SparseBooleanArray sparseBooleanArray = listView.getCheckedItemPositions();
        for (int i = 0; i < sparseBooleanArray.size(); i++) {
            if (sparseBooleanArray.valueAt(i)) {
                Integer selectedItem = (Integer) listView.getItemAtPosition(sparseBooleanArray.keyAt(i));
                selectedFiles.add("#" + Integer.toHexString(selectedItem).replaceFirst("ff", ""));
            }
        }
        return selectedFiles;
    }
}

Вероятно, вы заметили, что здесь мы надуваем новый Action Bar (context_menu). Он выглядит так:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
            android:id="@+id/cab_add"
            android:icon="@android:drawable/ic_menu_add"
            android:orderInCategory="1"
            android:showAsAction="ifRoom"
            android:title="add"/>
    <item
            android:id="@+id/cab_share"
            android:icon="@android:drawable/ic_menu_share"
            android:orderInCategory="1"
            android:showAsAction="ifRoom"
            android:title="share"/>
</menu>

Итак, теперь по порядку. В ListView мы устанавливаем специальный режим выделения — CHOICE_MODE_MULTIPLE_MODAL, который подразумевает, что мы подсунем ListView класс, реализующий интерфейс AbsListView.MultiChoiceModeListener. В этом классе мы реализуем методы, в которых указываем, что хотим получить на событие выделения, клика по item в CAB или на уничтожение CAB.

Теперь осталось добавить возможность выделения ряда по клику на иконку. Для этого требуется навесить на нее в методе getView OnClickListener:
h.indicator.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        selectRow(v);
    }

    private void selectRow(View v) {
        listView.setItemChecked(position, !isItemChecked(position));
    }

    private boolean isItemChecked(int pos) {
        SparseBooleanArray sparseBooleanArray = listView.getCheckedItemPositions();
        return sparseBooleanArray.get(pos);
    }
});

Здесь, в случае если ряд уже выделен, снимаем выделение, в противном случае выделяем.

На этом все. Полный код примера можно найти у меня на BitBucket.

UPD. Практически все, что использовано в этой статье было добавлено в API 11, а кое-что даже в 14. Так, что если хотите совместимости с API < 11, советую посмотреть сюда.
Так же есть замечательная разработка товарища HoloEverywhere
Tags:
Hubs:
+23
Comments 6
Comments Comments 6

Articles