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

от автора

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

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

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

Итак, в первую очередь нам потребуется создать 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.

ссылка на оригинал статьи http://habrahabr.ru/post/185004/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *