Списки, разделённые на секции, встречаются довольно часто как в системных приложениях, так и в сторонних. Но, как ни странно, платформа не предлагает способов для их реализации «из коробки», в отличие от iOS.
Мне известны два подхода к реализации заголовков списков:
- В вёрстке каждого элемента списка прописывается заголовок, видимость которого будет определяться условием «является ли данный элемент первым в секции?»
- Отдельно верстается заголовок для списка, а в методе Adapter#getView идёт выбор: создавать элемент типа «заголовок», либо элемент типа «обычная строка»
Первый способ проще с точки зрения написания кода: не надо задумываться о выборе типа ячейки списка. Но влечёт за собой более «тяжёлый», с точки зрениния производительности, интерфейс. Второй — чуть более громоздкий в реализации, но немного быстрее и, что самое главное, позволяет использовать системные вёрстки.
В данной статье используется второй подход.
Базовый класс адаптера
Базовый адаптер берёт на себя ответственность за определение типа строки списка, взамен просит своих наследников уметь создавать заголовки и строки с данными:
protected abstract int getSectionsCount( );
protected abstract int getRowsCountInSection( int section );
protected abstract View getHeaderView( int section, View convertView,
ViewGroup parent, LayoutInflater inflater );
protected abstract View getItemView( int section, int row, View convertView,
ViewGroup parent, LayoutInflater inflater );
Далее, при изменении данных в списке (при вызове notifyDataSetChanged), мы считаем общее количество строк в списке, кешируем количество элементов в каждой секции, количество разных типов строк списка и связь этих типов с секциями:
private void setupAdapter( ) {
int sectionsCount = getSectionsCount();
_rowsCountsInSections = new int [sectionsCount];
_viewTypes = new int [sectionsCount];
_totalRowsCount = sectionsCount;
Set<Integer> viewTypesSet = new HashSet<Integer>();
for (int i = 0; i < sectionsCount; i++) {
_rowsCountsInSections[i] = getRowsCountInSection(i);
_totalRowsCount += _rowsCountsInSections[i];
_viewTypes[i] = getViewTypeForSection(i);
viewTypesSet.add(_viewTypes[i]);
}
viewTypesSet.add(VIEW_TYPE_HEADER);
_viewTypesCount = viewTypesSet.size();
}
На основе собранной информации можно без особых проблем выбрать, что нам нужно создавать в getView:
@Override
final public View getView( int position, View convertView, ViewGroup parent ) {
int section = getItemSection(position);
if (isItemHeader(position))
return getHeaderView(section, convertView, parent, _inflater);
int rowInSection = getItemRowInSection(position);
return getItemView(section, rowInSection, convertView, parent, _inflater);
}
Определяем, является ли строка заголовком или элементом секции, так:
private boolean isItemHeader( int position ) {
int sum = 0;
for (int i = 0; i < _rowsCountsInSections.length && sum <= position; i++) {
if (sum == position)
return true;
sum += _rowsCountsInSections[i] + 1;
}
return false;
}
private int getItemSection( int position ) {
int sum = 0;
int section = 0;
while (sum <= position && section < _rowsCountsInSections.length)
sum += _rowsCountsInSections[section++] + 1;
return section - 1;
}
private int getItemRowInSection( int position ) {
int section = getItemSection(position);
int sum = 0;
for (int i = 0; i < section; i++)
sum += _rowsCountsInSections[i] + 1;
return position - sum - 1;
}
В принципе, методы получения номера секции и индекса внутри секции можно объединить в один, разделение оставлено для наглядности.
Базовый класс целиком:
abstract public class BaseSectionedListAdapter extends BaseAdapter {
private static int VIEW_TYPE_HEADER = 0;
protected static int VIEW_TYPE_DATA = 1;
final private LayoutInflater _inflater;
private int _totalRowsCount = -1;
private int [] _rowsCountsInSections;
private int [] _viewTypes;
private int _viewTypesCount;
public BaseSectionedListAdapter( Context context ) {
_inflater = LayoutInflater.from(context);
}
@Override
final public int getCount( ) {
if (_totalRowsCount == -1)
setupAdapter();
return _totalRowsCount;
}
@Override
final public Object getItem( int position ) {
return null;
}
@Override
final public long getItemId( int position ) {
return position;
}
@Override
final public View getView( int position, View convertView, ViewGroup parent ) {
int section = getItemSection(position);
if (isItemHeader(position))
return getHeaderView(section, convertView, parent, _inflater);
int rowInSection = getItemRowInSection(position);
return getItemView(section, rowInSection, convertView, parent, _inflater);
}
@Override
public int getItemViewType( int position ) {
if (isItemHeader(position))
return VIEW_TYPE_HEADER;
int section = getItemSection(position);
return _viewTypes[section];
}
@Override
public int getViewTypeCount( ) {
return _viewTypesCount;
}
@Override
public boolean isEnabled( int position ) {
return !isItemHeader(position);
}
@Override
final public boolean areAllItemsEnabled( ) {
return false;
}
@Override
final public void notifyDataSetChanged( ) {
super.notifyDataSetChanged();
setupAdapter();
}
@Override
public boolean hasStableIds( ) {
return true;
}
private boolean isItemHeader( int position ) {
int sum = 0;
for (int i = 0; i < _rowsCountsInSections.length && sum <= position; i++) {
if (sum == position)
return true;
sum += _rowsCountsInSections[i] + 1;
}
return false;
}
private int getItemSection( int position ) {
int sum = 0;
int section = 0;
while (sum <= position && section < _rowsCountsInSections.length)
sum += _rowsCountsInSections[section++] + 1;
return section - 1;
}
private int getItemRowInSection( int position ) {
int section = getItemSection(position);
int sum = 0;
for (int i = 0; i < section; i++)
sum += _rowsCountsInSections[i] + 1;
return position - sum - 1;
}
private void setupAdapter( ) {
int sectionsCount = getSectionsCount();
_rowsCountsInSections = new int [sectionsCount];
_viewTypes = new int [sectionsCount];
_totalRowsCount = sectionsCount;
Set<Integer> viewTypesSet = new HashSet<Integer>();
for (int i = 0; i < sectionsCount; i++) {
_rowsCountsInSections[i] = getRowsCountInSection(i);
_totalRowsCount += _rowsCountsInSections[i];
_viewTypes[i] = getViewTypeForSection(i);
viewTypesSet.add(_viewTypes[i]);
}
viewTypesSet.add(VIEW_TYPE_HEADER);
_viewTypesCount = viewTypesSet.size();
}
protected int getViewTypeForSection( int section ) {
return VIEW_TYPE_DATA;
}
protected abstract int getSectionsCount( );
protected abstract int getRowsCountInSection( int section );
protected abstract View getHeaderView( int section, View convertView,
ViewGroup parent, LayoutInflater inflater );
protected abstract View getItemView( int section, int row, View convertView,
ViewGroup parent, LayoutInflater inflater );
}
Пример использования
Простой пример: на вход подаём список «секций», рисуем все данные секций одним стилем.
Секция представляет название заголовка и набор данных:
final public class SimpleSection {
final private String _title;
final private List<?> _data;
public SimpleSection( String title, List<?> data ) {
_title = title;
_data = data;
}
public String getTitle( ) {
return _title;
}
public List<?> getData( ) {
return _data;
}
}
Для заголовка используется простейшая вёрстка, состаящая из одного TextView:
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="30dp"
android:textAppearance="?android:attr/textAppearanceSmallInverse"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:background="@android:color/black"
/>
Простой адаптер целиком:
final public class SimpleSectionedListAdapter extends BaseSectionedListAdapter {
final private List<SimpleSection> _sections;
public SimpleSectionedListAdapter( Context context, List<SimpleSection> sections ) {
super(context);
_sections = sections;
}
@Override
protected int getSectionsCount( ) {
return _sections.size();
}
@Override
protected int getRowsCountInSection( int section ) {
return _sections.get(section).getData().size();
}
@Override
protected View getHeaderView( int section, View convertView, ViewGroup parent, LayoutInflater inflater ) {
if (convertView == null)
convertView = inflater.inflate(R.layout.list_header, parent, false);
TextView text = (TextView)convertView;
text.setText(_sections.get(section).getTitle());
return convertView;
}
protected Object getItemInSection( int section, int row ) {
return _sections.get(section).getData().get(row);
}
@Override
protected View getItemView( int section, int row, View convertView, ViewGroup parent, LayoutInflater inflater ) {
Object item = getItemInSection(section, row);
if (convertView == null)
convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
TextView text = (TextView)convertView;
text.setText(item.toString());
return convertView;
}
}
Итого
Если натравить на приведённый выше адаптер некоторый набор данных, то можно будет получить что-то похожее на (если поиграться с getViewTypeForSection, то легко получаем вторую картинку):
Приветствуются любые комментарии, пожелания, а также ругательства относительно стиля кодирования.
Автор: HighFlyer