![]() |
VOOZH | about |
dotnet add package MvxSectionedRecyclerView --version 1.0.1.1
NuGet\Install-Package MvxSectionedRecyclerView -Version 1.0.1.1
<PackageReference Include="MvxSectionedRecyclerView" Version="1.0.1.1" />
<PackageVersion Include="MvxSectionedRecyclerView" Version="1.0.1.1" />Directory.Packages.props
<PackageReference Include="MvxSectionedRecyclerView" />Project file
paket add MvxSectionedRecyclerView --version 1.0.1.1
#r "nuget: MvxSectionedRecyclerView, 1.0.1.1"
#:package MvxSectionedRecyclerView@1.0.1.1
#addin nuget:?package=MvxSectionedRecyclerView&version=1.0.1.1Install as a Cake Addin
#tool nuget:?package=MvxSectionedRecyclerView&version=1.0.1.1Install as a Cake Tool
This is an unofficial package that contains an expandable AndroidX RecyclerView supported for MvvmCross. This view allows us to bind a collection of items (objects, ViewModels, etc) to the ItemsSource property. It works similarly to a RecyclerView. However, this comes with out-of-the-box functionality such as grouping items with collapsible/expandable headers. Additional functionality can be implemented such as dragging items up and down and swiping them by setting a boolean property to EnableDrag, EnableSwipeRight and/or EnableSwipeLeft attributes, via MvxSmartRecyclerView.
All original functionality of MvxRecyclerView is also available and it is highly encouraged that you read the documentation before proceeding.
All functionality of MvxSmartRecyclerView is also available and it is essential to read the documentation for a better understanding, before proceeding.
You will need to ensure that you have the MvxSectionedRecyclerView NuGet package installed in your .Droid project.
.Core project, we will need to create our classes: Student.cs and Lesson.cs.public class Student : MvxNotifyPropertyChanged
{
private string firstName;
private string lastName;
private Lesson lesson;
private int sequence;
public Student(string firstName, string lastName, Lesson lesson)
{
FirstName = firstName;
LastName = lastName;
Lesson = lesson;
}
public string FirstName { get => firstName; set => SetProperty(ref firstName, value); }
public string LastName { get => lastName; set => SetProperty(ref lastName, value); }
public Lesson Lesson { get => lesson; set => SetProperty(ref lesson, value); }
public int Sequence { get => sequence; set => SetProperty(ref sequence, value); }
}
public class Lesson : MvxNotifyPropertyChanged
{
public static readonly Lesson Empty = new Lesson(Subject.None, DateTime.MinValue);
private int sequence;
public Lesson(Subject subject, DateTime dateTime)
{
Subject = subject;
DateTime = dateTime;
}
public DateTime DateTime { get; }
public int Sequence { get => sequence; set => SetProperty(ref sequence, value); }
public Subject Subject { get; }
}
public enum Subject
{
None = 0,
English = 1,
Math = 2,
}
Students for binding.public MvxObservableCollection<Student> Students { get; set; }
.Droid project. We will create a layout to display our Student.cs entity by creating StudentView.xml.StudentView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:MvxBind="Text Format('{0} {1} - {2} on {3:d}', FirstName, LastName, Lesson.Subject, Lesson.DateTime);"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textColor="?android:attr/colorAccent"
app:MvxBind="Text Sequence;"/>
</LinearLayout>
xml representing a Lesson called LessonHeaderView.xml. This view binds to properties in IMvxSection such as IMvxSection.Header, IMvxSection.Items and IMvxSection.IsCollapsed.LessonHeaderView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_blue_bright"
android:orientation="horizontal"
android:padding="8sp"
android:gravity="center">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/arrow_down_float"
app:MvxBind="Visible IsCollapsed;" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/arrow_up_float"
app:MvxBind="Visible !IsCollapsed;" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Base.TextAppearance.AppCompat.Headline"
app:MvxBind="Text Format('{0} on {1:d}', Header.Subject, Header.DateTime);"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textColor="@android:color/holo_blue_light"
app:MvxBind="Text Header.Sequence;" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textColor="@android:color/holo_red_dark"
app:MvxBind="Text Items.Count;" />
</LinearLayout>
MvxSectionedRecyclerView to know how to group and display our items, we will need to create an adapter class that inherits MvxSectionedRecyclerAdapter<THeader, TItem> as follows:namespace AppointmentPlanner.DroidX.Components
{
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
public AppointmentSectionedRecyclerAdapter()
{
}
public AppointmentSectionedRecyclerAdapter(IMvxAndroidBindingContext bindingContext)
: base(bindingContext)
{
}
protected override Lesson GetItemHeader(Student item) => item.Lesson;
protected override void SetItemHeader(Lesson header, Student item) => item.Lesson = header;
}
}
Important: To set this adapter via xml you will need to provide it in the MvxAdapter attribute on the MvxSectionedRecyclerView. It must be of the format: Fully.Qualified.ClassName, Assembly.Name. Hence, for the example above: let us say the assembly will be AppointmentPlanner.DroidX and as you see the namespace is AppointmentPlanner.DroidX.Components then the string will be: AppointmentPlanner.DroidX.Components.AppointmentSectionedRecyclerAdapter, AppointmentPlanner.DroidX.
MvxSectionedTemplateSelector<THeader, TItem> to handle displaying layouts for the corresponding section header(s) and item(s).namespace AppointmentPlanner.DroidX.Components
{
public class AppointmentSectionedTemplateSelector : MvxSectionedTemplateSelector<Lesson, Student>
{
protected override int SelectItemViewType(Student item)
{
return Resource.Layout.StudentView;
}
protected override int SelectSectionViewType(MvxSection<Lesson, Student> section)
{
return Resource.Layout.LessonHeaderView;
}
}
}
MvxSectionedRecyclerView to one of your View.xml is very simple.<MvvmCross.SectionedRecyclerView.MvxSectionedRecyclerView
android:id="@+id/appointment_sectioned_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:MvxAdapter="AppointmentPlanner.DroidX.Components.AppointmentSectionedRecyclerAdapter, AppointmentPlanner.DroidX"
app:MvxTemplateSelector="AppointmentPlanner.DroidX.Components.AppointmentSectionedTemplateSelector, AppointmentPlanner.DroidX"
app:MvxBind="ItemsSource Students;"/>
Important: MvxExpandableRecyclerView will require you to bind a MvxObservableCollection<TItem> to ItemsSource. It will need to have your custom MvxSectionedRecyclerAdapter<THeader, TItem> and MvxSectionedTemplateSelector<THeader, TItem> for it to display your headers and items correctly by adding the app:MvxAdapter and app:MvxTemplateSelector attributes. The view will also require THeader to inherit IComparable<THeader> or else the view will not show the items and extra setup will be needed. More info is provided in the following.
IComparable<T> / IComparer<T>.InvalidOperationException: Failed to compare two elements in the array.
ArgumentException: At least one object must implement IComparable.
If your list doesn't display or you come across any of the above errors when loading MvxSectionedRecyclerView, it means that the view does not know how to order the sections based on the header class. To fix this, you can either:
IComparable<T> where T is your header class. In our example, our Lessons will be compared based on their DateTime.public class Lesson : MvxNotifyPropertyChanged, IComparable<Lesson>
{
// Constructors, properties, etc...
public int CompareTo(Lesson other)
{
if (other == null)
{
return 1;
}
return DateTime.CompareTo(other.DateTime);
}
}
IComparer<T> where T is your header class. For example:public class LessonComparer : IComparer<Lesson>
{
public int Compare(Lesson x, Lesson y)
{
if (x == null && y == null)
{
return 0;
}
else if (x == null)
{
return -1;
}
else if (y == null)
{
return 1;
}
else
{
return x.DateTime.CompareTo(y.DateTime);
}
}
}
Then you override the GetSectionComparer method in your custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem> and return your custom IComparer<T>. For example:
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
/// Constructors, properties, etc...
protected override IComparer<Lesson> GetSectionComparer() => new LessonComparer();
}
IEquatable<T> / IEqualityComparer<T>Similar to the above, you can modify your header class to implement IEquatable<T> where T is your header class to determine if 2 headers are the same. For example:
public class Lesson : MvxNotifyPropertyChanged, IEquatable<Lesson>
{
// Constructors, properties, etc...
public bool Equals(Lesson other)
{
if (other == null)
{
return false;
}
return Subject == other.Subject && DateTime.Equals(other.DateTime);
}
public override bool Equals(object obj)
{
if (obj is Lesson lesson)
{
return Equals(lesson);
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(Subject, DateTime);
}
}
However, if you're not able to override the equality of your header class, you can override the GetSectionEqualityComparer method (in your custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem>) and return an IEqualityComparer<T>. For example:
public class LessonEqualityComparer : IEqualityComparer<Lesson>
{
public bool Equals(Lesson x, Lesson y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x == null || y == null)
{
return false;
}
return x.Subject.Equals(y.Subject) && x.DateTime.Equals(y.DateTime);
}
public int GetHashCode(Lesson obj) => HashCode.Combine(obj?.Subject, obj?.DateTime);
}
public class AppointmentRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
/// Constructors, properties, etc...
protected override IEqualityComparer<Lesson> GetSectionEqualityComparer() => new LessonEqualityComparer();
}
If you would like to always show certain headers in your list, you can modify your custom adapter and override the AddInitialHeaders method to return a list of headers. For example:
public class AppointmentRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
/// Constructors, properties, etc...
protected override IEnumerable<Lesson> AddInitialHeaders()
{
return new List<Lesson>(1)
{
Lesson.Empty,
};
}
}
To enable the dragging feature, we need to modify our xml and set the EnableDrag attribute to true.
<MvvmCross.SectionedRecyclerView.MvxSectionedRecyclerView
app:EnableItemDrag="true"/>
If you want to prevent certain items in the list from being dragged, you can create a custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem> and override the ShouldDragItem(TItem item) method. This allows you to specify conditions on which items are allowed to be dragged.
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
// Constructors...
public override bool ShouldDragItem(Student item)
{
if (item is something...)
{
// Allow dragging
return true;
}
else
{
// Prevent dragging
return false;
}
}
}
To enable the swiping feature, we need to modify our xml and set EnableSwipeRight and/or EnableSwipeLeft attributes to true.
<MvvmCross.SectionedRecyclerView.MvxSectionedRecyclerView
app:EnableSwipeRight="true"
app:EnableSwipeLeft="true"
app:MvxBind="ItemSwipeRight SwipeRightCommand;
ItemSwipeLeft SwipeLeftCommand;"/>
Swipe actions are bindable and can have 2 different actions depending on the direction of the swipe. ItemSwipeLeft and ItemSwipeRight are bindable and are done in the same way as MvxRecyclerView's ItemClickCommand and ItemLongClickCommand.
If you want to prevent certain items in the list from being swiped left or right, you can create a custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem> and override either the ShouldSwipeLeft(TItem item) and/or ShouldSwipeRight(TItem item) methods. This allows you to specify conditions on which items are allowed to be swiped left or right. For example:
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
// Constructors...
public override bool ShouldSwipeLeft(Student item)
{
if (item is something...)
{
// Allow left swipe
return true;
}
else
{
// Prevent left swipe
return false;
}
}
public override bool ShouldSwipeRight(Student item)
{
if (item is something...)
{
// Allow right swipe
return true;
}
else
{
// Prevent right swipe
return false;
}
}
}
We can show different backgrounds for an item depending on the swipe direction. In this example, we create 2 new layout files:
UnplanItemBackgroundView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@id/smart_recycler_item_left_background_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="start"
android:background="@android:color/holo_blue_light">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginRight="10sp"
android:background="@drawable/abc_ic_ab_back_material"
android:contentDescription="Unplan Item" />
</LinearLayout>
RemoveItemBackgroundView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@id/smart_recycler_item_right_background_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="end"
android:background="@android:color/holo_red_light">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10sp"
android:background="@drawable/abc_ic_clear_material"
android:contentDescription="Remove Item" />
</LinearLayout>
Important: the important thing to notice in these files is that each layout has an android:id attribute. This is important for the control because it identifies which layout to show when swiping left or right, or not swiping at all. The android:ids needed for the control are:
android:id="@id/smart_recycler_item_left_background_view" (show layout when swiping right)android:id="@id/smart_recycler_item_right_background_view" (show layout when swiping left)android:id="@id/smart_recycler_item_foreground_view" (show layout for item when user is not swiping).We then modify our StudentView.xml to include these layouts and wrap everything in a FrameLayout, making sure the background layouts are added first. We also need to add android:id="@id/smart_recycler_item_foreground_view" to the nested LinearLayout holding the default view to show when the user isn't swiping.
Note: by default, the LinearLayout's background is transparent, resulting in the background views to show. The LinearLayout's background attribute is set to @android:color/white to ensure the background views are hidden.
StudentView.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/unplanitembackgroundview"/>
<include
layout="@layout/removeitembackgroundview"/>
<LinearLayout
android:id="@id/smart_recycler_item_foreground_view"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
android:gravity="center"
android:background="@android:color/white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:MvxBind="Text Format('{0} {1} - {2} on {3:d}', FirstName, LastName, Lesson.Subject, Lesson.DateTime);"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textColor="?android:attr/colorAccent"
app:MvxBind="Text Sequence;"/>
</LinearLayout>
</FrameLayout>
If you want to show different swipe backgrounds depending on certain conditions relating to the item, you can create a custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem> and override either the GetLeftBackgroundResourceId(TItem item) and/or GetRightBackgroundResourceId(TItem item) methods. This allows you to specify conditions on what backgrounds to show when swiping left or right.
You will also need to override the GetBackgroundResourceIds() method to return a list of Resource Ids that will be used for the background views to help prevent visual bugs.
For example, continuing on from the Swipe Backgrounds section above, we will create 2 more layouts:
PlanItemBackgroundView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/plan_item_background_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="start"
android:background="@android:color/holo_green_light">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginRight="10sp"
android:background="@drawable/abc_ic_ab_back_material"
android:contentDescription="Plan Item" />
</LinearLayout>
ResetItemBackgroundView.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/reset_item_background_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="end"
android:background="@android:color/holo_orange_light">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10sp"
android:background="@drawable/abc_ic_clear_material"
android:contentDescription="Reset Item" />
</LinearLayout>
Important: make sure each layout has an android:id attribute. This is important for the control because it will help identify which layout to show when swiping left or right. In this example, they are:
android:id="@+id/plan_item_background_view"android:id="@+id/reset_item_background_view"For example, in our custom adapter: MvxSectionedRecyclerAdapter<Lesson, Student> we make sure to override the required methods: GetLeftBackgroundResourceId(Student item), GetRightBackgroundResourceId(Student item) and GetBackgroundResourceIds():
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
// Constructors...
public override IEnumerable<int> GetBackgroundResourceIds() => new List<int>(2)
{
Resource.Id.reset_item_background_view,
Resource.Id.plan_item_background_view,
};
public override int GetLeftBackgroundResourceId(Student item)
{
if (item is something...)
{
return Resource.Id.plan_item_background_view;
}
return base.GetLeftBackgroundResourceId(item);
}
public override int GetRightBackgroundResourceId(Student item)
{
if (item is something...)
{
return Resource.Id.reset_item_background_view;
}
return base.GetRightBackgroundResourceId(item);
}
}
Note: the base implementation for:
GetLeftBackgroundResourceId(object item) returns Resource.Id.smart_recycler_item_left_background_viewGetRightBackgroundResourceId(object item) returns Resource.Id.smart_recycler_item_right_background_viewWe then modify our StudentView.xml to include these new layouts and wrap everything in a FrameLayout, making sure the background layouts are added first.
StudentView.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/unplanitembackgroundview"/>
<include
layout="@layout/removeitembackgroundview"/>
<include
layout="@layout/planitembackgroundview"/>
<include
layout="@layout/resetitembackgroundview"/>
<LinearLayout
android:id="@id/smart_recycler_item_foreground_view"
android:layout_width="match_parent"
android:layout_height="30dp"
android:orientation="horizontal"
android:gravity="center"
android:background="@android:color/white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:MvxBind="Text Format('{0} {1} - {2} on {3:d}', FirstName, LastName, Lesson.Subject, Lesson.DateTime);"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textColor="?android:attr/colorAccent"
app:MvxBind="Text Sequence;"/>
</LinearLayout>
</FrameLayout>
Note: for the actual logic that gets executed when swiping left or right, the ItemSwipeLeft and ItemSwipeRight will need to have logic that accounts for this.
If you want to fine-tune when the RecyclerView should update its views, you can modify your custom adapter that inherits MvxSectionedRecyclerAdapter<THeader, TItem> and override the CreateDiffUtilHelper(IList oldList, IList newList) method. This method allows you to return a DiffUtil.Callback object which helps the RecyclerView determine what views to update when performing an operation (add, remove, update).
By default, this method returns a MvxDefaultSmartDiffUtilHelper which handles any object. However, you can override the method and return your own custom class that inherits MvxSmartDiffUtilHelper<T> where T is your item (not header) class to fine-tune when the RecyclerView should update its views.
Using the list of students as an example:
public class AppoinmentDiffUtilHelper : MvxSmartDiffUtilHelper<Student>
{
public AppoinmentDiffUtilHelper(IList oldList, IList newList)
: base(oldList, newList)
{
}
protected override bool AreContentsTheSame(Student oldItem, Student newItem)
{
return oldItem.FirstName == newItem.FirstName
&& oldItem.LastName == newItem.LastName
&& oldItem.Lesson.Subject == newItem.Lesson.Subject
&& oldItem.Lesson.DateTime == newItem.Lesson.DateTime;
}
protected override bool AreItemsTheSame(Student oldItem, Student newItem)
{
return oldItem.FirstName == oldItem.FirstName
&& oldItem.LastName == newItem.LastName;
}
}
Note: the AreItemsTheSame(Student oldItem, Student newItem) method checks if the two items represent the same item. If true: the AreContentsTheSame(Student oldItem, Student newItem) method is called and checks if the item's content has been changed. If true: the RecyclerView updates the corresponding view, otherwise it doesn't.
In our custom adapter we make sure to override the CreateDiffUtilHelper(IList oldList, IList newList) method and return a new instance of our custom DiffUtil.Callback class: AppoinmentDiffUtilHelper.
public class AppointmentSectionedRecyclerAdapter : MvxSectionedRecyclerAdapter<Lesson, Student>
{
// Constructors...
protected override DiffUtil.Callback CreateDiffUtilHelper(IList oldList, IList newList) => new AppoinmentDiffUtilHelper(oldList, newList);
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| MonoAndroid | monoandroid10.0 monoandroid10.0 is compatible. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.