diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/CollectionView.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/CollectionView.cs new file mode 100644 index 000000000..573ea9f14 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/CollectionView.cs @@ -0,0 +1,482 @@ +using System; +using System.Collections.Specialized; +using ElmSharp; +using EBox = ElmSharp.Box; +using EScroller = ElmSharp.Scroller; +using ESize = ElmSharp.Size; +using EPoint = ElmSharp.Point; +using System.Collections.Generic; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public class CollectionView : EBox, ICollectionViewController + { + RecyclerPool _pool = new RecyclerPool(); + ICollectionViewLayoutManager _layoutManager; + ItemAdaptor _adaptor; + EBox _innerLayout; + + Dictionary _viewHolderIndexTable = new Dictionary(); + ViewHolder _lastSelectedViewHolder; + int _selectedItemIndex = -1; + CollectionViewSelectionMode _selectionMode = CollectionViewSelectionMode.None; + + bool _requestLayoutItems = false; + SnapPointsType _snapPoints; + ESize _itemSize = new ESize(-1, -1); + + public CollectionView(EvasObject parent) : base(parent) + { + SetLayoutCallback(OnLayout); + Scroller = CreateScroller(parent); + Scroller.Show(); + PackEnd(Scroller); + Scroller.Scrolled += OnScrolled; + + _innerLayout = new EBox(parent); + _innerLayout.SetLayoutCallback(OnInnerLayout); + _innerLayout.Show(); + Scroller.SetContent(_innerLayout); + } + + public CollectionViewSelectionMode SelectionMode + { + get => _selectionMode; + set + { + _selectionMode = value; + UpdateSelectionMode(); + } + } + + public int SelectedItemIndex + { + get => _selectedItemIndex; + set + { + if (_selectedItemIndex != value) + { + _selectedItemIndex = value; + UpdateSelectedItemIndex(); + } + } + } + + public SnapPointsType SnapPointsType + { + get => _snapPoints; + set + { + _snapPoints = value; + UpdateSnapPointsType(_snapPoints); + } + } + + protected EScroller Scroller { get; } + + public ICollectionViewLayoutManager LayoutManager + { + get => _layoutManager; + set + { + OnLayoutManagerChanging(); + _layoutManager = value; + OnLayoutManagerChanged(); + } + } + + public ItemAdaptor Adaptor + { + get => _adaptor; + set + { + OnAdaptorChanging(); + _adaptor = value; + _adaptor.CollectionView = this; + OnAdaptorChanged(); + } + } + + int ICollectionViewController.Count => Adaptor?.Count ?? 0; + + EPoint ICollectionViewController.ParentPosition => new EPoint + { + X = Scroller.Geometry.X - Scroller.CurrentRegion.X, + Y = Scroller.Geometry.Y - Scroller.CurrentRegion.Y + }; + + ESize AllocatedSize { get; set; } + + Rect ViewPort => Scroller.CurrentRegion; + + public void ScrollTo(int index, ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true) + { + var itemBound = LayoutManager.GetItemBound(index); + int itemStart; + int itemEnd; + int scrollStart; + int scrollEnd; + int itemPadding = 0; + int itemSize; + int viewportSize; + + if (LayoutManager.IsHorizontal) + { + itemStart = itemBound.Left; + itemEnd = itemBound.Right; + itemSize = itemBound.Width; + scrollStart = Scroller.CurrentRegion.Left; + scrollEnd = Scroller.CurrentRegion.Right; + viewportSize = AllocatedSize.Width; + } + else + { + itemStart = itemBound.Top; + itemEnd = itemBound.Bottom; + itemSize = itemBound.Height; + scrollStart = Scroller.CurrentRegion.Top; + scrollEnd = Scroller.CurrentRegion.Bottom; + viewportSize = AllocatedSize.Height; + } + + if (position == ScrollToPosition.MakeVisible) + { + if (itemStart < scrollStart) + { + position = ScrollToPosition.Start; + } + else if (itemEnd > scrollEnd) + { + position = ScrollToPosition.End; + } + else + { + // already visible + return; + } + } + + if (itemSize < viewportSize) + { + switch (position) + { + case ScrollToPosition.Center: + itemPadding = (viewportSize - itemSize) / 2; + break; + case ScrollToPosition.End: + itemPadding = (viewportSize - itemSize); + break; + } + itemSize = viewportSize; + } + + if (LayoutManager.IsHorizontal) + { + itemBound.X -= itemPadding; + itemBound.Width = itemSize; + } + else + { + itemBound.Y -= itemPadding; + itemBound.Height = itemSize; + } + + Scroller.ScrollTo(itemBound, animate); + } + + public void ScrollTo(object item, ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true) + { + ScrollTo(Adaptor.GetItemIndex(item), position, animate); + } + + void ICollectionViewController.RequestLayoutItems() => RequestLayoutItems(); + + + ESize ICollectionViewController.GetItemSize() + { + if (Adaptor == null) + { + return new ESize(0, 0); + } + if (_itemSize.Width > 0 && _itemSize.Height > 0) + { + return _itemSize; + } + + _itemSize = Adaptor.MeasureItem(AllocatedSize.Width, AllocatedSize.Height); + _itemSize.Width = Math.Max(_itemSize.Width, 10); + _itemSize.Height = Math.Max(_itemSize.Height, 10); + + if (_snapPoints != SnapPointsType.None) + { + Scroller.SetPageSize(_itemSize.Width, _itemSize.Height); + } + return _itemSize; + } + + ViewHolder ICollectionViewController.RealizeView(int index) + { + if (Adaptor == null) + return null; + + var holder = _pool.GetRecyclerView(); + if (holder != null) + { + holder.Show(); + } + else + { + var content = Adaptor.CreateNativeView(this); + holder = new ViewHolder(this); + holder.RequestSelected += OnRequestItemSelection; + holder.Content = content; + _innerLayout.PackEnd(holder); + } + + Adaptor.SetBinding(holder.Content, index); + _viewHolderIndexTable[holder] = index; + if (index == SelectedItemIndex) + { + OnRequestItemSelection(holder, EventArgs.Empty); + } + return holder; + } + + void OnRequestItemSelection(object sender, EventArgs e) + { + if (SelectionMode == CollectionViewSelectionMode.None) + return; + + + if (_lastSelectedViewHolder != null) + { + _lastSelectedViewHolder.State = ViewHolderState.Normal; + } + + _lastSelectedViewHolder = sender as ViewHolder; + if (_lastSelectedViewHolder != null) + { + _lastSelectedViewHolder.State = ViewHolderState.Selected; + if (_viewHolderIndexTable.TryGetValue(_lastSelectedViewHolder, out int index)) + { + _selectedItemIndex = index; + Adaptor?.SendItemSelected(index); + } + } + } + + void ICollectionViewController.UnrealizeView(ViewHolder view) + { + _viewHolderIndexTable.Remove(view); + view.ResetState(); + view.Hide(); + _pool.AddRecyclerView(view); + } + + protected virtual EScroller CreateScroller(EvasObject parent) + { + return new EScroller(parent); + } + + void UpdateSelectedItemIndex() + { + if (SelectionMode == CollectionViewSelectionMode.None) + return; + + ViewHolder holder = null; + foreach (var item in _viewHolderIndexTable) + { + if (item.Value == SelectedItemIndex) + { + holder = item.Key; + break; + } + } + OnRequestItemSelection(holder, EventArgs.Empty); + } + + void UpdateSelectionMode() + { + if (SelectionMode == CollectionViewSelectionMode.None) + { + if (_lastSelectedViewHolder != null) + { + _lastSelectedViewHolder.State = ViewHolderState.Normal; + _lastSelectedViewHolder = null; + } + _selectedItemIndex = -1; + } + } + + + void OnLayoutManagerChanging() + { + _layoutManager?.Reset(); + } + + void OnLayoutManagerChanged() + { + if (_layoutManager == null) + return; + + _layoutManager.CollectionView = this; + _layoutManager.SizeAllocated(AllocatedSize); + RequestLayoutItems(); + } + + void OnAdaptorChanging() + { + _layoutManager?.Reset(); + if (Adaptor != null) + { + _pool.Clear(Adaptor); + (Adaptor as INotifyCollectionChanged).CollectionChanged -= OnCollectionChanged; + Adaptor.CollectionView = null; + } + } + void OnAdaptorChanged() + { + if (_adaptor == null) + return; + + _itemSize = new ESize(-1, -1); + (Adaptor as INotifyCollectionChanged).CollectionChanged += OnCollectionChanged; + + RequestLayoutItems(); + + if (LayoutManager != null) + { + var itemSize = (this as ICollectionViewController).GetItemSize(); + } + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + int idx = e.NewStartingIndex; + foreach (var item in e.NewItems) + { + LayoutManager.ItemInserted(idx++); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + int idx = e.OldStartingIndex; + foreach (var item in e.OldItems) + { + LayoutManager.ItemRemoved(idx); + } + } + else if (e.Action == NotifyCollectionChangedAction.Move) + { + LayoutManager.ItemRemoved(e.OldStartingIndex); + LayoutManager.ItemInserted(e.NewStartingIndex); + } + else if (e.Action == NotifyCollectionChangedAction.Replace) + { + LayoutManager.ItemUpdated(e.NewStartingIndex); + } + else if (e.Action == NotifyCollectionChangedAction.Reset) + { + LayoutManager.Reset(); + } + RequestLayoutItems(); + } + + Rect _lastGeometry; + void OnLayout() + { + if (_lastGeometry == Geometry) + { + return; + } + + _lastGeometry = Geometry; + Scroller.Geometry = Geometry; + Scroller.ScrollBlock = ScrollBlock.None; + AllocatedSize = Geometry.Size; + _itemSize = new ESize(-1, -1); + + if (_adaptor != null && _layoutManager != null) + { + _layoutManager?.SizeAllocated(Geometry.Size); + _layoutManager?.LayoutItems(ViewPort); + } + } + + void RequestLayoutItems() + { + if (!_requestLayoutItems) + { + _requestLayoutItems = true; + Device.BeginInvokeOnMainThread(() => + { + _requestLayoutItems = false; + if (_adaptor != null && _layoutManager != null) + { + OnInnerLayout(); + _layoutManager?.LayoutItems(ViewPort, true); + } + }); + } + } + + void OnInnerLayout() + { + var size = _layoutManager.GetScrollCanvasSize(); + _innerLayout.MinimumWidth = size.Width; + _innerLayout.MinimumHeight = size.Height; + } + + void OnScrolled(object sender, EventArgs e) + { + _layoutManager.LayoutItems(Scroller.CurrentRegion); + } + + void UpdateSnapPointsType(SnapPointsType snapPoints) + { + var itemSize = new ESize(0, 0); + switch (snapPoints) + { + case SnapPointsType.None: + Scroller.HorizontalPageScrollLimit = 0; + Scroller.VerticalPageScrollLimit = 0; + break; + case SnapPointsType.MandatorySingle: + Scroller.HorizontalPageScrollLimit = 1; + Scroller.VerticalPageScrollLimit = 1; + itemSize = (this as ICollectionViewController).GetItemSize(); + break; + case SnapPointsType.Mandatory: + Scroller.HorizontalPageScrollLimit = 0; + Scroller.VerticalPageScrollLimit = 0; + itemSize = (this as ICollectionViewController).GetItemSize(); + break; + } + Scroller.SetPageSize(itemSize.Width, itemSize.Height); + } + } + + public interface ICollectionViewController + { + EPoint ParentPosition { get; } + + ViewHolder RealizeView(int index); + + void UnrealizeView(ViewHolder view); + + void RequestLayoutItems(); + + int Count { get; } + + ESize GetItemSize(); + } + + public enum CollectionViewSelectionMode + { + None, + Single, + } + +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/EmptyItemAdaptor.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/EmptyItemAdaptor.cs new file mode 100644 index 000000000..5aa6d6d0f --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/EmptyItemAdaptor.cs @@ -0,0 +1,61 @@ +using System.Collections; +using System.Collections.Generic; + +using ESize = ElmSharp.Size; +using XLabel = Xamarin.Forms.Label; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public class EmptyItemAdaptor : ItemTemplateAdaptor + { + static DataTemplate s_defaultEmptyTemplate = new DataTemplate(typeof(EmptyView)); + public EmptyItemAdaptor(ItemsView itemsView, IEnumerable items, DataTemplate template) : base(itemsView, items, template) + { + } + + public static EmptyItemAdaptor Create(ItemsView itemsView) + { + DataTemplate template = null; + if (itemsView.EmptyView is View emptyView) + { + template = new DataTemplate(() => + { + return emptyView; + }); + } + else + { + template = itemsView.EmptyViewTemplate ?? s_defaultEmptyTemplate; + } + var empty = new List + { + itemsView.EmptyView ?? new object() + }; + return new EmptyItemAdaptor(itemsView, empty, template); + } + + public override ElmSharp.Size MeasureItem(int widthConstraint, int heightConstraint) + { + return new ESize(widthConstraint, heightConstraint); + } + + class EmptyView : StackLayout + { + public EmptyView() + { + HorizontalOptions = LayoutOptions.FillAndExpand; + VerticalOptions = LayoutOptions.FillAndExpand; + Children.Add( + new XLabel + { + Text = "No items found", + VerticalOptions = LayoutOptions.CenterAndExpand, + HorizontalOptions = LayoutOptions.CenterAndExpand, + HorizontalTextAlignment = Xamarin.Forms.TextAlignment.Center, + VerticalTextAlignment = Xamarin.Forms.TextAlignment.Center, + } + ); + } + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/GridLayoutManager.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/GridLayoutManager.cs new file mode 100644 index 000000000..56f0090a7 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/GridLayoutManager.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ElmSharp; +using ESize = ElmSharp.Size; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public class GridLayoutManager : ICollectionViewLayoutManager + { + ESize _allocatedSize; + ESize _scrollCanvasSize; + bool _isLayouting; + Rect _last; + Dictionary _realizedItem = new Dictionary(); + + public GridLayoutManager(bool isHorizontal, int span = 1) + { + IsHorizontal = isHorizontal; + Span = span; + } + + public int Span { get; internal set; } + + public bool IsHorizontal { get; } + + public ICollectionViewController CollectionView { get; set; } + + public void SizeAllocated(ESize size) + { + Reset(); + _allocatedSize = size; + _scrollCanvasSize = new ESize(0, 0); + } + + public ESize GetScrollCanvasSize() + { + if (_scrollCanvasSize.Width > 0 && _scrollCanvasSize.Height > 0) + return _scrollCanvasSize; + + var itemCount = CollectionView.Count; + var itemSize = CollectionView.GetItemSize(); + if (IsHorizontal) + { + return _scrollCanvasSize = new ESize((int)Math.Ceiling(itemCount / (double)Span) * itemSize.Width , _allocatedSize.Height); + } + else + { + return _scrollCanvasSize = new ESize(_allocatedSize.Width, (int)Math.Ceiling(itemCount / (double)Span) * itemSize.Height); + } + } + + bool ShouldRearrange(Rect viewport) + { + if (_isLayouting) + return false; + if (_last.Size != viewport.Size) + return true; + + var diff = IsHorizontal ? Math.Abs(_last.X - viewport.X) : Math.Abs(_last.Y - viewport.Y); + var margin = IsHorizontal ? CollectionView.GetItemSize().Width : CollectionView.GetItemSize().Height; + if (diff > margin) + return true; + + return false; + } + + public void LayoutItems(Rect bound, bool force) + { + // TODO : need to optimization. it was frequently called with similar bound value. + if (!ShouldRearrange(bound) && !force) + { + return; + } + _isLayouting = true; + _last = bound; + + var size = CollectionView.GetItemSize(); + var itemSize = IsHorizontal ? size.Width : size.Height; + + int padding = Span * 2; + int startIndex = Math.Max(GetStartIndex(bound, itemSize) - padding, 0); + int endIndex = Math.Min(GetEndIndex(bound, itemSize) + padding, CollectionView.Count - 1); + + foreach (var index in _realizedItem.Keys.ToList()) + { + if (index < startIndex || index > endIndex) + { + CollectionView.UnrealizeView(_realizedItem[index].View); + _realizedItem.Remove(index); + } + } + + var parent = CollectionView.ParentPosition; + for (int i = startIndex; i <= endIndex; i++) + { + EvasObject itemView = null; + if (!_realizedItem.ContainsKey(i)) + { + var view = CollectionView.RealizeView(i); + + _realizedItem[i] = new RealizedItem + { + View = view, + Index = i, + }; + itemView = view; + } + else + { + itemView = _realizedItem[i].View; + } + + var itemBound = GetItemBound(i); + itemBound.X += parent.X; + itemBound.Y += parent.Y; + itemView.Geometry = itemBound; + } + _isLayouting = false; + } + + public void UpdateSpan(int span) + { + Span = span; + _scrollCanvasSize = new ESize(0, 0); + CollectionView.RequestLayoutItems(); + } + + public void ItemInserted(int inserted) + { + var items = _realizedItem.Keys.OrderByDescending(key => key); + foreach (var index in items) + { + if (index >= inserted) + { + _realizedItem[index + 1] = _realizedItem[index]; + } + } + if (_realizedItem.ContainsKey(inserted)) + { + _realizedItem.Remove(inserted); + } + else + { + var last = items.LastOrDefault(); + if (last >= inserted) + { + _realizedItem.Remove(last); + } + } + + _scrollCanvasSize = new ESize(0, 0); + } + + public void ItemRemoved(int removed) + { + if (_realizedItem.ContainsKey(removed)) + { + CollectionView.UnrealizeView(_realizedItem[removed].View); + _realizedItem.Remove(removed); + } + + var items = _realizedItem.Keys.OrderBy(key => key); + foreach (var index in items) + { + if (index > removed) + { + _realizedItem[index - 1] = _realizedItem[index]; + } + } + + var last = items.LastOrDefault(); + if (last > removed) + { + _realizedItem.Remove(last); + } + + _scrollCanvasSize = new ESize(0, 0); + } + + public void ItemUpdated(int index) + { + if (_realizedItem.ContainsKey(index)) + { + var bound = _realizedItem[index].View.Geometry; + CollectionView.UnrealizeView(_realizedItem[index].View); + var view = CollectionView.RealizeView(index); + _realizedItem[index].View = view; + view.Geometry = bound; + } + } + + public Rect GetItemBound(int index) + { + var size = CollectionView.GetItemSize(); + if (IsHorizontal) + { + size.Height = _allocatedSize.Height / Span; + } + else + { + size.Width = _allocatedSize.Width / Span; + } + + int rowIndex = index / Span; + int colIndex = index % Span; + var colSize = IsHorizontal ? size.Height : size.Width; + + return + IsHorizontal ? + new Rect(rowIndex * size.Width, colIndex * size.Height, size.Width, size.Height) : + new Rect(colIndex * size.Width, rowIndex * size.Height, size.Width, size.Height); + } + + public void Reset() + { + foreach (var realizedItem in _realizedItem.Values) + { + CollectionView.UnrealizeView(realizedItem.View); + } + _realizedItem.Clear(); + _scrollCanvasSize = new ESize(0, 0); + } + + int GetStartIndex(Rect bound, int itemSize) + { + return ViewPortStartPoint(bound) / itemSize * Span; + } + + int GetEndIndex(Rect bound, int itemSize) + { + return (int)Math.Ceiling(ViewPortEndPoint(bound) / (double)itemSize) * Span; + } + + int ViewPortStartPoint(Rect viewPort) + { + return IsHorizontal ? viewPort.X : viewPort.Y; + } + + int ViewPortEndPoint(Rect viewPort) + { + return ViewPortStartPoint(viewPort) + ViewPortSize(viewPort); + } + + int ViewPortSize(Rect viewPort) + { + return IsHorizontal ? viewPort.Width : viewPort.Height; + } + + class RealizedItem + { + public ViewHolder View { get; set; } + public int Index { get; set; } + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ICollectionViewLayoutManager.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ICollectionViewLayoutManager.cs new file mode 100644 index 000000000..dc7bf73ee --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ICollectionViewLayoutManager.cs @@ -0,0 +1,28 @@ +using ElmSharp; +using ESize = ElmSharp.Size; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public interface ICollectionViewLayoutManager + { + ICollectionViewController CollectionView { get; set; } + + bool IsHorizontal { get; } + + void SizeAllocated(ESize size); + + ESize GetScrollCanvasSize(); + + void LayoutItems(Rect bound, bool force = false); + + Rect GetItemBound(int index); + + void ItemInserted(int index); + + void ItemRemoved(int index); + + void ItemUpdated(int index); + + void Reset(); + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemAdaptor.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemAdaptor.cs new file mode 100644 index 000000000..80ba76643 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemAdaptor.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using ElmSharp; +using ESize = ElmSharp.Size; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + + public abstract class ItemAdaptor : INotifyCollectionChanged + { + IList _itemsSource; + + public CollectionView CollectionView { get; set; } + + protected ItemAdaptor(IEnumerable items) + { + SetItemsSource(items); + } + + public event EventHandler ItemSelected; + + public virtual void SendItemSelected(int index) + { + ItemSelected?.Invoke(this, new SelectedItemChangedEventArgs(this[index], index)); + } + + public void RequestItemSelected(object item) + { + if (CollectionView != null) + { + CollectionView.SelectedItemIndex = _itemsSource.IndexOf(item); + } + } + + protected void SetItemsSource(IEnumerable items) + { + switch (items) + { + case IList list: + _itemsSource = list; + _observableCollection = list as INotifyCollectionChanged; + break; + case IEnumerable generic: + _itemsSource = new List(generic); + break; + case IEnumerable _: + _itemsSource = new List(); + foreach (var item in items) + { + _itemsSource.Add(item); + } + break; + } + } + + public object this[int index] + { + get + { + return _itemsSource[index]; + } + } + + public int Count => _itemsSource.Count; + + INotifyCollectionChanged _observableCollection; + event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged + { + add + { + if (_observableCollection != null) + { + _observableCollection.CollectionChanged += value; + } + } + remove + { + if (_observableCollection != null) + { + _observableCollection.CollectionChanged -= value; + } + } + } + + public int GetItemIndex(object item) + { + return _itemsSource.IndexOf(item); + } + + public abstract EvasObject CreateNativeView(EvasObject parent); + + public abstract void RemoveNativeView(EvasObject native); + + public abstract void SetBinding(EvasObject view, int index); + + public abstract ESize MeasureItem(int widthConstraint, int heightConstraint); + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemTemplateAdaptor.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemTemplateAdaptor.cs new file mode 100644 index 000000000..46338e136 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ItemTemplateAdaptor.cs @@ -0,0 +1,112 @@ +using System.Collections; +using System.Collections.Generic; +using ElmSharp; +using ESize = ElmSharp.Size; +using XLabel = Xamarin.Forms.Label; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public class ItemDefaultTemplateAdaptor : ItemTemplateAdaptor + { + public ItemDefaultTemplateAdaptor(ItemsView itemsView) : base(itemsView) + { + ItemTemplate = new DataTemplate(() => + { + return new StackLayout + { + BackgroundColor = Color.White, + Padding = 30, + Children = + { + new XLabel() + } + }; + }); + } + public override void SetBinding(EvasObject native, int index) + { + ((GetTemplatedView(native) as StackLayout).Children[0] as XLabel).Text = this[index].ToString(); + } + + public override ESize MeasureItem(int widthConstraint, int heightConstraint) + { + var view = (View)ItemTemplate.CreateContent(); + if (Count > 0) + { + ((view as StackLayout).Children[0] as XLabel).Text = this[0].ToString(); + } + var renderer = Platform.GetOrCreateRenderer(view); + var request = view.Measure(Forms.ConvertToScaledDP(widthConstraint), Forms.ConvertToScaledDP(heightConstraint), MeasureFlags.IncludeMargins).Request; + renderer.Dispose(); + return request.ToPixel(); + } + } + + public class ItemTemplateAdaptor : ItemAdaptor + { + Dictionary _nativeFormsTable = new Dictionary(); + ItemsView _itemsView; + + public ItemTemplateAdaptor(ItemsView itemsView) : base(itemsView.ItemsSource) + { + ItemTemplate = itemsView.ItemTemplate; + _itemsView = itemsView; + } + + protected ItemTemplateAdaptor(ItemsView itemsView, IEnumerable items, DataTemplate template) : base(items) + { + ItemTemplate = template; + _itemsView = itemsView; + } + + protected DataTemplate ItemTemplate { get; set; } + + protected View GetTemplatedView(EvasObject evasObject) + { + return _nativeFormsTable[evasObject]; + } + + public override EvasObject CreateNativeView(EvasObject parent) + { + var view = ItemTemplate.CreateContent() as View; + var renderer = Platform.GetOrCreateRenderer(view); + var native = Platform.GetOrCreateRenderer(view).NativeView; + view.Parent = _itemsView; + (renderer as LayoutRenderer)?.RegisterOnLayoutUpdated(); + + _nativeFormsTable[native] = view; + return native; + } + + public override void RemoveNativeView(EvasObject native) + { + if (_nativeFormsTable.TryGetValue(native, out View view)) + { + Platform.GetRenderer(view)?.Dispose(); + _nativeFormsTable.Remove(native); + } + } + + public override void SetBinding(EvasObject native, int index) + { + if (_nativeFormsTable.TryGetValue(native, out View view)) + { + view.BindingContext = this[index]; + } + } + + public override ESize MeasureItem(int widthConstraint, int heightConstraint) + { + var view = ItemTemplate.CreateContent() as View; + var renderer = Platform.GetOrCreateRenderer(view); + view.Parent = _itemsView; + if (Count > 0) + view.BindingContext = this[0]; + var request = view.Measure(Forms.ConvertToScaledDP(widthConstraint), Forms.ConvertToScaledDP(heightConstraint), MeasureFlags.IncludeMargins).Request; + renderer.Dispose(); + + return request.ToPixel(); + } + + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/LinearLayoutManager.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/LinearLayoutManager.cs new file mode 100644 index 000000000..d5c2fc4e0 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/LinearLayoutManager.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ElmSharp; +using ESize = ElmSharp.Size; + + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + public class LinearLayoutManager : ICollectionViewLayoutManager + { + ESize _allocatedSize; + bool _isLayouting; + Rect _last; + Dictionary _realizedItem = new Dictionary(); + + public LinearLayoutManager(bool isHorizontal) + { + IsHorizontal = isHorizontal; + } + + public bool IsHorizontal { get; } + + public ICollectionViewController CollectionView { get; set; } + + public void SizeAllocated(ESize size) + { + Reset(); + _allocatedSize = size; + _scrollCanvasSize = new ESize(0, 0); + } + + ESize _scrollCanvasSize; + + public ESize GetScrollCanvasSize() + { + if (_scrollCanvasSize.Width > 0 && _scrollCanvasSize.Height > 0) + return _scrollCanvasSize; + + var itemCount = CollectionView.Count; + var itemSize = CollectionView.GetItemSize(); + if (IsHorizontal) + { + return _scrollCanvasSize = new ESize(itemCount * itemSize.Width, _allocatedSize.Height); + } + else + { + return _scrollCanvasSize = new ESize(_allocatedSize.Width, itemCount * itemSize.Height); + } + } + + bool ShouldRearrange(Rect viewport) + { + if (_isLayouting) + return false; + if (_last.Size != viewport.Size) + return true; + + var diff = IsHorizontal ? Math.Abs(_last.X - viewport.X) : Math.Abs(_last.Y - viewport.Y); + var margin = IsHorizontal ? CollectionView.GetItemSize().Width : CollectionView.GetItemSize().Height; + if (diff > margin) + return true; + + return false; + } + + public void LayoutItems(Rect bound, bool force) + { + // TODO : need to optimization. it was frequently called with similar bound value. + if (!ShouldRearrange(bound) && !force) + { + return; + } + _isLayouting = true; + _last = bound; + + var size = CollectionView.GetItemSize(); + var itemSize = IsHorizontal ? size.Width : size.Height; + int startIndex = Math.Max(GetStartIndex(bound, itemSize) - 2, 0); + int endIndex = Math.Min(GetEndIndex(bound, itemSize) + 2, CollectionView.Count - 1); + + foreach (var index in _realizedItem.Keys.ToList()) + { + + if (index < startIndex || index > endIndex) + { + CollectionView.UnrealizeView(_realizedItem[index].View); + _realizedItem.Remove(index); + } + } + + var parent = CollectionView.ParentPosition; + for (int i = startIndex; i <= endIndex; i++) + { + EvasObject itemView = null; + if (!_realizedItem.ContainsKey(i)) + { + var view = CollectionView.RealizeView(i); + _realizedItem[i] = new RealizedItem + { + View = view, + Index = i, + }; + itemView = view; + } + else + { + itemView = _realizedItem[i].View; + } + var itemBound = GetItemBound(i); + itemBound.X += parent.X; + itemBound.Y += parent.Y; + itemView.Geometry = itemBound; + } + _isLayouting = false; + } + + public void ItemInserted(int inserted) + { + var items = _realizedItem.Keys.OrderByDescending(key => key); + foreach (var index in items) + { + if (index >= inserted) + { + _realizedItem[index + 1] = _realizedItem[index]; + } + } + if (_realizedItem.ContainsKey(inserted)) + { + _realizedItem.Remove(inserted); + } + else + { + var last = items.LastOrDefault(); + if (last >= inserted) + { + _realizedItem.Remove(last); + } + } + + _scrollCanvasSize = new ESize(0, 0); + } + + public void ItemRemoved(int removed) + { + if (_realizedItem.ContainsKey(removed)) + { + CollectionView.UnrealizeView(_realizedItem[removed].View); + _realizedItem.Remove(removed); + } + + var items = _realizedItem.Keys.OrderBy(key => key); + foreach (var index in items) + { + if (index > removed) + { + _realizedItem[index - 1] = _realizedItem[index]; + } + } + + var last = items.LastOrDefault(); + if (last > removed) + { + _realizedItem.Remove(last); + } + + _scrollCanvasSize = new ESize(0, 0); + } + + public void ItemUpdated(int index) + { + if (_realizedItem.ContainsKey(index)) + { + var bound = _realizedItem[index].View.Geometry; + CollectionView.UnrealizeView(_realizedItem[index].View); + var view = CollectionView.RealizeView(index); + _realizedItem[index].View = view; + view.Geometry = bound; + } + } + + public Rect GetItemBound(int index) + { + var size = CollectionView.GetItemSize(); + if (IsHorizontal) + { + size.Height = _allocatedSize.Height; + } + else + { + size.Width = _allocatedSize.Width; + } + return + IsHorizontal ? + new Rect(index * size.Width, 0, size.Width, size.Height) : + new Rect(0, index * size.Height, size.Width, size.Height); + } + + public void Reset() + { + foreach (var realizedItem in _realizedItem.Values) + { + CollectionView.UnrealizeView(realizedItem.View); + } + _realizedItem.Clear(); + _scrollCanvasSize = new ESize(0, 0); + } + + int GetStartIndex(Rect bound, int itemSize) + { + return ViewPortStartPoint(bound) / itemSize; + } + + int GetEndIndex(Rect bound, int itemSize) + { + return (int)Math.Ceiling(ViewPortEndPoint(bound) / (double)itemSize); + } + + int ViewPortStartPoint(Rect viewPort) + { + return IsHorizontal ? viewPort.X : viewPort.Y; + } + + int ViewPortEndPoint(Rect viewPort) + { + return ViewPortStartPoint(viewPort) + ViewPortSize(viewPort); + } + + int ViewPortSize(Rect viewPort) + { + return IsHorizontal ? viewPort.Width : viewPort.Height; + } + + class RealizedItem + { + public ViewHolder View { get; set; } + public int Index { get; set; } + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/RecyclerPool.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/RecyclerPool.cs new file mode 100644 index 000000000..37f2e4fb5 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/RecyclerPool.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using ElmSharp; + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + class RecyclerPool + { + LinkedList _pool = new LinkedList(); + + public void Clear(ItemAdaptor adaptor) + { + foreach (var item in _pool) + { + adaptor.RemoveNativeView(item); + } + _pool.Clear(); + } + + public void AddRecyclerView(ViewHolder view) + { + _pool.AddLast(view); + } + + public ViewHolder GetRecyclerView() + { + if (_pool.First != null) + { + var fisrt = _pool.First; + _pool.RemoveFirst(); + return fisrt.Value; + } + return null; + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ViewHolder.cs b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ViewHolder.cs new file mode 100644 index 000000000..53e688711 --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Native/CollectionView/ViewHolder.cs @@ -0,0 +1,171 @@ +using System; +using ElmSharp; +using ERectangle = ElmSharp.Rectangle; +using EColor = ElmSharp.Color; + + +namespace Xamarin.Forms.Platform.Tizen.Native +{ + + public enum ViewHolderState + { + Normal, + Selected, + } + + public class ViewHolder : Box + { + static readonly EColor s_defaultFocusEffectColor = EColor.FromRgba(244, 244, 244, 200); + static readonly EColor s_defaultSelectedColor = EColor.FromRgba(227, 242, 253, 200); + + ERectangle _background; + Button _focusArea; + EvasObject _content; + ViewHolderState _state; + + public ViewHolder(EvasObject parent) : base(parent) + { + Initialize(parent); + } + + public EColor FocusedColor { get; set; } + public EColor SelectedColor { get; set; } + + EColor EffectiveFocusedColor => FocusedColor == EColor.Default ? s_defaultFocusEffectColor : FocusedColor; + EColor EffectiveSelectedColor => SelectedColor == EColor.Default ? s_defaultSelectedColor : FocusedColor; + + EColor FocusSelectedColor + { + get + { + var color1 = EffectiveFocusedColor; + var color2 = EffectiveSelectedColor; + return new EColor( + (color1.R + color2.R) / 2, + (color1.G + color2.G) / 2, + (color1.B + color2.B) / 2, + (color1.A + color2.A) / 2); + } + } + + public EvasObject Content + { + get + { + return _content; + } + set + { + if (_content != null) + { + UnPack(_content); + } + _content = value; + if (_content != null) + { + PackAfter(_content, _background); + _content.StackBelow(_focusArea); + } + } + } + + public ViewHolderState State + { + get { return _state; } + set + { + _state = value; + UpdateState(); + } + } + + public event EventHandler Selected; + public event EventHandler RequestSelected; + + public void ResetState() + { + State = ViewHolderState.Normal; + _background.Color = EColor.Transparent; + } + + protected void SendSelected() + { + Selected?.Invoke(this, EventArgs.Empty); + } + + protected void Initialize(EvasObject parent) + { + SetLayoutCallback(OnLayout); + + _background = new ERectangle(parent) + { + Color = EColor.Transparent + }; + _background.Show(); + + _focusArea = new Button(parent); + _focusArea.Color = EColor.Transparent; + _focusArea.BackgroundColor = EColor.Transparent; + _focusArea.SetPartColor("effect", EColor.Transparent); + _focusArea.Clicked += OnClicked; + _focusArea.Focused += OnFocused; + _focusArea.Unfocused += OnFocused; + _focusArea.KeyUp += OnKeyUp; + _focusArea.RepeatEvents = true; + _focusArea.Show(); + + PackEnd(_background); + PackEnd(_focusArea); + FocusedColor = EColor.Default; + Show(); + } + + protected virtual void OnFocused(object sender, EventArgs e) + { + if (_focusArea.IsFocused) + { + _background.Color = State == ViewHolderState.Selected ? FocusSelectedColor : EffectiveFocusedColor; + } + else + { + _background.Color = State == ViewHolderState.Selected ? EffectiveSelectedColor : EColor.Transparent; + } + } + + protected virtual void OnClicked(object sender, EventArgs e) + { + RequestSelected?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnLayout() + { + _background.Geometry = Geometry; + _focusArea.Geometry = Geometry; + if (_content != null) + { + _content.Geometry = Geometry; + } + } + + protected virtual void UpdateState() + { + if (State == ViewHolderState.Normal) + { + _background.Color = _focusArea.IsFocused ? EffectiveFocusedColor : EColor.Transparent; + } else + { + _background.Color = _focusArea.IsFocused ? FocusSelectedColor : SelectedColor; + SendSelected(); + } + } + + + void OnKeyUp(object sender, EvasKeyEventArgs e) + { + if (e.KeyName == "Enter" && _focusArea.IsFocused) + { + RequestSelected?.Invoke(this, EventArgs.Empty); + } + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Properties/AssemblyInfo.cs b/Xamarin.Forms.Platform.Tizen/Properties/AssemblyInfo.cs index 9f5d23533..8f685cc5c 100644 --- a/Xamarin.Forms.Platform.Tizen/Properties/AssemblyInfo.cs +++ b/Xamarin.Forms.Platform.Tizen/Properties/AssemblyInfo.cs @@ -36,6 +36,7 @@ using System.Reflection; [assembly: ExportRenderer(typeof(NativeViewWrapper), typeof(NativeViewWrapperRenderer))] [assembly: ExportRenderer(typeof(WebView), typeof(WebViewRenderer))] [assembly: ExportRenderer(typeof(ImageButton), typeof(ImageButtonRenderer))] +[assembly: ExportRenderer(typeof(ItemsView), typeof(ItemsViewRenderer))] [assembly: ExportImageSourceHandler(typeof(FileImageSource), typeof(FileImageSourceHandler))] [assembly: ExportImageSourceHandler(typeof(StreamImageSource), typeof(StreamImageSourceHandler))] diff --git a/Xamarin.Forms.Platform.Tizen/Renderers/ItemsViewRenderer.cs b/Xamarin.Forms.Platform.Tizen/Renderers/ItemsViewRenderer.cs new file mode 100644 index 000000000..01e8ec74e --- /dev/null +++ b/Xamarin.Forms.Platform.Tizen/Renderers/ItemsViewRenderer.cs @@ -0,0 +1,182 @@ +using System.Collections.Specialized; +using System.Linq; + +using Xamarin.Forms.Platform.Tizen.Native; + +namespace Xamarin.Forms.Platform.Tizen +{ + public class ItemsViewRenderer : ViewRenderer + { + INotifyCollectionChanged _observableSource; + + public ItemsViewRenderer() + { + RegisterPropertyHandler(ItemsView.ItemsSourceProperty, UpdateItemsSource); + RegisterPropertyHandler(ItemsView.ItemTemplateProperty, UpdateAdaptor); + RegisterPropertyHandler(ItemsView.ItemsLayoutProperty, UpdateItemsLayout); + RegisterPropertyHandler(SelectableItemsView.SelectedItemProperty, UpdateSelectedItem); + RegisterPropertyHandler(SelectableItemsView.SelectionModeProperty, UpdateSelectionMode); + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (Control == null) + { + SetNativeControl(new Native.CollectionView(Forms.NativeParent)); + } + + if (e.NewElement != null) + { + e.NewElement.ScrollToRequested += OnScrollToRequest; + } + + base.OnElementChanged(e); + UpdateAdaptor(false); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Element != null) + { + Element.ScrollToRequested -= OnScrollToRequest; + Element.ItemsLayout.PropertyChanged -= OnLayoutPropertyChanged; + } + if (_observableSource != null) + { + _observableSource.CollectionChanged -= OnCollectionChanged; + } + } + base.Dispose(disposing); + } + + void UpdateSelectedItem(bool initialize) + { + if (initialize) + return; + + if (Element is SelectableItemsView selectable) + { + Control?.Adaptor?.RequestItemSelected(selectable.SelectedItem); + } + } + + void UpdateSelectionMode() + { + if (Element is SelectableItemsView selectable) + { + Control.SelectionMode = selectable.SelectionMode == SelectionMode.None ? CollectionViewSelectionMode.None : CollectionViewSelectionMode.Single; + } + } + + void OnScrollToRequest(object sender, ScrollToRequestEventArgs e) + { + if (e.Mode == ScrollToMode.Position) + { + Control.ScrollTo(e.Index, e.ScrollToPosition, e.IsAnimated); + } + else + { + Control.ScrollTo(e.Item, e.ScrollToPosition, e.IsAnimated); + } + } + + void UpdateItemsSource(bool initialize) + { + if (Element.ItemsSource is INotifyCollectionChanged collectionChanged) + { + if (_observableSource != null) + { + _observableSource.CollectionChanged -= OnCollectionChanged; + } + _observableSource = collectionChanged; + _observableSource.CollectionChanged += OnCollectionChanged; + } + UpdateAdaptor(initialize); + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (Element.ItemsSource == null || !Element.ItemsSource.Cast().Any()) + { + Control.Adaptor = EmptyItemAdaptor.Create(Element); + } + else + { + if (Control.Adaptor is EmptyItemAdaptor) + { + UpdateAdaptor(false); + } + } + } + + void UpdateAdaptor(bool initialize) + { + if (!initialize) + { + if (Element.ItemsSource == null || !Element.ItemsSource.Cast().Any()) + { + Control.Adaptor = EmptyItemAdaptor.Create(Element); + } + else if (Element.ItemTemplate == null) + { + Control.Adaptor = new ItemDefaultTemplateAdaptor(Element); + } + else + { + Control.Adaptor = new ItemTemplateAdaptor(Element); + Control.Adaptor.ItemSelected += OnItemSelectedFromUI; + } + } + } + + void OnItemSelectedFromUI(object sender, SelectedItemChangedEventArgs e) + { + if (Element is SelectableItemsView selectableItemsView) + { + selectableItemsView.SelectedItem = e.SelectedItem; + } + } + + void UpdateItemsLayout() + { + if (Element.ItemsLayout != null) + { + Control.LayoutManager = Element.ItemsLayout.ToLayoutManager(); + Control.SnapPointsType = (Element.ItemsLayout as ItemsLayout)?.SnapPointsType ?? SnapPointsType.None; + Element.ItemsLayout.PropertyChanged += OnLayoutPropertyChanged; + } + } + + void OnLayoutPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ItemsLayout.SnapPointsType)) + { + Control.SnapPointsType = (Element.ItemsLayout as ItemsLayout)?.SnapPointsType ?? SnapPointsType.None; + } + else if (e.PropertyName == nameof(GridItemsLayout.Span)) + { + ((GridLayoutManager)(Control.LayoutManager)).UpdateSpan(((GridItemsLayout)Element.ItemsLayout).Span); + } + } + } + + static class ItemsLayoutExtension + { + public static ICollectionViewLayoutManager ToLayoutManager(this IItemsLayout layout) + { + switch (layout) + { + case ListItemsLayout listItemsLayout: + return new LinearLayoutManager(listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal); + case GridItemsLayout gridItemsLayout: + return new GridLayoutManager(gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal, gridItemsLayout.Span); + default: + break; + } + + return new LinearLayoutManager(false); + } + } +} diff --git a/Xamarin.Forms.Platform.Tizen/Renderers/VisualElementRenderer.cs b/Xamarin.Forms.Platform.Tizen/Renderers/VisualElementRenderer.cs index bc3bf74d9..286b728d0 100644 --- a/Xamarin.Forms.Platform.Tizen/Renderers/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.Tizen/Renderers/VisualElementRenderer.cs @@ -598,12 +598,12 @@ namespace Xamarin.Forms.Platform.Tizen static double ComputeAbsoluteX(VisualElement e) { - return e.X + ((e.RealParent is VisualElement) && !(e.RealParent is ListView) ? Forms.ConvertToScaledDP(Platform.GetRenderer(e.RealParent).GetNativeContentGeometry().X) : 0.0); + return e.X + ((e.RealParent is VisualElement) && !(e.RealParent is ListView || e.RealParent is ItemsView) ? Forms.ConvertToScaledDP(Platform.GetRenderer(e.RealParent).GetNativeContentGeometry().X) : 0.0); } static double ComputeAbsoluteY(VisualElement e) { - return e.Y + ((e.RealParent is VisualElement) && !(e.RealParent is ListView) ? Forms.ConvertToScaledDP(Platform.GetRenderer(e.RealParent).GetNativeContentGeometry().Y) : 0.0); + return e.Y + ((e.RealParent is VisualElement) && !(e.RealParent is ListView || e.RealParent is ItemsView) ? Forms.ConvertToScaledDP(Platform.GetRenderer(e.RealParent).GetNativeContentGeometry().Y) : 0.0); } static Point ComputeAbsolutePoint(VisualElement e)