Робота з ListView в Xamarin.Android

Нещодавно у мене виникла ідея зібрати всі базові найбільш часто використовувані фічі для ListView в Android і для зручності об'єднати їх в один проект. Як звичайно, я зайшов в інтернет і знайшов там чудову статті та її переклади на хабре (переклад 1, переклад 2). Не всі, на мій погляд, було потрібним і корисним в цій статті, тому я включив в кінцевий проект тільки те, що мені здалося значущим. Сподіваюся, в майбутньому це стане в нагоді кому-то ще.

До речі, я Xamarin розробник, тому проект (і семпли, відповідно) будуть написані на C# для Xamarin.Android.

Отже, приступимо:

Наповнення даними (TODO 1)

Як відомо, ListView в Android — це елемент, який надає дані у вигляді списку, де кожен елемент представлений своєї View. Для управління відображенням клітинок Adapters. Для того щоб наповнити ListView даними в самому скромному вигляді можна використовувати ArrayAdapter. Це робиться в два рядки:

var animals = GetAnimals ();
var adapter = new ArrayAdapter<Animal> (this, Android.Resource.Layout.SimpleListItem1, animals); 

Як ви бачите, результат відповідний — швидко, дешево і не особливо красиво, але до цього ми ще повернемося.



Обробка вибору комірки (TODO 2)

Користувачеві часто потрібно якось спілкуватися з коротким списком — вибирати елементи, переглядати детальну інформацію і т. д. Для обробки вибору комірки в Xamarin.Android потрібно всього лише підписатися на подію ItemClick у ListView. Для видалення виділеного елемента використовується наступний код:

bool inDeletion = false;
list.ItemClick += (sender, e) => {
if (!inDeletion) {
inDeletion = true;
e.View.Animate ()
.SetDuration (500)
.Alpha (0.0 f)
.WithEndAction (new Runnable (() => {
#region TODO5
//TODO 05
var item = adapter.GetRawItem (e.Position);
#endregion
//var item = list.Adapter.GetItem(e.Position);
adapter.Remove (item);
adapter.NotifyDataSetChanged ();
e.View.Alpha = 1;
#region TODO9
//TODO 09
ShowSnackBar (1);
#endregion
inDeletion = false;
}));
}
};

Обробка порожнього списку TODO 3)

Є ймовірність, що користувач побачить списку без елементів. Для того, щоб він не загубився і не закрив додаток, краще всього використовувати EmptyView. Його слід оголосити в layout Activity і одним рядком дати зрозуміти ListView, що цю View він повинен показати, коли у нього немає елементів:

list.EmptyView = FindViewById<TextView> (Resource.Id.empty); 

Зміна зовнішнього вигляду клітинки (TODO 4,5)

Щоб трохи поліпшити зовнішній вигляд нашого списку, можна застосувати перевантаження конструктора ArrayAdapter і передати в нього layout, який буде використовуватися для відображення елемента списку. Також потрібно передати Id текстового поля, де буде показуватися рядковий аналог об'єкта:

var adapter = new ArrayAdapter<Animal> (this, Resource.Layout.row_custom, Resource.Id.row_custom_text, animals); 

Тепер наш список виглядає трохи краще:



Для подальшої модернізації зовнішнього вигляду ми створимо власний адаптер, в якому переопределим метод GetView і вже самі будемо управляти зовнішнім виглядом осередків. Реалізація адаптера сама базова, тому постити сюди весь код зайве. Якщо хочете побачити зміни, расскоментируйте код в регіоні TODO5. Також деякий старий код потребує модифікації — метод GetItem може повертати тільки об'єкт типу Java.Lang.Object. Так як обертати кожен елемент списку в Java.Lang.Object — це зайва розкіш, створимо свій метод GetRawItem, який буде повертати потрібний тип. Тепер що стосується модифікації, щоб проект скомпилировался, при видаленні комірки потрібно замінити виклик GetItem на GetRawItem.



Ось таке ми бачимо в результаті. На конкурс дизайну не годиться, але для тестового проекту те, що потрібно.

Додавання Contextual Action Bar (CAB) (TODO 6)

Наступним нашим кроком буде додавання CAB функціоналу до нашого ListView. Для довідки, опис і детальне керівництво по впровадженню можна знайти на тут. Для того, щоб додати CAB, по-перше потрібно меню, яке буде з'являтися при користувальницькому long tap. Файл меню:

<?xml version="1.0" encoding="UTF-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/ActionModeDeleteItem"
android:title="Delete"
android:showAsAction="always"/>
</menu>

По-друге, необхідно реалізувати в Activity інтерфейс ActionMode.ICallback наступним чином:

public bool OnActionItemClicked (ActionMode mode, IMenuItem item)
{
switch(item.ItemId){
case Resource.Id.ActionModeDeleteItem:
SparseBooleanArray selected = _adapter.SelectedIds;
List<Animal> itemList = new List<Animal> ();
var keys = new List < int> ();
for (int i = (selected.Size () - 1); i >= 0; i--) {
//checkisvaluecheckedby user
if (selected.ValueAt (i)) {
keys.Add (selected.KeyAt (i));
var selectedItem = _adapter.GetRawItem (selected.KeyAt (i));
itemList.Add (selectedItem);
}
}
_adapter.Remove (itemList);
_mode.Finish();
return true;
default:
return false;
}
}

public bool OnCreateActionMode (ActionMode mode, IMenu menu)
{
mode.MenuInflater.Inflate (Resource.Menu.menu menu);
inActionMode = true;
return true;
}

public void OnDestroyActionMode (ActionMode mode)
{
_adapter.RemoveSelection ();
_adapter.NotifyDataSetChanged ();
_mode = null;
inActionMode = false;
}

public bool OnPrepareActionMode (ActionMode mode, IMenu menu)
{
return false;
}

По-третє, в методі обробки користувальницького кліка потрібно врахувати, що користувач може знаходиться в ActionMode:

if(inActionMode){
adapter.ToggleSelection(e.Position);
_mode.Title=(adapter.SelectedCount.ToString()+" selected");
return;
} 

По-четверте, в адаптер слід додати методи, які будуть відповідати за вибір елемента:

protected virtual void SelectView(int position, bool value){
if(value)
_selectedItemsIds.Put(position,value);
else
_selectedItemsIds.Delete(position);
NotifyDataSetChanged();
}

public void ToggleSelection(int position){
SelectView(position, !_selectedItemsIds.Get(position));
}

public int SelectedCount {
get{
return _selectedItemsIds.Size();
}
}

public void RemoveSelection(){
_selectedItemsIds=new SparseBooleanArray();
NotifyDataSetChanged();
}

public SparseBooleanArray SelectedIds{
get {
return _selectedItemsIds;
}
} 

Після чого отримуємо необхідний результат:



Продуктивність в ListView (TODO 7)

Щоб ListView показував свої елементи швидше, реалізуємо ViewHolder патерн. Для цього створимо клас ViewHolder, який успадковує базовий Java.Lang.Object (це потрібно для того, щоб привласнити ViewHolder властивості view.Tag). Знаючі люди говорят, що ListView з ViewHolder працює на 15% швидше. У мене на симуляторі швидкодія від 1-3ms підскочив до 0-1ms для однієї View. Зрозуміло, це залежить від того, як часто викликається FindViewByID. Тепер, власне, подивимося, як змінився наш GetView метод:

public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = this [position];
var view = convertView;
if (view == null) {
view = LayoutInflater.From (parent.Context).Inflate (Resource.Layout.row_custom_adapter, parent, false);
var viewHolder = new ViewHolder ();
viewHolder.Text = view.FindViewById<TextView> (Resource.Id.row_custom_name);
viewHolder.Weight = view.FindViewById<TextView> (Resource.Id.row_custom_weight);
viewHolder.Icon = view.FindViewById<ImageView> (Resource.Id.row_custom_icon);
view.Tag = viewHolder;
}
var holder = (ViewHolder)view.Tag;
holder.Text.Text = item.Name;
holder.Weight.Text = item.Weight.ToString ("F");
if (_selectedItemsIds.Size () > 0 && _selectedItemsIds.Get (position)) {
view.SetBackgroundColor (Android.Graphics.Color.CadetBlue);
} else {
view.Background = GetBackground (item.Color);
}
holder.Icon.SetImageResource (GetImage (item.Name));
return view;
} 

Додавання секцій (TODO 8)

Для того, щоб наші звірі виглядали більш консистентним, слід розбити їх на групи за кольорами. Для цього створимо новий адаптер SectionAnimalAdapter, який буде успадковувати AnimalAdapter. Головна відмінність цього адаптера в тому, що він зберігає об'єкти як словник Dictionary<string List> і при кожному видалення елемента викликає метод BuildSections, щоб відновити Dictionary. Також додасться новий Viewtype — header і layout для нього. Код можна подивитись у файле.

Тепер наш список буде виглядати наступним чином:



Скасування останнього дії (TODO 9)

Дуже часто у користувачів з'являється бажання скасувати останню дію, особливо коли це стосується видалення. Реалізуємо цей функціонал за допомогою Snackbar. Після видалення будемо показувати користувачеві Snackbar з пропозицією скасувати останню дію. Звичайно ж, це потребує невеликих маніпуляцій в адаптері — слід створити додатковий список, де буде зберігатися такий стан, до якого зможе відкотитися користувач, і метод Rollback:

public virtual void RollBack ()
{
_animals = _rollbackAnimals;
_rollbackAnimals = null;
} 



Розкривають списки (TODO 10)

При необхідності можна реалізувати розкривають списки. Щоб список був розкривним, він повинен мати тип ExpandableListView, а його адаптера необхідно наслідувати тип BaseExpandableListAdapter. Основна його відмінність від звичайного адаптера — це два методи для групи і для child елемента:

public override View GetGroupView (int position, bool isExpandable, View convertView, ViewGroup parent)
{
GroupViewHolder holder = null;
var view = convertView;

if (view != null)
holder = view.Tag as GroupViewHolder;

if (holder == null) {
view = LayoutInflater.From (parent.Context).Inflate (Resource.Layout.row_expandable_header, null);
holder = new GroupViewHolder ();
holder.Text = view as CheckedTextView;
}
var sect = _sections.Keys.ToList () [position];

var name = _sections [секта].First ().Name;
holder.Text.Text = name;
holder.Text.Checked = isExpandable;
return view;
}

public override View GetChildView (int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent)
{
var view = convertView;
var sect = _sections.Keys.ToList () [groupPosition];
var item = _sections [секта] [childPosition];
if (view == null) {
view = LayoutInflater.From (parent.Context).Inflate (Resource.Layout.row_custom_adapter, parent, false);
var viewHolder = new ChildViewHolder ();
viewHolder.Text = view.FindViewById<TextView> (Resource.Id.row_custom_name);
viewHolder.Weight = view.FindViewById<TextView> (Resource.Id.row_custom_weight);
viewHolder.Icon = view.FindViewById<ImageView> (Resource.Id.row_custom_icon);
view.Tag = viewHolder;
}
var holder = (ChildViewHolder)view.Tag;
holder.Text.Text = item.Name;
holder.Weight.Text = item.Weight.ToString ("F");
holder.Icon.SetImageResource (GetImage (item.Name));
return view;
}



drag'n'drop списки (TODO 11)

Щоб реалізувати список, який буде підтримувати drag'n'drop, потрібно створити новий клас, який успадковує клас ListView. Вихідний приклад взятий з відео. У двох словах, при long-tap користувача на клітинку створюється її snapshot, який переміщується в слід за пальцем користувача. Коли палець досягає сусідньої комірки, об'єкти в адаптері міняються місцями і перерисовуются. Код DynamicListView здесь.

Для того, щоб запустити додаток з drag'n'drop списком, слід зробити стартовою SecondaryActivity. Расскоментировав рядок MainLauncher = true.



Це все, що я хотів розповісти про ListView для Android. Але, можливо, проекту ще не вистачає якихось важливих фіч? Буду радий почути в коментарях, які фічі найчастіше використовують ваші ListView :)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.