Last active
June 29, 2018 01:05
-
-
Save mzhukovs/4d2a7fc77f2b131e8309c9ae619f5f30 to your computer and use it in GitHub Desktop.
FlexLayout RepeaterView for Xamarin Forms with Fade In Animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public delegate void RepeaterViewItemAddedEventHandler(object sender, RepeaterViewItemAddedEventArgs args); | |
/// <summary> | |
/// Don't set IsVisible to false or you will have a bad time | |
/// this won't relayout its child elements when you change the visibility | |
/// </summary> | |
// in lieu of an actual Xamarin Forms ItemsControl, this is a heavily modified version of code from https://forums.xamarin.com/discussion/21635/xforms-needs-an-itemscontrol | |
public class RepeaterView : FlexLayout | |
{ | |
public RepeaterView() | |
{ | |
Dictionary<Element, IDisposable> activatedViews | |
= new Dictionary<Element, IDisposable>(); | |
} | |
public static readonly BindableProperty ItemsSourceProperty = | |
BindableProperty.Create( | |
nameof(ItemsSource), | |
typeof(IEnumerable), | |
typeof(RepeaterView), | |
defaultValue: null, | |
defaultBindingMode: BindingMode.OneWay, | |
propertyChanged: ItemsChanged); | |
public static readonly BindableProperty ItemTemplateProperty = | |
BindableProperty.Create( | |
nameof(ItemTemplate), | |
typeof(DataTemplate), | |
typeof(RepeaterView), | |
defaultValue: null, | |
defaultBindingMode: BindingMode.OneWay); | |
/// <summary> | |
/// Set to True in order to fade in each child element as it gets added to the repeater view | |
/// </summary> | |
public bool ApplyFadingToChildren { get; set; } = true; | |
public int FadeDuration { get; set; } = 500; | |
public int AddChildDelay { get; set; } = 180; | |
bool waitingForBindingContext = false; | |
public event RepeaterViewItemAddedEventHandler ItemCreated; | |
public IEnumerable ItemsSource | |
{ | |
get { return (IEnumerable)GetValue(ItemsSourceProperty); } | |
set { SetValue(ItemsSourceProperty, value); } | |
} | |
public DataTemplate ItemTemplate | |
{ | |
get { return (DataTemplate)GetValue(ItemTemplateProperty); } | |
set { SetValue(ItemTemplateProperty, value); } | |
} | |
protected override void OnBindingContextChanged() | |
{ | |
base.OnBindingContextChanged(); | |
if (BindingContext != null && waitingForBindingContext && ItemsSource != null) | |
{ | |
ItemsChanged(this, null, ItemsSource); | |
} | |
} | |
private static async void ItemsChanged(BindableObject bindable, object old, object newVal) | |
{ | |
IEnumerable oldValue = old as IEnumerable; | |
IEnumerable newValue = newVal as IEnumerable; | |
var control = (RepeaterView)bindable; | |
var oldObservableCollection = oldValue as INotifyCollectionChanged; | |
if (oldObservableCollection != null) | |
{ | |
oldObservableCollection.CollectionChanged -= control.OnItemsSourceCollectionChanged; | |
} | |
//HACK:SHANE | |
if (control.BindingContext == null) | |
{ | |
control.waitingForBindingContext = true; | |
//this means this control has been removed from the visual tree | |
//so don't update it other wise you get random null reference exceptions | |
return; | |
} | |
control.waitingForBindingContext = false; | |
var newObservableCollection = newValue as INotifyCollectionChanged; | |
if (newObservableCollection != null) | |
{ | |
newObservableCollection.CollectionChanged += control.OnItemsSourceCollectionChanged; | |
} | |
try | |
{ | |
control.Children.Clear(); | |
if (newValue != null) | |
{ | |
var i = 0; | |
foreach (var item in newValue) | |
{ | |
if (control.AddChildDelay > 0 && i > 0) | |
await Task.Delay(control.AddChildDelay); | |
var view = control.CreateChildViewFor(item); | |
control.Children.Add(view); | |
control.OnItemCreated(view); | |
i++; | |
} | |
} | |
control.UpdateChildrenLayout(); | |
control.InvalidateLayout(); | |
} | |
catch (NullReferenceException) | |
{ | |
try | |
{ | |
Debug.WriteLine( | |
String.Format($"RepeaterView: NullReferenceException Parent:{control.Parent} ParentView:{control.Parent} IsVisible:{control.IsVisible}") | |
); | |
} | |
catch (Exception exc) | |
{ | |
Debug.WriteLine($"NullReferenceException Logging Failed {exc}"); | |
} | |
} | |
} | |
protected virtual void OnItemCreated(View view) | |
{ | |
if (this.ItemCreated != null) | |
{ | |
ItemCreated.Invoke(this, new RepeaterViewItemAddedEventArgs(view, view.BindingContext)); | |
} | |
} | |
private async void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) | |
{ | |
try | |
{ | |
var invalidate = false; | |
List<View> createdViews = new List<View>(); | |
if (e.Action == NotifyCollectionChangedAction.Reset) | |
{ | |
var list = sender as IEnumerable; | |
this.Children.SyncList( | |
list, | |
(item) => | |
{ | |
var view = this.CreateChildViewFor(item); | |
createdViews.Add(view); | |
return view; | |
}, (item, view) => view.BindingContext == item, | |
null); | |
foreach (View view in createdViews) | |
{ | |
OnItemCreated(view); | |
} | |
invalidate = true; | |
} | |
if (e.OldItems != null) | |
{ | |
this.Children.RemoveAt(e.OldStartingIndex); | |
invalidate = true; | |
} | |
if (e.NewItems != null) | |
{ | |
for (var i = 0; i < e.NewItems.Count; ++i) | |
{ | |
var item = e.NewItems[i]; | |
if (this.AddChildDelay > 0 && i > 0) | |
await Task.Delay(this.AddChildDelay); | |
var view = this.CreateChildViewFor(item); | |
this.Children.Insert(i + e.NewStartingIndex, view); | |
OnItemCreated(view); | |
} | |
invalidate = true; | |
} | |
if (invalidate) | |
{ | |
this.UpdateChildrenLayout(); | |
this.InvalidateLayout(); | |
} | |
} | |
catch (NullReferenceException) | |
{ | |
try | |
{ | |
Debug.WriteLine( | |
$"RepeaterView.OnItemsSourceCollectionChanged: NullReferenceException Parent:{Parent} ParentView:{Parent} IsVisible:{IsVisible} BindingContext: {BindingContext}" | |
); | |
} | |
catch (Exception exc) | |
{ | |
Debug.WriteLine($"OnItemsSourceCollectionChanged: NullReferenceException Logging Failed {exc}"); | |
} | |
} | |
} | |
private View CreateChildViewFor(object item) | |
{ | |
this.ItemTemplate.SetValue(BindableObject.BindingContextProperty, item); | |
View view; | |
if (this.ItemTemplate is DataTemplateSelector) | |
{ | |
var dts = (DataTemplateSelector)this.ItemTemplate; | |
view = (View)dts.SelectTemplate(item, null).CreateContent(); | |
} | |
else | |
{ | |
view = (View)this.ItemTemplate.CreateContent(); | |
} | |
if (ApplyFadingToChildren) | |
{ | |
view.Opacity = 0; | |
var fadeInTrigger = new Trigger(typeof(View)); | |
fadeInTrigger.Property = View.IsVisibleProperty; | |
fadeInTrigger.Value = true; | |
fadeInTrigger.EnterActions.Add(new FadeTriggerAction() { Duration = (uint)this.FadeDuration }); | |
fadeInTrigger.ExitActions.Add(new FadeTriggerAction() { Duration = (uint)this.FadeDuration, FadingOut = true }); | |
view.Triggers.Add(fadeInTrigger); | |
} | |
return view; | |
} | |
} | |
public class RepeaterViewItemAddedEventArgs : EventArgs | |
{ | |
private readonly View view; | |
private readonly object model; | |
public RepeaterViewItemAddedEventArgs(View view, object model) | |
{ | |
this.view = view; | |
this.model = model; | |
} | |
public View View { get { return view; } } | |
public object Model { get { return model; } } | |
} | |
public class FadeTriggerAction : TriggerAction<View> | |
{ | |
public FadeTriggerAction() { } | |
public bool FadingOut { set; get; } = false; | |
public uint Duration { set; get; } = 500; | |
protected override void Invoke(View visual) | |
{ | |
if (FadingOut) | |
visual.FadeTo(0, Duration); | |
else | |
visual.FadeTo(1, Duration); | |
} | |
} | |
public static class IListMixIns | |
{ | |
public static void SyncList<T>( | |
this IList<T> This, | |
IEnumerable<T> sourceList) | |
{ | |
This.SyncList<T, T>(sourceList, x => x, (x, y) => x.Equals(y), null); | |
} | |
public static void SyncList<T>( | |
this IList<T> This, | |
IEnumerable sourceList, | |
Func<object, T> selector, | |
Func<object, T, bool> areEqual, | |
Action<object, T> updateExisting, | |
bool dontRemove = false) | |
{ | |
var sourceListEnum = sourceList.OfType<Object>().ToList(); | |
//passengers | |
foreach (T dest in This.ToList()) | |
{ | |
var match = sourceListEnum.FirstOrDefault(p => areEqual(p, dest)); | |
if (match != null) | |
{ | |
if (updateExisting != null) | |
updateExisting(match, dest); | |
} | |
else if (!dontRemove) | |
{ | |
This.Remove(dest); | |
} | |
} | |
sourceListEnum.Where(x => !This.Any(p => areEqual(x, p))) | |
.ToList().ForEach(p => | |
{ | |
if (This.Count >= sourceListEnum.IndexOf(p)) | |
This.Insert(sourceListEnum.IndexOf(p), selector(p)); | |
else | |
{ | |
var result = selector(p); | |
if (!EqualityComparer<T>.Default.Equals(result, default(T))) | |
This.Add(result); | |
} | |
}); | |
} | |
public static bool IsEmpty<T>(this IEnumerable<T> list) | |
{ | |
return !list.Any(); | |
} | |
public static bool IsEmpty<T>(this Array list) | |
{ | |
return list.Length == 0; | |
} | |
public static bool IsEmpty<T>(this List<T> list) | |
{ | |
return list.Count == 0; | |
} | |
public static void ForEach<T>(this IEnumerable<T> list, Action<T> doMe) | |
{ | |
foreach (var item in list) | |
{ | |
doMe(item); | |
} | |
} | |
public static void SyncList<T, Source>( | |
this IList<T> This, | |
IEnumerable<Source> sourceList, | |
Func<Source, T> selector, | |
Func<Source, T, bool> areEqual, | |
Action<Source, T> updateExisting, | |
bool dontRemove = false) | |
{ | |
//passengers | |
foreach (T dest in This.ToList()) | |
{ | |
var match = sourceList.FirstOrDefault(p => areEqual(p, dest)); | |
if (!EqualityComparer<Source>.Default.Equals(match, default(Source))) | |
{ | |
updateExisting?.Invoke(match, dest); | |
} | |
else if (!dontRemove) | |
{ | |
This.Remove(dest); | |
} | |
} | |
sourceList.Where(x => !This.Any(p => areEqual(x, p))) | |
.ToList().ForEach(p => | |
{ | |
var result = selector(p); | |
if (!EqualityComparer<T>.Default.Equals(result, default(T))) | |
This.Add(result); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment