From: SangHyeon Jade Lee Date: Tue, 9 Feb 2021 04:19:19 +0000 (+0900) Subject: [NUI] Introduce CollectionView and related classes. (#2525) X-Git-Tag: accepted/tizen/unified/20210219.040944~15 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;ds=sidebyside;h=e8633c34be7296487a54b63c46e8fc105a61cb1c;p=platform%2Fcore%2Fcsapi%2Ftizenfx.git [NUI] Introduce CollectionView and related classes. (#2525) * Introduce CollectionView and related classes. View : [ItemsView] Base class of model based view. create item by DataTemplate and layouting them with layouter using source data. inherited from ScrollableBase. [CollectionView] groupable and selectable ItemsView. Item : [ViewItem] Base item for CollectionView. [ViewItem.Internal] Internal partial class of ViewItem. [ViewItemStyle] Style class of ViewItem. [OneLineLinearItem] Linear type ViewItem of 1 line text Label and Icon, Extra View. [OutLineGridItem] Grid type ViewItem of outline caption Label and ImageView Image and View Badge. Layouter : [ItemsLayouter] Base compositor class of item layouter on ItemsView. [LinearLayouter] Layouter class of layouting item in linear list. restricted for CollectionView class only. [GridLayouter] Layouter class of layouting item in grid list. restricted for CollectionView class only. Item Source : [ItemsSourceFactory] Factory of creating IItemSource from IEnumerable source. [IItemSource] Interface for encapsulated data source from IEnumerable user source. [IGroupedItemSource] Interface for grouped user data. [EmptySource] Empty data source. [ListSource] List data source. [ObservableItemSource] Item source who provide observable notifications. [ObservableGroupedSource] Grouped item source who provide observable notifications. [UngroupedItemSource] Ungrouped item source. Enum and Interface : [ICollectionChangedNotifier] Interface of collection changed notifier. [ItemSelectionMode] Mode enum value for Selection. [ItemSizingStrategy] Sizing strategy for item measure. [SelectionChangedEventArgs] Event for Selection changed notify. Helper Class : [MarshalingObservableCollection] Marshaling ObservableCollection for itemSource. [SelectionList] Internal list for SelectionModel. Other Changes : Moving and change internal to public of DataTemplate related classes. Known Issues : - Empty items or pages shows when scroll fast, jump by ScrollTo(). - ScrollTo() is moving incorrectly. - Scrolling event passing scrollPosition negative value. - Application cannot initialize the cached ViewItem. - Image trembling while scrolling. Remaining Works : - Implement groupable features. - Implement observable changes(ICollectionChangedNotifier). - Implement dynamic size and dynamic template selector. - Implement Item / Layouter animations and support custom animations. - Adding samples and test. - Adding Unit Test. - Documents and guides. Design Documents : https://code.sec.samsung.net/confluence/display/ENUIFWC/5.+Model-based++User+Interfaces * [NUI] Update templates to EditorBrowsableState.Never * [CollectionView] fix wrong initialize layouter and item count which makes missing 0 and last indexed item in Header/Footer exist * [CollectionView] fix missing call of OnRelayout in base class. * [CollectionView] Implement Groupable Features and Refactoring Items 1. Implement Groupable features in CollectionView, LinearLayouter, GridLayouter. 2. Refactoring items and adding styles. OneLineLinearItem => DefaultLinearItem : SubLabel added. Use RelativeLayout. Apply DefaultLinerItemStyle. OutLineGridItem => DefaultGridItem : Label changed Caption. Use RelativeLayout. Support CaptionOrientation. Apply DefaultGridItemStyle. DefaultTitleItem is added for Header / GroupHeader. 3. Sample is udpated. 4. Remove _ in the code. * [CollectionView] Refactoring class directories - Move all item source related classes in ItemSource - Move all item class in Item - Move all layouter class in Layouter * [NUI] add copy-right license pre-comments * Introduce CollectionView and related classes. View : [ItemsView] Base class of model based view. create item by DataTemplate and layouting them with layouter using source data. inherited from ScrollableBase. [CollectionView] groupable and selectable ItemsView. Item : [ViewItem] Base item for CollectionView. [ViewItem.Internal] Internal partial class of ViewItem. [ViewItemStyle] Style class of ViewItem. [OneLineLinearItem] Linear type ViewItem of 1 line text Label and Icon, Extra View. [OutLineGridItem] Grid type ViewItem of outline caption Label and ImageView Image and View Badge. Layouter : [ItemsLayouter] Base compositor class of item layouter on ItemsView. [LinearLayouter] Layouter class of layouting item in linear list. restricted for CollectionView class only. [GridLayouter] Layouter class of layouting item in grid list. restricted for CollectionView class only. Item Source : [ItemsSourceFactory] Factory of creating IItemSource from IEnumerable source. [IItemSource] Interface for encapsulated data source from IEnumerable user source. [IGroupedItemSource] Interface for grouped user data. [EmptySource] Empty data source. [ListSource] List data source. [ObservableItemSource] Item source who provide observable notifications. [ObservableGroupedSource] Grouped item source who provide observable notifications. [UngroupedItemSource] Ungrouped item source. Enum and Interface : [ICollectionChangedNotifier] Interface of collection changed notifier. [ItemSelectionMode] Mode enum value for Selection. [ItemSizingStrategy] Sizing strategy for item measure. [SelectionChangedEventArgs] Event for Selection changed notify. Helper Class : [MarshalingObservableCollection] Marshaling ObservableCollection for itemSource. [SelectionList] Internal list for SelectionModel. Other Changes : Moving and change internal to public of DataTemplate related classes. Known Issues : - Empty items or pages shows when scroll fast, jump by ScrollTo(). - ScrollTo() is moving incorrectly. - Scrolling event passing scrollPosition negative value. - Application cannot initialize the cached ViewItem. - Image trembling while scrolling. Remaining Works : - Implement groupable features. - Implement observable changes(ICollectionChangedNotifier). - Implement dynamic size and dynamic template selector. - Implement Item / Layouter animations and support custom animations. - Adding samples and test. - Adding Unit Test. - Documents and guides. Design Documents : https://code.sec.samsung.net/confluence/display/ENUIFWC/5.+Model-based++User+Interfaces * [NUI] Update templates to EditorBrowsableState.Never * [CollectionView] fix wrong initialize layouter and item count which makes missing 0 and last indexed item in Header/Footer exist * [CollectionView] fix missing call of OnRelayout in base class. * [CollectionView] Implement Groupable Features and Refactoring Items 1. Implement Groupable features in CollectionView, LinearLayouter, GridLayouter. 2. Refactoring items and adding styles. OneLineLinearItem => DefaultLinearItem : SubLabel added. Use RelativeLayout. Apply DefaultLinerItemStyle. OutLineGridItem => DefaultGridItem : Label changed Caption. Use RelativeLayout. Support CaptionOrientation. Apply DefaultGridItemStyle. DefaultTitleItem is added for Header / GroupHeader. 3. Sample is udpated. 4. Remove _ in the code. * [CollectionView] Refactoring class directories - Move all item source related classes in ItemSource - Move all item class in Item - Move all layouter class in Layouter * [NUI] add copy-right license pre-comments * [NUI] Remove Old RecyclerView and Renamed ItemsView to RecyclerView Old RecyclerView was never published or browsed, so get rid of this class and renamed ItemsView to RecyclerView. RecyclerVIew/ -> Removed. ItemsView -> RecyclerView ViewItem -> RecyclerViewItem ViewItemStyle -> RecyclerViewItemStyle fix warnings. * [NUI] fix build errors and comments. FishEyeLayoutManager and WearableList no longer supported as recyclerView is removed. * [NUI] Update Samples * [NUI] apply cache in group header and footer * [NUI] re-alive wearable list and move RecyclerView in wearble. * Introduce CollectionView and related classes. View : [ItemsView] Base class of model based view. create item by DataTemplate and layouting them with layouter using source data. inherited from ScrollableBase. [CollectionView] groupable and selectable ItemsView. Item : [ViewItem] Base item for CollectionView. [ViewItem.Internal] Internal partial class of ViewItem. [ViewItemStyle] Style class of ViewItem. [OneLineLinearItem] Linear type ViewItem of 1 line text Label and Icon, Extra View. [OutLineGridItem] Grid type ViewItem of outline caption Label and ImageView Image and View Badge. Layouter : [ItemsLayouter] Base compositor class of item layouter on ItemsView. [LinearLayouter] Layouter class of layouting item in linear list. restricted for CollectionView class only. [GridLayouter] Layouter class of layouting item in grid list. restricted for CollectionView class only. Item Source : [ItemsSourceFactory] Factory of creating IItemSource from IEnumerable source. [IItemSource] Interface for encapsulated data source from IEnumerable user source. [IGroupedItemSource] Interface for grouped user data. [EmptySource] Empty data source. [ListSource] List data source. [ObservableItemSource] Item source who provide observable notifications. [ObservableGroupedSource] Grouped item source who provide observable notifications. [UngroupedItemSource] Ungrouped item source. Enum and Interface : [ICollectionChangedNotifier] Interface of collection changed notifier. [ItemSelectionMode] Mode enum value for Selection. [ItemSizingStrategy] Sizing strategy for item measure. [SelectionChangedEventArgs] Event for Selection changed notify. Helper Class : [MarshalingObservableCollection] Marshaling ObservableCollection for itemSource. [SelectionList] Internal list for SelectionModel. Other Changes : Moving and change internal to public of DataTemplate related classes. Known Issues : - Empty items or pages shows when scroll fast, jump by ScrollTo(). - ScrollTo() is moving incorrectly. - Scrolling event passing scrollPosition negative value. - Application cannot initialize the cached ViewItem. - Image trembling while scrolling. Remaining Works : - Implement groupable features. - Implement observable changes(ICollectionChangedNotifier). - Implement dynamic size and dynamic template selector. - Implement Item / Layouter animations and support custom animations. - Adding samples and test. - Adding Unit Test. - Documents and guides. Design Documents : https://code.sec.samsung.net/confluence/display/ENUIFWC/5.+Model-based++User+Interfaces * [NUI] Update templates to EditorBrowsableState.Never * [CollectionView] fix wrong initialize layouter and item count which makes missing 0 and last indexed item in Header/Footer exist * [CollectionView] fix missing call of OnRelayout in base class. * [CollectionView] Implement Groupable Features and Refactoring Items 1. Implement Groupable features in CollectionView, LinearLayouter, GridLayouter. 2. Refactoring items and adding styles. OneLineLinearItem => DefaultLinearItem : SubLabel added. Use RelativeLayout. Apply DefaultLinerItemStyle. OutLineGridItem => DefaultGridItem : Label changed Caption. Use RelativeLayout. Support CaptionOrientation. Apply DefaultGridItemStyle. DefaultTitleItem is added for Header / GroupHeader. 3. Sample is udpated. 4. Remove _ in the code. * [CollectionView] Refactoring class directories - Move all item source related classes in ItemSource - Move all item class in Item - Move all layouter class in Layouter * [NUI] add copy-right license pre-comments * Introduce CollectionView and related classes. View : [ItemsView] Base class of model based view. create item by DataTemplate and layouting them with layouter using source data. inherited from ScrollableBase. [CollectionView] groupable and selectable ItemsView. Item : [ViewItem] Base item for CollectionView. [ViewItem.Internal] Internal partial class of ViewItem. [ViewItemStyle] Style class of ViewItem. [OneLineLinearItem] Linear type ViewItem of 1 line text Label and Icon, Extra View. [OutLineGridItem] Grid type ViewItem of outline caption Label and ImageView Image and View Badge. Layouter : [ItemsLayouter] Base compositor class of item layouter on ItemsView. [LinearLayouter] Layouter class of layouting item in linear list. restricted for CollectionView class only. [GridLayouter] Layouter class of layouting item in grid list. restricted for CollectionView class only. Item Source : [ItemsSourceFactory] Factory of creating IItemSource from IEnumerable source. [IItemSource] Interface for encapsulated data source from IEnumerable user source. [IGroupedItemSource] Interface for grouped user data. [EmptySource] Empty data source. [ListSource] List data source. [ObservableItemSource] Item source who provide observable notifications. [ObservableGroupedSource] Grouped item source who provide observable notifications. [UngroupedItemSource] Ungrouped item source. Enum and Interface : [ICollectionChangedNotifier] Interface of collection changed notifier. [ItemSelectionMode] Mode enum value for Selection. [ItemSizingStrategy] Sizing strategy for item measure. [SelectionChangedEventArgs] Event for Selection changed notify. Helper Class : [MarshalingObservableCollection] Marshaling ObservableCollection for itemSource. [SelectionList] Internal list for SelectionModel. Other Changes : Moving and change internal to public of DataTemplate related classes. Known Issues : - Empty items or pages shows when scroll fast, jump by ScrollTo(). - ScrollTo() is moving incorrectly. - Scrolling event passing scrollPosition negative value. - Application cannot initialize the cached ViewItem. - Image trembling while scrolling. Remaining Works : - Implement groupable features. - Implement observable changes(ICollectionChangedNotifier). - Implement dynamic size and dynamic template selector. - Implement Item / Layouter animations and support custom animations. - Adding samples and test. - Adding Unit Test. - Documents and guides. Design Documents : https://code.sec.samsung.net/confluence/display/ENUIFWC/5.+Model-based++User+Interfaces * [NUI] Update templates to EditorBrowsableState.Never * [CollectionView] fix wrong initialize layouter and item count which makes missing 0 and last indexed item in Header/Footer exist * [CollectionView] fix missing call of OnRelayout in base class. * [CollectionView] Implement Groupable Features and Refactoring Items 1. Implement Groupable features in CollectionView, LinearLayouter, GridLayouter. 2. Refactoring items and adding styles. OneLineLinearItem => DefaultLinearItem : SubLabel added. Use RelativeLayout. Apply DefaultLinerItemStyle. OutLineGridItem => DefaultGridItem : Label changed Caption. Use RelativeLayout. Support CaptionOrientation. Apply DefaultGridItemStyle. DefaultTitleItem is added for Header / GroupHeader. 3. Sample is udpated. 4. Remove _ in the code. * [CollectionView] Refactoring class directories - Move all item source related classes in ItemSource - Move all item class in Item - Move all layouter class in Layouter * [NUI] add copy-right license pre-comments * [NUI] Remove Old RecyclerView and Renamed ItemsView to RecyclerView Old RecyclerView was never published or browsed, so get rid of this class and renamed ItemsView to RecyclerView. RecyclerVIew/ -> Removed. ItemsView -> RecyclerView ViewItem -> RecyclerViewItem ViewItemStyle -> RecyclerViewItemStyle fix warnings. * [NUI] fix build errors and comments. FishEyeLayoutManager and WearableList no longer supported as recyclerView is removed. * [NUI] Update Samples * [NUI] apply cache in group header and footer * [NUI] re-alive wearable list and move RecyclerView in wearble. Co-authored-by: dongsug-song <35130733+dongsug-song@users.noreply.github.com> --- diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/CollectionView.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/CollectionView.cs new file mode 100644 index 0000000..fc69968 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/CollectionView.cs @@ -0,0 +1,1068 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Input; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Components +{ + /// + /// This class provides a View that can layouting items in list and grid with high performance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class CollectionView : RecyclerView + { + /// + /// Binding Property of selected item in single selection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty SelectedItemProperty = + BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(CollectionView), null, + propertyChanged: (bindable, oldValue, newValue)=> + { + var colView = (CollectionView)bindable; + oldValue = colView.selectedItem; + colView.selectedItem = newValue; + var args = new SelectionChangedEventArgs(oldValue, newValue); + + foreach (RecyclerViewItem item in colView.ContentContainer.Children.Where((item) => item is RecyclerViewItem)) + { + if (item.BindingContext == null) continue; + if (item.BindingContext == oldValue) item.IsSelected = false; + else if (item.BindingContext == newValue) item.IsSelected = true; + } + + SelectionPropertyChanged(colView, args); + }, + defaultValueCreator: (bindable)=> + { + var colView = (CollectionView)bindable; + return colView.selectedItem; + }); + + /// + /// Binding Property of selected items list in multiple selection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty SelectedItemsProperty = + BindableProperty.Create(nameof(SelectedItems), typeof(IList), typeof(CollectionView), null, + propertyChanged: (bindable, oldValue, newValue)=> + { + var colView = (CollectionView)bindable; + var oldSelection = colView.selectedItems ?? selectEmpty; + //FIXME : CoerceSelectedItems calls only isCreatedByXaml + var newSelection = (SelectionList)CoerceSelectedItems(colView, newValue); + colView.selectedItems = newSelection; + colView.SelectedItemsPropertyChanged(oldSelection, newSelection); + }, + defaultValueCreator: (bindable) => + { + var colView = (CollectionView)bindable; + colView.selectedItems = colView.selectedItems ?? new SelectionList(colView); + return colView.selectedItems; + }); + + /// + /// Binding Property of selected items list in multiple selection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty SelectionModeProperty = + BindableProperty.Create(nameof(SelectionMode), typeof(ItemSelectionMode), typeof(CollectionView), ItemSelectionMode.None, + propertyChanged: (bindable, oldValue, newValue)=> + { + var colView = (CollectionView)bindable; + oldValue = colView.selectionMode; + colView.selectionMode = (ItemSelectionMode)newValue; + SelectionModePropertyChanged(colView, oldValue, newValue); + }, + defaultValueCreator: (bindable) => + { + var colView = (CollectionView)bindable; + return colView.selectionMode; + }); + + + private static readonly IList selectEmpty = new List(0); + private DataTemplate itemTemplate = null; + private IEnumerable itemsSource = null; + private ItemsLayouter itemsLayouter = null; + private DataTemplate groupHeaderTemplate; + private DataTemplate groupFooterTemplate; + private bool isGrouped; + private bool wasRelayouted = false; + private bool needInitalizeLayouter = false; + private object selectedItem; + private SelectionList selectedItems; + private bool suppressSelectionChangeNotification; + private ItemSelectionMode selectionMode = ItemSelectionMode.None; + private RecyclerViewItem header; + private RecyclerViewItem footer; + private View focusedView; + private int prevFocusedDataIndex = 0; + private List recycleGroupHeaderCache { get; } = new List(); + private List recycleGroupFooterCache { get; } = new List(); + + /// + /// Base constructor. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CollectionView() : base() + { + FocusGroup = true; + SetKeyboardNavigationSupport(true); + } + + /// + /// Base constructor with ItemsSource + /// + /// item's data source + [EditorBrowsable(EditorBrowsableState.Never)] + public CollectionView(IEnumerable itemsSource) : this() + { + ItemsSource = itemsSource; + } + + /// + /// Base constructor with ItemsSource, ItemsLayouter and ItemTemplate + /// + /// item's data source + /// item's layout manager + /// item's view template with data bindings + [EditorBrowsable(EditorBrowsableState.Never)] + public CollectionView(IEnumerable itemsSource, ItemsLayouter layouter, DataTemplate template) : this() + { + ItemsSource = itemsSource; + ItemTemplate = template; + ItemsLayouter = layouter; + } + + /// + /// Event of Selection changed. + /// old selection list and new selection will be provided. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public event EventHandler SelectionChanged; + + /// + /// Align item in the viewport when ScrollTo() calls. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public enum ItemScrollTo + { + /// + /// Scroll to show item in nearest viewport on scroll direction. + /// item is above the scroll viewport, item will be came into front, + /// item is under the scroll viewport, item will be came into end, + /// item is in the scroll viewport, no scroll. + /// + Nearest, + /// + /// Scroll to show item in start of the viewport. + /// + Start, + /// + /// Scroll to show item in center of the viewport. + /// + Center, + /// + /// Scroll to show item in end of the viewport. + /// + End, + } + + /// + /// Item's source data. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override IEnumerable ItemsSource + { + get + { + return itemsSource; + } + set + { + + itemsSource = value; + if (value == null) + { + if (InternalItemSource != null) InternalItemSource.Dispose(); + //layouter.Clear() + return; + } + if (InternalItemSource != null) InternalItemSource.Dispose(); + InternalItemSource = ItemsSourceFactory.Create(this); + + if (itemsLayouter == null) return; + + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// DataTemplate for items. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override DataTemplate ItemTemplate + { + get + { + return itemTemplate; + } + set + { + itemTemplate = value; + if (value == null) + { + //layouter.clear() + return; + } + + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// Items Layouter. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual ItemsLayouter ItemsLayouter + { + get + { + return itemsLayouter; + } + set + { + itemsLayouter = value; + base.InternalItemsLayouter = ItemsLayouter; + if (value == null) + { + needInitalizeLayouter = false; + return; + } + + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// Scrolling direction to display items layout. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new Direction ScrollingDirection + { + get + { + return base.ScrollingDirection; + } + set + { + base.ScrollingDirection = value; + + if (ScrollingDirection == Direction.Horizontal) + { + ContentContainer.SizeWidth = ItemsLayouter.CalculateLayoutOrientationSize(); + } + else + { + ContentContainer.SizeHeight = ItemsLayouter.CalculateLayoutOrientationSize(); + } + } + } + + /// + /// Selected item in single selection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public object SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + /// + /// Selected items list in multiple selection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IList SelectedItems + { + get => (IList)GetValue(SelectedItemsProperty); + // set => SetValue(SelectedItemsProperty, new SelectionList(this, value)); + } + + /// + /// Selection mode to handle items selection. See ItemSelectionMode for details. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ItemSelectionMode SelectionMode + { + get => (ItemSelectionMode)GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); + } + + /// + /// Command of selection changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ICommand SelectionChangedCommand { set; get; } + + /// + /// Command parameter of selection changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public object SelectionChangedCommandParameter { set; get; } + + /// + /// Size strategy of measuring scroll content. see details in ItemSizingStrategy. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ItemSizingStrategy SizingStrategy { get; set; } + + /// + /// Header item which placed in top-most position. + /// note : internal index and count will be increased. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItem Header + { + get => header; + set + { + if (header != null) + { + //ContentContainer.Remove(header); + Utility.Dispose(header); + } + if (value != null) + { + value.Index = 0; + value.ParentItemsView = this; + value.IsHeader = true; + ContentContainer.Add(value); + } + header = value; + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// Footer item which placed in bottom-most position. + /// note : internal count will be increased. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItem Footer + { + get => footer; + set + { + if (footer != null) + { + //ContentContainer.Remove(footer); + Utility.Dispose(footer); + } + if (value != null) + { + value.Index = InternalItemSource?.Count ?? 0; + value.ParentItemsView = this; + value.IsFooter = true; + ContentContainer.Add(value); + } + footer = value; + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// Boolean flag of group feature existence. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsGrouped + { + get => isGrouped; + set + { + isGrouped = value; + needInitalizeLayouter = true; + //Need to re-intialize Internal Item Source. + if (InternalItemSource != null) + { + InternalItemSource.Dispose(); + InternalItemSource = null; + } + if (ItemsSource != null) + InternalItemSource = ItemsSourceFactory.Create(this); + Init(); + } + } + + /// + /// DataTemplate of group header. Group feature is not supported yet. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DataTemplate GroupHeaderTemplate + { + get + { + return groupHeaderTemplate; + } + set + { + groupHeaderTemplate = value; + needInitalizeLayouter = true; + Init(); + } + } + + /// + /// DataTemplate of group footer. Group feature is not supported yet. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DataTemplate GroupFooterTemplate + { + get + { + return groupFooterTemplate; + } + set + { + groupFooterTemplate = value; + needInitalizeLayouter = true; + Init(); + } + } + + + /// + /// Internal encapsulated items data source. + /// + internal new IGroupableItemSource InternalItemSource + { + get + { + return (base.InternalItemSource as IGroupableItemSource); + } + set + { + base.InternalItemSource = value; + } + } + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnRelayout(Vector2 size, RelayoutContainer container) + { + base.OnRelayout(size, container); + + wasRelayouted = true; + if (needInitalizeLayouter) Init(); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override View GetNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + { + View nextFocusedView = null; + + if (focusedView == null) + { + // If focusedView is null, find child which has previous data index + if (ContentContainer.Children.Count > 0 && InternalItemSource.Count > 0) + { + for (int i = 0; i < ContentContainer.Children.Count; i++) + { + RecyclerViewItem item = Children[i] as RecyclerViewItem; + if (item?.Index == prevFocusedDataIndex) + { + nextFocusedView = item; + break; + } + } + } + } + else + { + // If this is not first focus, request next focus to Layouter + nextFocusedView = ItemsLayouter.RequestNextFocusableView(currentFocusedView, direction, loopEnabled); + } + + if (nextFocusedView != null) + { + // Check next focused view is inside of visible area. + // If it is not, move scroll position to make it visible. + Position scrollPosition = ContentContainer.CurrentPosition; + float targetPosition = -(ScrollingDirection == Direction.Horizontal ? scrollPosition.X : scrollPosition.Y); + + float left = nextFocusedView.Position.X; + float right = nextFocusedView.Position.X + nextFocusedView.Size.Width; + float top = nextFocusedView.Position.Y; + float bottom = nextFocusedView.Position.Y + nextFocusedView.Size.Height; + + float visibleRectangleLeft = -scrollPosition.X; + float visibleRectangleRight = -scrollPosition.X + Size.Width; + float visibleRectangleTop = -scrollPosition.Y; + float visibleRectangleBottom = -scrollPosition.Y + Size.Height; + + if (ScrollingDirection == Direction.Horizontal) + { + if ((direction == View.FocusDirection.Left || direction == View.FocusDirection.Up) && left < visibleRectangleLeft) + { + targetPosition = left; + } + else if ((direction == View.FocusDirection.Right || direction == View.FocusDirection.Down) && right > visibleRectangleRight) + { + targetPosition = right - Size.Width; + } + } + else + { + if ((direction == View.FocusDirection.Up || direction == View.FocusDirection.Left) && top < visibleRectangleTop) + { + targetPosition = top; + } + else if ((direction == View.FocusDirection.Down || direction == View.FocusDirection.Right) && bottom > visibleRectangleBottom) + { + targetPosition = bottom - Size.Height; + } + } + + focusedView = nextFocusedView; + prevFocusedDataIndex = (nextFocusedView as RecyclerViewItem)?.Index ?? -1; + + ScrollTo(targetPosition, true); + } + else + { + // If nextView is null, it means that we should move focus to outside of Control. + // Return FocusableView depending on direction. + switch (direction) + { + case View.FocusDirection.Left: + { + nextFocusedView = LeftFocusableView; + break; + } + case View.FocusDirection.Right: + { + nextFocusedView = RightFocusableView; + break; + } + case View.FocusDirection.Up: + { + nextFocusedView = UpFocusableView; + break; + } + case View.FocusDirection.Down: + { + nextFocusedView = DownFocusableView; + break; + } + } + + if(nextFocusedView != null) + { + focusedView = null; + } + else + { + //If FocusableView doesn't exist, not move focus. + nextFocusedView = focusedView; + } + } + + return nextFocusedView; + } + + /// + /// Update selected items list in multiple selection. + /// + /// updated selection list by user + [EditorBrowsable(EditorBrowsableState.Never)] + public void UpdateSelectedItems(IList newSelection) + { + var oldSelection = new List(SelectedItems); + + suppressSelectionChangeNotification = true; + + SelectedItems.Clear(); + + if (newSelection?.Count > 0) + { + for (int n = 0; n < newSelection.Count; n++) + { + SelectedItems.Add(newSelection[n]); + } + } + + suppressSelectionChangeNotification = false; + + SelectedItemsPropertyChanged(oldSelection, newSelection); + } + + /// + /// Scroll to specific position with or without animation. + /// + /// Destination. + /// Scroll with or without animation + [EditorBrowsable(EditorBrowsableState.Never)] + public new void ScrollTo(float position, bool animate) => base.ScrollTo(position, animate); + + /// + /// Scroll to specific item's aligned position with or without animation. + /// + /// Target item of dataset. + /// Boolean flag of animation. + /// Align state of item. see details in ItemScrollTo. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void ScrollTo(object item, bool animate = false, ItemScrollTo align = ItemScrollTo.Nearest) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + if (ItemsLayouter == null) throw new Exception("Item Layouter must exist."); + + if (InternalItemSource.GetPosition(item) == -1) + { + throw new Exception("ScrollTo parameter item is not a member of ItemsSource"); + } + + float scrollPos, curPos, curSize, curItemSize; + (float x, float y) = ItemsLayouter.GetItemPosition(item); + (float width, float height) = ItemsLayouter.GetItemSize(item); + if (ScrollingDirection == Direction.Horizontal) + { + scrollPos = x; + curPos = ScrollPosition.X; + curSize = Size.Width; + curItemSize = width; + } + else + { + scrollPos = y; + curPos = ScrollPosition.Y; + curSize = Size.Height; + curItemSize = height; + } + + //Console.WriteLine("[NUI] ScrollTo [{0}:{1}], curPos{2}, itemPos{3}, curSize{4}, itemSize{5}", InternalItemSource.GetPosition(item), align, curPos, scrollPos, curSize, curItemSize); + switch (align) + { + case ItemScrollTo.Start: + //nothing necessary. + break; + case ItemScrollTo.Center: + scrollPos = scrollPos - (curSize / 2) + (curItemSize / 2); + break; + case ItemScrollTo.End: + scrollPos = scrollPos - curSize + curItemSize; + break; + case ItemScrollTo.Nearest: + if (scrollPos < curPos - curItemSize) + { + // item is placed before the current screen. scrollTo.Top + } + else if (scrollPos >= curPos + curSize + curItemSize) + { + // item is placed after the current screen. scrollTo.End + scrollPos = scrollPos - curSize + curItemSize; + } + else + { + // item is in the scroller. ScrollTo() is ignored. + return; + } + break; + } + + //Console.WriteLine("[NUI] ScrollTo [{0}]-------------------", scrollPos); + base.ScrollTo(scrollPos, animate); + } + + // Realize and Decorate the item. + internal override RecyclerViewItem RealizeItem(int index) + { + if (index == 0 && Header != null) + { + Header.Show(); + return Header; + } + + if (index == InternalItemSource.Count - 1 && Footer != null) + { + Footer.Show(); + return Footer; + } + + if (isGrouped) + { + var context = InternalItemSource.GetItem(index); + if (InternalItemSource.IsGroupHeader(index)) + { + DataTemplate templ = (groupHeaderTemplate as DataTemplateSelector)?.SelectDataTemplate(context, this) ?? groupHeaderTemplate; + + RecyclerViewItem groupHeader = PopRecycleGroupCache(templ, true); + if (groupHeader == null) + { + groupHeader = (RecyclerViewItem)DataTemplateExtensions.CreateContent(groupHeaderTemplate, context, this); + + groupHeader.ParentItemsView = this; + groupHeader.Template = templ; + groupHeader.isGroupHeader = true; + groupHeader.isGroupFooter = false; + ContentContainer.Add(groupHeader); + } + groupHeader.Index = index; + groupHeader.ParentGroup = context; + groupHeader.BindingContext = context; + //group selection? + return groupHeader; + } + else if (InternalItemSource.IsGroupFooter(index)) + { + DataTemplate templ = (groupFooterTemplate as DataTemplateSelector)?.SelectDataTemplate(context, this) ?? groupFooterTemplate; + + RecyclerViewItem groupFooter = PopRecycleGroupCache(templ, false); + if (groupFooter == null) + { + groupFooter = (RecyclerViewItem)DataTemplateExtensions.CreateContent(groupFooterTemplate, context, this); + + groupFooter.ParentItemsView = this; + groupFooter.Template = templ; + groupFooter.isGroupHeader = false; + groupFooter.isGroupFooter = true; + ContentContainer.Add(groupFooter); + } + groupFooter.Index = index; + groupFooter.ParentGroup = context; + groupFooter.BindingContext = context; + + //group selection? + return groupFooter; + } + } + + RecyclerViewItem item = base.RealizeItem(index); + if (isGrouped) item.ParentGroup = InternalItemSource.GetGroupParent(index); + + switch (SelectionMode) + { + case ItemSelectionMode.SingleSelection: + if (item.BindingContext == SelectedItem) item.IsSelected = true; + break; + + case ItemSelectionMode.MultipleSelections: + if (SelectedItems?.Contains(item.BindingContext) ?? false) item.IsSelected = true; + break; + case ItemSelectionMode.None: + item.IsSelectable = false; + break; + } + + return item; + } + + // Unrealize and caching the item. + internal override void UnrealizeItem(RecyclerViewItem item, bool recycle = true) + { + if (item == Header) + { + item.Hide(); + return; + } + if (item == Footer) + { + item.Hide(); + return; + } + if (item.isGroupHeader || item.isGroupFooter) + { + if (!recycle || !PushRecycleGroupCache(item)) + Utility.Dispose(item); + return; + } + + item.IsSelected = false; + base.UnrealizeItem(item, recycle); + } + + internal void SelectedItemsPropertyChanged(IList oldSelection, IList newSelection) + { + if (suppressSelectionChangeNotification) + { + return; + } + + foreach (RecyclerViewItem item in ContentContainer.Children.Where((item) => item is RecyclerViewItem)) + { + if (item.BindingContext == null) continue; + if (newSelection.Contains(item.BindingContext)) + { + if (!item.IsSelected) item.IsSelected = true; + } + else + { + if (item.IsSelected) item.IsSelected = false; + } + } + SelectionPropertyChanged(this, new SelectionChangedEventArgs(oldSelection, newSelection)); + + OnPropertyChanged(SelectedItemsProperty.PropertyName); + } + + /// + /// Internal selection callback. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void OnSelectionChanged(SelectionChangedEventArgs args) + { + //Selection Callback + } + + /// + /// Adjust scrolling position by own scrolling rules. + /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item) + /// + /// Scroll position which is calculated by ScrollableBase + /// Adjusted scroll destination + [EditorBrowsable(EditorBrowsableState.Never)] + protected override float AdjustTargetPositionOfScrollAnimation(float position) + { + // Destination is depending on implementation of layout manager. + // Get destination from layout manager. + return ItemsLayouter.CalculateCandidateScrollPosition(position); + } + + /// + /// OnScroll event callback. + /// + /// Scroll source object + /// Scroll event argument + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void OnScrolling(object source, ScrollEventArgs args) + { + if (disposed) return; + + if (needInitalizeLayouter) + { + ItemsLayouter.Initialize(this); + needInitalizeLayouter = false; + } + base.OnScrolling(source, args); + } + + /// + /// Dispose ItemsView and all children on it. + /// + /// Dispose type. + protected override void Dispose(DisposeTypes type) + { + if (disposed) + { + return; + } + + if (type == DisposeTypes.Explicit) + { + disposed = true; + if (InternalItemSource != null) + { + InternalItemSource.Dispose(); + InternalItemSource = null; + } + if (Header != null) + { + Utility.Dispose(Header); + Header = null; + } + if (Footer != null) + { + Utility.Dispose(Footer); + Footer = null; + } + groupHeaderTemplate = null; + groupFooterTemplate = null; + // + } + + base.Dispose(type); + } + + private static void SelectionPropertyChanged(CollectionView colView, SelectionChangedEventArgs args) + { + var command = colView.SelectionChangedCommand; + + if (command != null) + { + var commandParameter = colView.SelectionChangedCommandParameter; + + if (command.CanExecute(commandParameter)) + { + command.Execute(commandParameter); + } + } + colView.SelectionChanged?.Invoke(colView, args); + colView.OnSelectionChanged(args); + } + + private static object CoerceSelectedItems(BindableObject bindable, object value) + { + if (value == null) + { + return new SelectionList((CollectionView)bindable); + } + + if (value is SelectionList) + { + return value; + } + + return new SelectionList((CollectionView)bindable, value as IList); + } + + private static void SelectionModePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + var colView = (CollectionView)bindable; + + var oldMode = (ItemSelectionMode)oldValue; + var newMode = (ItemSelectionMode)newValue; + + IList previousSelection = new List(); + IList newSelection = new List(); + + switch (oldMode) + { + case ItemSelectionMode.None: + break; + case ItemSelectionMode.SingleSelection: + if (colView.SelectedItem != null) + { + previousSelection.Add(colView.SelectedItem); + } + break; + case ItemSelectionMode.MultipleSelections: + previousSelection = colView.SelectedItems; + break; + } + + switch (newMode) + { + case ItemSelectionMode.None: + break; + case ItemSelectionMode.SingleSelection: + if (colView.SelectedItem != null) + { + newSelection.Add(colView.SelectedItem); + } + break; + case ItemSelectionMode.MultipleSelections: + newSelection = colView.SelectedItems; + break; + } + + if (previousSelection.Count == newSelection.Count) + { + if (previousSelection.Count == 0 || (previousSelection[0] == newSelection[0])) + { + // Both selections are empty or have the same single item; no reason to signal a change + return; + } + } + + var args = new SelectionChangedEventArgs(previousSelection, newSelection); + SelectionPropertyChanged(colView, args); + } + + private void Init() + { + if (ItemsSource == null) return; + if (ItemsLayouter == null) return; + if (ItemTemplate == null) return; + + if (disposed) return; + if (needInitalizeLayouter) + { + if (InternalItemSource == null) return; + + InternalItemSource.HasHeader = (header != null); + InternalItemSource.HasFooter = (footer != null); + } + + if (!wasRelayouted) return; + + if (needInitalizeLayouter) + { + ItemsLayouter.Initialize(this); + needInitalizeLayouter = false; + } + ItemsLayouter.RequestLayout(0.0f, true); + + if (ScrollingDirection == Direction.Horizontal) + { + ContentContainer.SizeWidth = ItemsLayouter.CalculateLayoutOrientationSize(); + } + else + { + ContentContainer.SizeHeight = ItemsLayouter.CalculateLayoutOrientationSize(); + } + } + + private bool PushRecycleGroupCache(RecyclerViewItem item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + if (RecycleCache.Count >= 20) return false; + if (item.Template == null) return false; + if (item.isGroupHeader) + { + recycleGroupHeaderCache.Add(item); + } + else if (item.isGroupFooter) + { + recycleGroupFooterCache.Add(item); + } + else return false; + item.Hide(); + item.Index = -1; + return true; + } + + private RecyclerViewItem PopRecycleGroupCache(DataTemplate Template, bool isHeader) + { + RecyclerViewItem viewItem = null; + + var Cache = (isHeader ? recycleGroupHeaderCache : recycleGroupFooterCache); + for (int i = 0; i < Cache.Count; i++) + { + viewItem = Cache[i]; + if (Template == viewItem.Template) break; + } + + if (viewItem != null) + { + Cache.Remove(viewItem); + viewItem.Show(); + } + return viewItem; + } + + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ICollectionChangedNotifier.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ICollectionChangedNotifier.cs new file mode 100644 index 0000000..d00695d --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ICollectionChangedNotifier.cs @@ -0,0 +1,94 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; + +namespace Tizen.NUI.Components +{ + /// + /// Notify observers about dataset changes of observable items. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public interface ICollectionChangedNotifier + { + + /// + /// Notify the dataset is Changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyDataSetChanged(); + + /// + /// Notify the observable item in startIndex is changed. + /// + /// dataset source + /// changed item index + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemChanged(IItemSource source, int startIndex); + + /// + /// Notify the observable item is inserted in dataset. + /// + /// dataset source + /// Inserted item index + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemInserted(IItemSource source, int startIndex); + + /// + /// Notify the observable item is moved from fromPosition to ToPosition. + /// + /// + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition); + + /// + /// Notify the range of observable items from start to end are changed. + /// + /// + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemRangeChanged(IItemSource source, int startIndex, int endIndex); + + /// + /// Notify the count range of observable items are inserted in startIndex. + /// + /// + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemRangeInserted(IItemSource source, int startIndex, int count); + + /// + /// Notify the count range of observable items from the startIndex are removed. + /// + /// + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count); + + /// + /// Notify the observable item in startIndex is removed. + /// + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + void NotifyItemRemoved(IItemSource source, int startIndex); + } +} \ No newline at end of file diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultGridItem.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultGridItem.cs new file mode 100644 index 0000000..b618bc7 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultGridItem.cs @@ -0,0 +1,517 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; +using Tizen.NUI.Accessibility; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultGridItem is one kind of common component, a DefaultGridItem clearly describes what action will occur when the user selects it. + /// DefaultGridItem may contain text or an icon. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultGridItem : RecyclerViewItem + { + private TextLabel itemCaption; + private ImageView itemImage; + private View itemBadge; + private CaptionOrientation captionOrientation; + private bool layoutChanged; + + private DefaultGridItemStyle ItemStyle => ViewStyle as DefaultGridItemStyle; + + /// + /// Return a copied Style instance of DefaultLinearItem + /// + /// + /// It returns copied Style instance and changing it does not effect to the DefaultLinearItem. + /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new DefaultGridItemStyle Style + { + get + { + var result = new DefaultGridItemStyle(ItemStyle); + result.CopyPropertiesFromView(this); + if (itemCaption) result.Caption.CopyPropertiesFromView(itemCaption); + if (itemImage) result.Image.CopyPropertiesFromView(itemImage); + if (itemBadge) result.Badge.CopyPropertiesFromView(itemBadge); + + return result; + } + } + + static DefaultGridItem() {} + + /// + /// Creates a new instance of DefaultGridItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultGridItem() : base() + { + Initialize(); + } + + /// + /// Creates a new instance of DefaultGridItem with style + /// + /// Create DefaultGridItem by special style defined in UX. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultGridItem(string style) : base(style) + { + Initialize(); + } + + /// + /// Creates a new instance of DefaultGridItem with style + /// + /// Create DefaultGridItem by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultGridItem(DefaultGridItemStyle itemStyle) : base(itemStyle) + { + Initialize(); + } + + /// + /// Caption orientation. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public enum CaptionOrientation + { + /// + /// Outside of image bottom edge. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + OutsideBottom, + /// + /// Outside of image top edge. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + OutsideTop, + /// + /// inside of image bottom edge. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + InsideBottom, + /// + /// inside of image top edge. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + InsideTop, + } + + /// + /// DefaultGridItem's icon part. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ImageView Image + { + get + { + if ( itemImage == null) + { + itemImage = CreateImage(ItemStyle.Image); + if (itemImage != null) + { + Add(itemImage); + itemImage.Relayout += OnImageRelayout; + layoutChanged = true; + } + } + return itemImage; + } + internal set + { + itemImage = value; + layoutChanged = true; + } + } + + + /// + /// DefaultGridItem's badge object. will be placed in right-top edge. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Badge + { + get + { + return itemBadge; + + } + set + { + if (value == null) + { + Remove(itemBadge); + } + itemBadge = value; + if (itemBadge != null) + { + itemBadge.ApplyStyle(ItemStyle.Badge); + Add(itemBadge); + } + layoutChanged = true; + } + } + +/* open when ImageView using Uri not string + /// + /// Image image's resource url in DefaultGridItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string ImageUrl + { + get + { + return Image.ResourceUrl; + } + set + { + Image.ResourceUrl = value; + } + } +*/ + + /// + /// DefaultGridItem's text part. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabel Caption + { + get + { + if (itemCaption == null) + { + itemCaption = CreateLabel(ItemStyle.Caption); + if (itemCaption != null) + { + Add(itemCaption); + layoutChanged = true; + } + } + return itemCaption; + } + internal set + { + itemCaption = value; + layoutChanged = true; + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Label, itemCaption.Text); + } + } + + /// + /// The text of DefaultGridItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string Text + { + get + { + return Caption.Text; + } + set + { + Caption.Text = value; + } + } + + /// + /// Caption relative orientation with image in DefaultGridItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CaptionOrientation CaptionRelativeOrientation + { + get + { + return captionOrientation; + } + set + { + captionOrientation = value; + layoutChanged = true; + } + } + + /// + /// Apply style to DefaultLinearItemStyle. + /// + /// The style to apply. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void ApplyStyle(ViewStyle viewStyle) + { + + base.ApplyStyle(viewStyle); + if (viewStyle != null && viewStyle is DefaultGridItemStyle defaultStyle) + { + if (itemCaption != null) + itemCaption.ApplyStyle(defaultStyle.Caption); + if (itemImage != null) + itemImage.ApplyStyle(defaultStyle.Image); + if (itemBadge != null) + itemBadge.ApplyStyle(defaultStyle.Badge); + } + } + + /// + /// Creates Item's text part. + /// + /// The created Item's text part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual TextLabel CreateLabel(TextLabelStyle textStyle) + { + return new TextLabel(textStyle) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + } + + /// + /// Creates Item's icon part. + /// + /// The created Item's icon part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual ImageView CreateImage(ImageViewStyle imageStyle) + { + return new ImageView(imageStyle); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void MeasureChild() + { + //nothing to do. + if (itemCaption) + { + var pad = Padding; + var margin = itemCaption.Margin; + itemCaption.SizeWidth = SizeWidth - pad.Start - pad.End - margin.Start - margin.End; + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void LayoutChild() + { + if (!layoutChanged) return; + if (itemImage == null) return; + + layoutChanged = false; + + RelativeLayout.SetLeftTarget(itemImage, this); + RelativeLayout.SetLeftRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetRightTarget(itemImage, this); + RelativeLayout.SetRightRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetHorizontalAlignment(itemImage, RelativeLayout.Alignment.Center); + + if (itemCaption != null) + { + itemCaption.RaiseAbove(itemImage); + RelativeLayout.SetLeftTarget(itemCaption, itemImage); + RelativeLayout.SetLeftRelativeOffset(itemCaption, 0.0F); + RelativeLayout.SetRightTarget(itemCaption, itemImage); + RelativeLayout.SetRightRelativeOffset(itemCaption, 1.0F); + RelativeLayout.SetHorizontalAlignment(itemCaption, RelativeLayout.Alignment.Center); + RelativeLayout.SetFillHorizontal(itemCaption, true); + } + else + { + RelativeLayout.SetTopTarget(itemImage, this); + RelativeLayout.SetTopRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetBottomTarget(itemImage, this); + RelativeLayout.SetBottomRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetVerticalAlignment(itemImage, RelativeLayout.Alignment.Center); + } + + if (itemBadge) + { + RelativeLayout.SetLeftTarget(itemBadge, itemImage); + RelativeLayout.SetLeftRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetRightTarget(itemBadge, itemImage); + RelativeLayout.SetRightRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetHorizontalAlignment(itemBadge, RelativeLayout.Alignment.End); + } + + switch (captionOrientation) + { + case CaptionOrientation.OutsideBottom: + if (itemCaption != null) + { + RelativeLayout.SetTopTarget(itemCaption, this); + RelativeLayout.SetTopRelativeOffset(itemCaption, 1.0F); + RelativeLayout.SetBottomTarget(itemCaption, this); + RelativeLayout.SetBottomRelativeOffset(itemCaption, 1.0F); + RelativeLayout.SetVerticalAlignment(itemCaption, RelativeLayout.Alignment.End); + + RelativeLayout.SetTopTarget(itemImage, this); + RelativeLayout.SetTopRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetBottomTarget(itemImage, itemCaption); + RelativeLayout.SetBottomRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetVerticalAlignment(itemImage, RelativeLayout.Alignment.Center); + } + + if (itemBadge) + { + RelativeLayout.SetTopTarget(itemBadge, itemImage); + RelativeLayout.SetTopRelativeOffset(itemBadge, 0.0F); + RelativeLayout.SetBottomTarget(itemBadge, itemImage); + RelativeLayout.SetBottomRelativeOffset(itemBadge, 0.0F); + RelativeLayout.SetVerticalAlignment(itemBadge, RelativeLayout.Alignment.Start); + } + break; + + case CaptionOrientation.OutsideTop: + if (itemCaption != null) + { + RelativeLayout.SetTopTarget(itemCaption, this); + RelativeLayout.SetTopRelativeOffset(itemCaption, 0.0F); + RelativeLayout.SetBottomTarget(itemCaption, this); + RelativeLayout.SetBottomRelativeOffset(itemCaption, 0.0F); + RelativeLayout.SetVerticalAlignment(itemCaption, RelativeLayout.Alignment.Start); + + RelativeLayout.SetTopTarget(itemImage, itemCaption); + RelativeLayout.SetTopRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetBottomTarget(itemImage, this); + RelativeLayout.SetBottomRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetVerticalAlignment(itemImage, RelativeLayout.Alignment.Center); + } + + if (itemBadge) + { + RelativeLayout.SetTopTarget(itemBadge, itemImage); + RelativeLayout.SetTopRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetBottomTarget(itemBadge, itemImage); + RelativeLayout.SetBottomRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetVerticalAlignment(itemBadge, RelativeLayout.Alignment.End); + } + break; + + case CaptionOrientation.InsideBottom: + if (itemCaption != null) + { + RelativeLayout.SetTopTarget(itemCaption, this); + RelativeLayout.SetTopRelativeOffset(itemCaption, 1.0F); + RelativeLayout.SetBottomTarget(itemCaption, this); + RelativeLayout.SetBottomRelativeOffset(itemCaption, 1.0F); + RelativeLayout.SetVerticalAlignment(itemCaption, RelativeLayout.Alignment.End); + + RelativeLayout.SetTopTarget(itemImage, this); + RelativeLayout.SetTopRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetBottomTarget(itemImage, this); + RelativeLayout.SetBottomRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetVerticalAlignment(itemImage, RelativeLayout.Alignment.Center); + } + + if (itemBadge) + { + RelativeLayout.SetTopTarget(itemBadge, itemImage); + RelativeLayout.SetTopRelativeOffset(itemBadge, 0.0F); + RelativeLayout.SetBottomTarget(itemBadge, itemImage); + RelativeLayout.SetBottomRelativeOffset(itemBadge, 0.0F); + RelativeLayout.SetVerticalAlignment(itemBadge, RelativeLayout.Alignment.Start); + } + break; + + case CaptionOrientation.InsideTop: + if (itemCaption != null) + { + RelativeLayout.SetTopTarget(itemCaption, this); + RelativeLayout.SetTopRelativeOffset(itemCaption, 0.0F); + RelativeLayout.SetBottomTarget(itemCaption, this); + RelativeLayout.SetBottomRelativeOffset(itemCaption, 0.0F); + RelativeLayout.SetVerticalAlignment(itemCaption, RelativeLayout.Alignment.Start); + + RelativeLayout.SetTopTarget(itemImage, this); + RelativeLayout.SetTopRelativeOffset(itemImage, 0.0F); + RelativeLayout.SetBottomTarget(itemImage, this); + RelativeLayout.SetBottomRelativeOffset(itemImage, 1.0F); + RelativeLayout.SetVerticalAlignment(itemImage, RelativeLayout.Alignment.Center); + } + + if (itemBadge) + { + RelativeLayout.SetTopTarget(itemBadge, itemImage); + RelativeLayout.SetTopRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetBottomTarget(itemBadge, itemImage); + RelativeLayout.SetBottomRelativeOffset(itemBadge, 1.0F); + RelativeLayout.SetVerticalAlignment(itemBadge, RelativeLayout.Alignment.End); + } + break; + } + + + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + private void Initialize() + { + Layout = new RelativeLayout(); + layoutChanged = true; + LayoutDirectionChanged += OnLayoutDirectionChanged; + } + + /// + /// Dispose Item and all children on it. + /// + /// Dispose type. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void Dispose(DisposeTypes type) + { + if (disposed) + { + return; + } + + if (type == DisposeTypes.Explicit) + { + //Extension : Extension?.OnDispose(this); + + if (itemImage != null) + { + Utility.Dispose(itemImage); + } + if (itemCaption != null) + { + Utility.Dispose(itemCaption); + } + if (itemBadge != null) + { + Utility.Dispose(itemBadge); + } + } + + base.Dispose(type); + } + + private void OnLayoutDirectionChanged(object sender, LayoutDirectionChangedEventArgs e) + { + MeasureChild(); + LayoutChild(); + } + private void OnImageRelayout(object sender, EventArgs e) + { + MeasureChild(); + LayoutChild(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultLinearItem.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultLinearItem.cs new file mode 100644 index 0000000..0e9972f --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultLinearItem.cs @@ -0,0 +1,565 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; +using Tizen.NUI.Accessibility; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultLinearItem is one kind of common component, a DefaultLinearItem clearly describes what action will occur when the user selects it. + /// DefaultLinearItem may contain text or an icon. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultLinearItem : RecyclerViewItem + { + private View itemIcon; + private TextLabel itemLabel; + private TextLabel itemSubLabel; + private View itemExtra; + private View itemSeperator; + private bool layoutChanged; + private Size prevSize; + private DefaultLinearItemStyle ItemStyle => ViewStyle as DefaultLinearItemStyle; + + /// + /// Return a copied Style instance of DefaultLinearItem + /// + /// + /// It returns copied Style instance and changing it does not effect to the DefaultLinearItem. + /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new DefaultLinearItemStyle Style + { + get + { + var result = new DefaultLinearItemStyle(ItemStyle); + result.CopyPropertiesFromView(this); + if (itemLabel) result.Label.CopyPropertiesFromView(itemLabel); + if (itemSubLabel) result.SubLabel.CopyPropertiesFromView(itemSubLabel); + if (itemIcon) result.Icon.CopyPropertiesFromView(itemIcon); + if (itemExtra) result.Extra.CopyPropertiesFromView(itemExtra); + if (itemSeperator) result.Seperator.CopyPropertiesFromView(itemSeperator); + + return result; + } + } + + static DefaultLinearItem() {} + + /// + /// Creates a new instance of DefaultLinearItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultLinearItem() : base() + { + Initialize(); + } + + /// + /// Creates a new instance of a DefaultLinearItem with style. + /// + /// Create DefaultLinearItem by style defined in UX. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultLinearItem(string style) : base(style) + { + Initialize(); + } + + /// + /// Creates a new instance of a DefaultLinearItem with style. + /// + /// Create DefaultLinearItem by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultLinearItem(DefaultLinearItemStyle itemStyle) : base(itemStyle) + { + Initialize(); + } + + /// + /// Icon part of DefaultLinearItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Icon + { + get + { + if (itemIcon == null) + { + itemIcon = CreateIcon(ItemStyle?.Icon); + if (itemIcon != null) + { + layoutChanged = true; + Add(itemIcon); + itemIcon.Relayout += OnIconRelayout; + } + } + return itemIcon; + } + set + { + itemIcon = value; + } + } + + /* open when imageView using Uri not string. + /// + /// Icon image's resource url. Only activatable for icon as ImageView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string IconUrl + { + get + { + return (Icon as ImageView)?.ResourceUrl; + } + set + { + if (itemIcon != null && !(itemIcon is ImageView)) + { + // Tizen.Log.Error("IconUrl only can set Icon is ImageView"); + return; + } + (Icon as ImageView).ResourceUrl = value; + } + } + */ + + /// + /// DefaultLinearItem's text part of DefaultLinearItem + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabel Label + { + get + { + if (itemLabel == null) + { + itemLabel = CreateLabel(ItemStyle?.Label); + if (itemLabel != null) + { + layoutChanged = true; + Add(itemLabel); + } + } + return itemLabel; + } + internal set + { + itemLabel = value; + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Label, itemLabel.Text); + } + } + + /// + /// The text of DefaultLinearItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string Text + { + get + { + return Label.Text; + } + set + { + Label.Text = value; + } + } + + /// + /// DefaultLinearItem's secondary text part of DefaultLinearItem + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabel SubLabel + { + get + { + if (itemSubLabel == null) + { + itemSubLabel = CreateLabel(ItemStyle?.SubLabel); + if (itemLabel != null) + { + layoutChanged = true; + Add(itemSubLabel); + } + } + return itemSubLabel; + } + internal set + { + itemSubLabel = value; + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Label, itemSubLabel.Text); + } + } + + /// + /// The text of DefaultLinearItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string SubText + { + get + { + return SubLabel.Text; + } + set + { + SubLabel.Text = value; + } + } + + /// + /// Extra icon part of DefaultLinearItem. it will place next of label. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Extra + { + get + { + if (itemExtra == null) + { + itemExtra = CreateIcon(ItemStyle?.Extra); + if (itemExtra != null) + { + layoutChanged = true; + Add(itemExtra); + itemExtra.Relayout += OnExtraRelayout; + } + } + return itemExtra; + } + set + { + if (itemExtra != null) Remove(itemExtra); + itemExtra = value; + Add(itemExtra); + } + } + + /// + /// Seperator devider of DefaultLinearItem. it will place at the end of item. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Seperator + { + get + { + if (itemSeperator == null) + { + itemSeperator = new View(ItemStyle?.Seperator) + { + //need to consider horizontal/vertical! + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 2, + ExcludeLayouting = true + }; + layoutChanged = true; + Add(itemSeperator); + } + return itemSeperator; + } + } + + /// + /// Apply style to DefaultLinearItemStyle. + /// + /// The style to apply. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void ApplyStyle(ViewStyle viewStyle) + { + + base.ApplyStyle(viewStyle); + if (viewStyle != null && viewStyle is DefaultLinearItemStyle defaultStyle) + { + if (itemLabel != null) + itemLabel.ApplyStyle(defaultStyle.Label); + if (itemSubLabel != null) + itemSubLabel.ApplyStyle(defaultStyle.SubLabel); + if (itemIcon != null) + itemIcon.ApplyStyle(defaultStyle.Icon); + if (itemExtra != null) + itemExtra.ApplyStyle(defaultStyle.Extra); + if (itemSeperator != null) + itemSeperator.ApplyStyle(defaultStyle.Seperator); + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnRelayout(Vector2 size, RelayoutContainer container) + { + base.OnRelayout(size, container); + + if (prevSize != Size) + { + prevSize = Size; + if (itemSeperator) + { + var margin = itemSeperator.Margin; + itemSeperator.SizeWidth = SizeWidth - margin.Start - margin.End; + itemSeperator.SizeHeight = itemSeperator.HeightSpecification; + itemSeperator.Position = new Position(margin.Start, SizeHeight - margin.Bottom -itemSeperator.SizeHeight); + } + } + } + + /// + /// Creates Item's text part. + /// + /// The created Item's text part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual TextLabel CreateLabel(TextLabelStyle style) + { + return new TextLabel(style) + { + HorizontalAlignment = HorizontalAlignment.Begin, + VerticalAlignment = VerticalAlignment.Center + }; + } + + /// + /// Creates Item's icon part. + /// + /// The created Item's icon part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual ImageView CreateIcon(ViewStyle style) + { + return new ImageView(style); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void MeasureChild() + { + var pad = Padding; + if (itemLabel) + { + var margin = itemLabel.Margin; + itemLabel.SizeWidth = SizeWidth - pad.Start - pad.End - margin.Start - margin.End; + } + + if (itemSubLabel) + { + var margin = itemSubLabel.Margin; + itemSubLabel.SizeWidth = SizeWidth - pad.Start - pad.End - margin.Start - margin.End; + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void LayoutChild() + { + if (!layoutChanged) return; + if (itemLabel == null) return; + + layoutChanged = false; + + if (itemIcon != null) + { + RelativeLayout.SetLeftTarget(itemIcon, this); + RelativeLayout.SetLeftRelativeOffset(itemIcon, 0.0F); + RelativeLayout.SetRightTarget(itemIcon, this); + RelativeLayout.SetRightRelativeOffset(itemIcon, 0.0F); + RelativeLayout.SetTopTarget(itemIcon, this); + RelativeLayout.SetTopRelativeOffset(itemIcon, 0.0F); + RelativeLayout.SetBottomTarget(itemIcon, this); + RelativeLayout.SetBottomRelativeOffset(itemIcon, 1.0F); + RelativeLayout.SetVerticalAlignment(itemIcon, RelativeLayout.Alignment.Center); + RelativeLayout.SetHorizontalAlignment(itemIcon, RelativeLayout.Alignment.Start); + } + + if (itemExtra != null) + { + RelativeLayout.SetLeftTarget(itemExtra, this); + RelativeLayout.SetLeftRelativeOffset(itemExtra, 1.0F); + RelativeLayout.SetRightTarget(itemExtra, this); + RelativeLayout.SetRightRelativeOffset(itemIcon, 1.0F); + RelativeLayout.SetTopTarget(itemExtra, this); + RelativeLayout.SetTopRelativeOffset(itemExtra, 0.0F); + RelativeLayout.SetBottomTarget(itemExtra, this); + RelativeLayout.SetBottomRelativeOffset(itemExtra, 1.0F); + RelativeLayout.SetVerticalAlignment(itemExtra, RelativeLayout.Alignment.Center); + RelativeLayout.SetHorizontalAlignment(itemExtra, RelativeLayout.Alignment.End); + } + + if (itemSubLabel != null) + { + if (itemIcon) + { + RelativeLayout.SetLeftTarget(itemSubLabel, itemIcon); + RelativeLayout.SetLeftRelativeOffset(itemSubLabel, 1.0F); + } + else + { + RelativeLayout.SetLeftTarget(itemSubLabel, this); + RelativeLayout.SetLeftRelativeOffset(itemSubLabel, 0.0F); + } + if (itemExtra) + { + RelativeLayout.SetRightTarget(itemSubLabel, itemExtra); + RelativeLayout.SetRightRelativeOffset(itemSubLabel, 0.0F); + } + else + { + RelativeLayout.SetRightTarget(itemSubLabel, this); + RelativeLayout.SetRightRelativeOffset(itemSubLabel, 1.0F); + } + + RelativeLayout.SetTopTarget(itemSubLabel, this); + RelativeLayout.SetTopRelativeOffset(itemSubLabel, 1.0F); + RelativeLayout.SetBottomTarget(itemSubLabel, this); + RelativeLayout.SetBottomRelativeOffset(itemSubLabel, 1.0F); + RelativeLayout.SetVerticalAlignment(itemSubLabel, RelativeLayout.Alignment.End); + + RelativeLayout.SetHorizontalAlignment(itemSubLabel, RelativeLayout.Alignment.Center); + RelativeLayout.SetFillHorizontal(itemSubLabel, true); + } + + if (itemIcon) + { + RelativeLayout.SetLeftTarget(itemLabel, itemIcon); + RelativeLayout.SetLeftRelativeOffset(itemLabel, 1.0F); + } + else + { + RelativeLayout.SetLeftTarget(itemLabel, this); + RelativeLayout.SetLeftRelativeOffset(itemLabel, 0.0F); + } + if (itemExtra) + { + RelativeLayout.SetRightTarget(itemLabel, itemExtra); + RelativeLayout.SetRightRelativeOffset(itemLabel, 0.0F); + } + else + { + RelativeLayout.SetRightTarget(itemLabel, this); + RelativeLayout.SetRightRelativeOffset(itemLabel, 1.0F); + } + + RelativeLayout.SetTopTarget(itemLabel, this); + RelativeLayout.SetTopRelativeOffset(itemLabel, 0.0F); + + if (itemSubLabel) + { + RelativeLayout.SetBottomTarget(itemLabel, itemSubLabel); + RelativeLayout.SetBottomRelativeOffset(itemLabel, 0.0F); + } + else + { + RelativeLayout.SetBottomTarget(itemLabel, this); + RelativeLayout.SetBottomRelativeOffset(itemLabel, 1.0F); + } + RelativeLayout.SetVerticalAlignment(itemLabel, RelativeLayout.Alignment.Center); + RelativeLayout.SetHorizontalAlignment(itemLabel, RelativeLayout.Alignment.Center); + RelativeLayout.SetFillHorizontal(itemLabel, true); + + if (prevSize != Size) + { + prevSize = Size; + if (itemSeperator) + { + var margin = itemSeperator.Margin; + itemSeperator.SizeWidth = SizeWidth - margin.Start - margin.End; + itemSeperator.SizeHeight = itemSeperator.HeightSpecification; + itemSeperator.Position = new Position(margin.Start, SizeHeight - margin.Bottom -itemSeperator.SizeHeight); + } + } + } + + /// + /// Dispose Item and all children on it. + /// + /// Dispose type. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void Dispose(DisposeTypes type) + { + if (disposed) + { + return; + } + + if (type == DisposeTypes.Explicit) + { + //Extension : Extension?.OnDispose(this); + + if (itemIcon != null) + { + Utility.Dispose(itemIcon); + } + if (itemExtra != null) + { + Utility.Dispose(itemExtra); + } + if (itemLabel != null) + { + Utility.Dispose(itemLabel); + } + if (itemSubLabel != null) + { + Utility.Dispose(itemSubLabel); + } + if (itemSeperator != null) + { + Utility.Dispose(itemSeperator); + } + } + + base.Dispose(type); + } + + /// + /// Get DefaultLinearItem style. + /// + /// The default DefaultLinearItem style. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override ViewStyle CreateViewStyle() + { + return new DefaultLinearItemStyle(); + } + + private void Initialize() + { + base.OnInitialize(); + Layout = new RelativeLayout(); + var seperator = Seperator; + layoutChanged = true; + LayoutDirectionChanged += OnLayoutDirectionChanged; + } + + private void OnLayoutDirectionChanged(object sender, LayoutDirectionChangedEventArgs e) + { + MeasureChild(); + LayoutChild(); + } + + private void OnIconRelayout(object sender, EventArgs e) + { + MeasureChild(); + LayoutChild(); + } + + private void OnExtraRelayout(object sender, EventArgs e) + { + MeasureChild(); + LayoutChild(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultTitleItem.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultTitleItem.cs new file mode 100644 index 0000000..432f61b --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/DefaultTitleItem.cs @@ -0,0 +1,409 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; +using Tizen.NUI.Accessibility; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultTitleItem is one kind of common component, a DefaultTitleItem clearly describes what action will occur when the user selects it. + /// DefaultTitleItem may contain text or an icon. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultTitleItem : RecyclerViewItem + { + private TextLabel itemLabel; + private View itemIcon; + private View itemSeperator; + private bool layoutChanged; + private Size prevSize; + private DefaultTitleItemStyle ItemStyle => ViewStyle as DefaultTitleItemStyle; + + /// + /// Return a copied Style instance of DefaultTitleItem + /// + /// + /// It returns copied Style instance and changing it does not effect to the DefaultTitleItem. + /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new DefaultTitleItemStyle Style + { + get + { + var result = new DefaultTitleItemStyle(ItemStyle); + result.CopyPropertiesFromView(this); + if (itemLabel) result.Label.CopyPropertiesFromView(itemLabel); + if (itemIcon) result.Icon.CopyPropertiesFromView(itemIcon); + if (itemSeperator) result.Seperator.CopyPropertiesFromView(itemSeperator); + + return result; + } + } + + static DefaultTitleItem() {} + + /// + /// Creates a new instance of DefaultTitleItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultTitleItem() : base() + { + Initialize(); + } + + /// + /// Creates a new instance of a DefaultTitleItem with style. + /// + /// Create DefaultTitleItem by style defined in UX. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultTitleItem(string style) : base(style) + { + Initialize(); + } + + /// + /// Creates a new instance of a DefaultTitleItem with style. + /// + /// Create DefaultTitleItem by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultTitleItem(DefaultTitleItemStyle itemStyle) : base(itemStyle) + { + Initialize(); + } + + /// + /// Icon part of DefaultTitleItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Icon + { + get + { + if (itemIcon == null) + { + itemIcon = CreateIcon(ItemStyle?.Icon); + if (itemIcon != null) + { + layoutChanged = true; + Add(itemIcon); + itemIcon.Relayout += OnIconRelayout; + } + } + return itemIcon; + } + set + { + itemIcon = value; + } + } + + /* open when imageView using Uri not string. + /// + /// Icon image's resource url. Only activatable for icon as ImageView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string IconUrl + { + get + { + return (Icon as ImageView)?.ResourceUrl; + } + set + { + if (itemIcon != null && !(itemIcon is ImageView)) + { + // Tizen.Log.Error("IconUrl only can set Icon is ImageView"); + return; + } + (Icon as ImageView).ResourceUrl = value; + } + } + */ + + /// + /// DefaultTitleItem's text part of DefaultTitleItem + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabel Label + { + get + { + if (itemLabel == null) + { + itemLabel = CreateLabel(ItemStyle?.Label); + if (itemLabel != null) + { + layoutChanged = true; + Add(itemLabel); + } + } + return itemLabel; + } + internal set + { + itemLabel = value; + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Label, itemLabel.Text); + } + } + + /// + /// The text of DefaultTitleItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string Text + { + get + { + return Label.Text; + } + set + { + Label.Text = value; + } + } + + /// + /// Seperator devider of DefaultTitleItem. it will place at the end of item. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public View Seperator + { + get + { + if (itemSeperator == null) + { + itemSeperator = new View(ItemStyle?.Seperator) + { + //need to consider horizontal/vertical! + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 2, + ExcludeLayouting = true + }; + layoutChanged = true; + Add(itemSeperator); + } + return itemSeperator; + } + } + + /// + /// Apply style to DefaultTitleItemStyle. + /// + /// The style to apply. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void ApplyStyle(ViewStyle viewStyle) + { + + base.ApplyStyle(viewStyle); + if (viewStyle != null && viewStyle is DefaultTitleItemStyle defaultStyle) + { + if (itemLabel != null) + itemLabel.ApplyStyle(defaultStyle.Label); + if (itemIcon != null) + itemIcon.ApplyStyle(defaultStyle.Icon); + if (itemSeperator != null) + itemSeperator.ApplyStyle(defaultStyle.Seperator); + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnRelayout(Vector2 size, RelayoutContainer container) + { + base.OnRelayout(size, container); + + if (prevSize != Size) + { + prevSize = Size; + if (itemSeperator) + { + var margin = itemSeperator.Margin; + itemSeperator.SizeWidth = SizeWidth - margin.Start - margin.End; + itemSeperator.SizeHeight = itemSeperator.HeightSpecification; + itemSeperator.Position = new Position(margin.Start, SizeHeight - margin.Bottom -itemSeperator.SizeHeight); + } + } + } + + /// + /// Creates Item's text part. + /// + /// The created Item's text part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual TextLabel CreateLabel(TextLabelStyle style) + { + return new TextLabel(style) + { + HorizontalAlignment = HorizontalAlignment.Begin, + VerticalAlignment = VerticalAlignment.Center + }; + } + + /// + /// Creates Item's icon part. + /// + /// The created Item's icon part. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual ImageView CreateIcon(ViewStyle style) + { + return new ImageView(style); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void MeasureChild() + { + if (itemLabel) + { + var pad = Padding; + var margin = itemLabel.Margin; + itemLabel.SizeWidth = SizeWidth - pad.Start - pad.End - margin.Start - margin.End; + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void LayoutChild() + { + if (!layoutChanged) return; + if (itemLabel == null) return; + + layoutChanged = false; + + if (itemIcon != null) + { + RelativeLayout.SetLeftTarget(itemIcon, this); + RelativeLayout.SetLeftRelativeOffset(itemIcon, 1.0F); + RelativeLayout.SetRightTarget(itemIcon, this); + RelativeLayout.SetRightRelativeOffset(itemIcon, 1.0F); + RelativeLayout.SetTopTarget(itemIcon, this); + RelativeLayout.SetTopRelativeOffset(itemIcon, 0.0F); + RelativeLayout.SetBottomTarget(itemIcon, this); + RelativeLayout.SetBottomRelativeOffset(itemIcon, 1.0F); + RelativeLayout.SetVerticalAlignment(itemIcon, RelativeLayout.Alignment.Center); + RelativeLayout.SetHorizontalAlignment(itemIcon, RelativeLayout.Alignment.End); + } + + RelativeLayout.SetLeftTarget(itemLabel, this); + RelativeLayout.SetLeftRelativeOffset(itemLabel, 0.0F); + if (itemIcon) + { + RelativeLayout.SetRightTarget(itemLabel, itemIcon); + RelativeLayout.SetRightRelativeOffset(itemLabel, 0.0F); + } + else + { + RelativeLayout.SetRightTarget(itemLabel, this); + RelativeLayout.SetRightRelativeOffset(itemLabel, 1.0F); + } + + RelativeLayout.SetTopTarget(itemLabel, this); + RelativeLayout.SetTopRelativeOffset(itemLabel, 0.0F); + RelativeLayout.SetBottomTarget(itemLabel, this); + RelativeLayout.SetBottomRelativeOffset(itemLabel, 1.0F); + RelativeLayout.SetVerticalAlignment(itemLabel, RelativeLayout.Alignment.Center); + RelativeLayout.SetHorizontalAlignment(itemLabel, RelativeLayout.Alignment.Center); + RelativeLayout.SetFillHorizontal(itemLabel, true); + + if (prevSize != Size) + { + prevSize = Size; + if (itemSeperator) + { + var margin = itemSeperator.Margin; + itemSeperator.SizeWidth = SizeWidth - margin.Start - margin.End; + itemSeperator.SizeHeight = itemSeperator.HeightSpecification; + itemSeperator.Position = new Position(margin.Start, SizeHeight - margin.Bottom -itemSeperator.SizeHeight); + } + } + } + + /// + /// Dispose Item and all children on it. + /// + /// Dispose type. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void Dispose(DisposeTypes type) + { + if (disposed) + { + return; + } + + if (type == DisposeTypes.Explicit) + { + //Extension : Extension?.OnDispose(this); + + if (itemIcon != null) + { + Utility.Dispose(itemIcon); + } + if (itemLabel != null) + { + Utility.Dispose(itemLabel); + } + if (itemSeperator != null) + { + Utility.Dispose(itemSeperator); + } + } + + base.Dispose(type); + } + + /// + /// Get DefaultTitleItem style. + /// + /// The default DefaultTitleItem style. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override ViewStyle CreateViewStyle() + { + return new DefaultTitleItemStyle(); + } + + private void Initialize() + { + base.OnInitialize(); + Layout = new RelativeLayout(); + var seperator = Seperator; + layoutChanged = true; + LayoutDirectionChanged += OnLayoutDirectionChanged; + } + + private void OnLayoutDirectionChanged(object sender, LayoutDirectionChangedEventArgs e) + { + MeasureChild(); + LayoutChild(); + } + + private void OnIconRelayout(object sender, EventArgs e) + { + MeasureChild(); + LayoutChild(); + } + + private void OnExtraRelayout(object sender, EventArgs e) + { + MeasureChild(); + LayoutChild(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs new file mode 100644 index 0000000..980dca2 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.Internal.cs @@ -0,0 +1,280 @@ +using System; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components.Extension; +using Tizen.NUI.Accessibility; // To use AccessibilityManager + +namespace Tizen.NUI.Components +{ + public partial class RecyclerViewItem + { + internal RecyclerView ParentItemsView = null; + internal object ParentGroup = null; + internal bool isGroupHeader; + internal bool isGroupFooter; + private bool styleApplied = false; + + /// + /// Update ViewItem State. + /// + internal void UpdateState() + { + if (!styleApplied) return; + + ControlState sourceState = ControlState; + ControlState targetState; + + // Normal, Disabled + targetState = IsEnabled ? ControlState.Normal : ControlState.Disabled; + + // Selected, DisabledSelected + if (IsSelected) targetState += ControlState.Selected; + + // Pressed, PressedSelected + if (IsPressed) targetState += ControlState.Pressed; + + // Focused, FocusedPressed, FocusedPressedSelected, DisabledFocused, DisabledSelectedFocused + if (IsFocused) targetState += ControlState.Focused; + + if (sourceState != targetState) + { + ControlState = targetState; + OnUpdate(); + } + } + + internal override bool OnAccessibilityActivated() + { + if (!IsEnabled) + { + return false; + } + + // Touch Down + IsPressed = true; + UpdateState(); + + // Touch Up + bool clicked = IsPressed && IsEnabled; + IsPressed = false; + + if (IsSelectable) + { + //IsSelected = !IsSelected; + } + else + { + UpdateState(); + } + + if (clicked) + { + ClickedEventArgs eventArgs = new ClickedEventArgs(); + OnClickedInternal(eventArgs); + } + return true; + } + + /// + /// Called when the ViewItem is Clicked by a user + /// + /// The click information. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void OnClicked(ClickedEventArgs eventArgs) + { + //Console.WriteLine("On Clicked Called {0}", this.Index); + } + + /// + /// Called when the ViewItem need to be updated + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void OnUpdate() + { + base.OnUpdate(); + UpdateContent(); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override bool HandleControlStateOnTouch(Touch touch) + { + if (!IsEnabled || null == touch) + { + return false; + } + + PointStateType state = touch.GetState(0); + + switch (state) + { + case PointStateType.Down: + IsPressed = true; + UpdateState(); + return true; + case PointStateType.Interrupted: + IsPressed = false; + UpdateState(); + return true; + case PointStateType.Up: + { + bool clicked = IsPressed && IsEnabled; + IsPressed = false; + + if (!clicked) return true; + + if (IsSelectable) + { + if (ParentItemsView as CollectionView) + { + CollectionView colView = ParentItemsView as CollectionView; + switch (colView.SelectionMode) + { + case ItemSelectionMode.SingleSelection : + colView.SelectedItem = IsSelected ? null : BindingContext; + break; + case ItemSelectionMode.MultipleSelections : + var selectedItems = colView.SelectedItems; + if (selectedItems.Contains(BindingContext)) selectedItems.Remove(BindingContext); + else selectedItems.Add(BindingContext); + break; + case ItemSelectionMode.None : + break; + } + } + } + else + { + // Extension : Extension?.SetTouchInfo(touch); + UpdateState(); + } + + if (clicked) + { + ClickedEventArgs eventArgs = new ClickedEventArgs(); + OnClickedInternal(eventArgs); + } + + return true; + } + default: + break; + } + return base.HandleControlStateOnTouch(touch); + } + + + /// + /// Measure child, it can be override. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void MeasureChild() + { + } + + /// + /// Layout child, it can be override. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void LayoutChild() + { + } + + /// + /// Dispose Item and all children on it. + /// + /// Dispose type. + protected override void Dispose(DisposeTypes type) + { + if (disposed) + { + return; + } + + if (type == DisposeTypes.Explicit) + { + // + } + + base.Dispose(type); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void OnControlStateChanged(ControlStateChangedEventArgs controlStateChangedInfo) + { + if (controlStateChangedInfo == null) throw new ArgumentNullException(nameof(controlStateChangedInfo)); + base.OnControlStateChanged(controlStateChangedInfo); + + var stateEnabled = !controlStateChangedInfo.CurrentState.Contains(ControlState.Disabled); + + if (IsEnabled != stateEnabled) + { + IsEnabled = stateEnabled; + } + + var statePressed = controlStateChangedInfo.CurrentState.Contains(ControlState.Pressed); + + if (IsPressed != statePressed) + { + IsPressed = statePressed; + } + } + + /// + /// Get ViewItem style. + /// + /// The default ViewItem style. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override ViewStyle CreateViewStyle() + { + return new RecyclerViewItemStyle(); + } + + /// + /// It is hijack by using protected, style copy problem when class inherited from Button. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + private void Initialize() + { + //FIXME! + IsCreateByXaml = true; + Layout = new AbsoluteLayout(); + UpdateState(); + + AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "ViewItem"); + } + + /// + /// Update the Content. it can be override. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void UpdateContent() + { + MeasureChild(); + LayoutChild(); + + Sensitive = IsEnabled; + } + + + /// FIXME!! This has to be done in Element or View class. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void OnBindingContextChanged() + { + foreach (View child in Children) + { + SetChildInheritedBindingContext(child, BindingContext); + } + } + + private void OnClickedInternal(ClickedEventArgs eventArgs) + { + Command?.Execute(CommandParameter); + OnClicked(eventArgs); + + Clicked?.Invoke(this, eventArgs); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.cs new file mode 100644 index 0000000..5f3e882 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Item/RecyclerViewItem.cs @@ -0,0 +1,328 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; +using Tizen.NUI.Accessibility; + +namespace Tizen.NUI.Components +{ + /// + /// This class provides a basic item for CollectionView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public partial class RecyclerViewItem : Control + { + /// + /// Property of boolean Enable flag. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(RecyclerViewItem), true, propertyChanged: (bindable, oldValue, newValue) => + { + var instance = (RecyclerViewItem)bindable; + if (newValue != null) + { + bool newEnabled = (bool)newValue; + if (instance.isEnabled != newEnabled) + { + instance.isEnabled = newEnabled; + if (instance.ItemStyle != null) + { + instance.ItemStyle.IsEnabled = newEnabled; + } + instance.UpdateState(); + } + } + }, + defaultValueCreator: (bindable) => ((RecyclerViewItem)bindable).isEnabled); + + /// + /// Property of boolean Selected flag. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(RecyclerViewItem), true, propertyChanged: (bindable, oldValue, newValue) => + { + var instance = (RecyclerViewItem)bindable; + if (newValue != null) + { + bool newSelected = (bool)newValue; + if (instance.isSelected != newSelected) + { + instance.isSelected = newSelected; + + if (instance.ItemStyle != null) + { + instance.ItemStyle.IsSelected = newSelected; + } + + if (instance.isSelectable) + { + instance.UpdateState(); + } + } + } + }, + defaultValueCreator: (bindable) => + { + var instance = (RecyclerViewItem)bindable; + return instance.isSelectable && instance.isSelected; + }); + + /// + /// Property of boolean Selectable flag. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsSelectableProperty = BindableProperty.Create(nameof(IsSelectable), typeof(bool), typeof(RecyclerViewItem), true, propertyChanged: (bindable, oldValue, newValue) => + { + var instance = (RecyclerViewItem)bindable; + if (newValue != null) + { + bool newSelectable = (bool)newValue; + if (instance.isSelectable != newSelectable) + { + instance.isSelectable = newSelectable; + + if (instance.ItemStyle != null) + { + instance.ItemStyle.IsSelectable = newSelectable; + } + + instance.UpdateState(); + } + } + }, + defaultValueCreator: (bindable) => ((RecyclerViewItem)bindable).isSelectable); + + private bool isSelected = false; + private bool isSelectable = true; + private bool isEnabled = true; + private RecyclerViewItemStyle ItemStyle => ViewStyle as RecyclerViewItemStyle; + + /// + /// Return a copied Style instance of Toast + /// + /// + /// It returns copied Style instance and changing it does not effect to the Toast. + /// Style setting is possible by using constructor or the function of ApplyStyle(ViewStyle viewStyle) + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new RecyclerViewItemStyle Style + { + get + { + var result = new RecyclerViewItemStyle(ItemStyle); + result.CopyPropertiesFromView(this); + return result; + } + } + + static RecyclerViewItem() {} + + /// + /// Creates a new instance of RecyclerViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItem() : base() + { + Initialize(); + } + + /// + /// Creates a new instance of RecyclerViewItem with style. + /// + /// Create RecyclerViewItem by special style defined in UX. + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItem(string style) : base(style) + { + Initialize(); + } + + /// + /// Creates a new instance of a RecyclerViewItem with style. + /// + /// Create RecyclerViewItem by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItem(RecyclerViewItemStyle itemStyle) : base(itemStyle) + { + Initialize(); + } + + /// + /// An event for the RecyclerViewItem clicked signal which can be used to subscribe or unsubscribe the event handler provided by the user. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public event EventHandler Clicked; + + /// + /// Flag to decide RecyclerViewItem can be selected or not. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsSelectable + { + get => (bool)GetValue(IsSelectableProperty); + set => SetValue(IsSelectableProperty, value); + } + + /// + /// Flag to decide selected state in RecyclerViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsSelected + { + get => (bool)GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + /// + /// Flag to decide enable or disable in RecyclerViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEnabled + { + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// + /// Data index which is binded to item. + /// Can access to data using this index. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public int Index { get; internal set; } = 0; + + /// + /// DataTemplate of this view object + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DataTemplate Template { get; internal set; } + + /// + /// State of Realization + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsRealized { get; internal set; } + internal bool IsHeader { get; set; } + internal bool IsFooter { get; set; } + internal bool IsPressed { get; set; } = false; + + /// + /// Called after a key event is received by the view that has had its focus set. + /// + /// The key event. + /// True if the key event should be consumed. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool OnKey(Key key) + { + if (!IsEnabled || null == key) + { + return false; + } + + if (key.State == Key.StateType.Down) + { + if (key.KeyPressedName == "Return") + { + IsPressed = true; + UpdateState(); + } + } + else if (key.State == Key.StateType.Up) + { + if (key.KeyPressedName == "Return") + { + bool clicked = IsPressed && IsEnabled; + + IsPressed = false; + + if (IsSelectable) + { + // Extension : Extension?.SetTouchInfo(touch); + if (ParentItemsView as CollectionView) + { + CollectionView colView = ParentItemsView as CollectionView; + switch (colView.SelectionMode) + { + case ItemSelectionMode.SingleSelection : + colView.SelectedItem = IsSelected ? null : BindingContext; + break; + case ItemSelectionMode.MultipleSelections : + var selectedItems = colView.SelectedItems; + if (selectedItems.Contains(BindingContext)) selectedItems.Remove(BindingContext); + else selectedItems.Add(BindingContext); + break; + case ItemSelectionMode.None : + break; + } + } + } + else + { + UpdateState(); + } + + if (clicked) + { + ClickedEventArgs eventArgs = new ClickedEventArgs(); + OnClickedInternal(eventArgs); + } + } + } + return base.OnKey(key); + } + + /// + /// Called when the control gain key input focus. Should be overridden by derived classes if they need to customize what happens when the focus is gained. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnFocusGained() + { + base.OnFocusGained(); + UpdateState(); + } + + /// + /// Called when the control loses key input focus. + /// Should be overridden by derived classes if they need to customize + /// what happens when the focus is lost. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnFocusLost() + { + base.OnFocusLost(); + UpdateState(); + } + + /// + /// Apply style to RecyclerViewItem. + /// + /// The style to apply. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void ApplyStyle(ViewStyle viewStyle) + { + styleApplied = false; + + base.ApplyStyle(viewStyle); + if (viewStyle != null) + { + //Extension = RecyclerViewItemStyle.CreateExtension(); + } + + styleApplied = true; + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSelectionMode.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSelectionMode.cs new file mode 100644 index 0000000..3156bf2 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSelectionMode.cs @@ -0,0 +1,43 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; + +namespace Tizen.NUI.Components +{ + /// + /// Selection mode of CollecitonView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public enum ItemSelectionMode + { + /// + /// None of item can be selected. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + None, + /// + /// Single selection. select item exculsively so previous selected item will be unselected. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + SingleSelection, + /// + /// Multiple selections. select multiple items and previous selected item still remains selected. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + MultipleSelections + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSizingStrategy.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSizingStrategy.cs new file mode 100644 index 0000000..006da23 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSizingStrategy.cs @@ -0,0 +1,42 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; + +namespace Tizen.NUI.Components +{ + /// + /// Size calculation strategy for CollectionView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public enum ItemSizingStrategy + { + + /// + /// Measure all items in advanced. + /// Estimate first item size for all, and when scroll reached position, + /// measure strictly. Note : This will make scroll bar trembling. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + MeasureAll, + /// + /// Measure first item and deligate size for all items. + /// if template is selector, the size of first item from each template will be deligated. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + MeasureFirst, + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/EmptySource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/EmptySource.cs new file mode 100644 index 0000000..d348b00 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/EmptySource.cs @@ -0,0 +1,63 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; + +namespace Tizen.NUI.Components +{ + internal sealed class EmptySource : IItemSource + { + public int Count => 0; + + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + + public void Dispose() + { + + } + + public bool IsHeader(int index) + { + return HasHeader && index == 0; + } + + public bool IsFooter(int index) + { + if (!HasFooter) + { + return false; + } + + if (HasHeader) + { + return index == 1; + } + + return index == 0; + } + + public int GetPosition(object item) + { + return -1; + } + + public object GetItem(int position) + { + throw new IndexOutOfRangeException("IItemSource is empty"); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/IItemSource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/IItemSource.cs new file mode 100644 index 0000000..4a4d86b --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/IItemSource.cs @@ -0,0 +1,105 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.ComponentModel; + +namespace Tizen.NUI.Components +{ + /// + /// Base interface for encapsulated data source in RecyclerView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IItemSource : IDisposable + { + /// + /// Count of data source. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + int Count { get; } + + /// + /// Position integer value of data object. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + int GetPosition(object item); + + /// + /// Item object in position. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + object GetItem(int position); + + /// + /// Flag of header existence. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + bool HasHeader { get; set; } + + /// + /// Flag of Footer existence. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + bool HasFooter { get; set; } + + /// + /// Boolean checker for position is header or not. + /// 0 index will be header if header exist. + /// warning: if header exist, all item index will be increased. + /// + /// The position for checking header. + [EditorBrowsable(EditorBrowsableState.Never)] + bool IsHeader(int position); + + /// + /// Boolean checker for position is footer or not. + /// last index will be footer if footer exist. + /// warning: footer will be place original data count or data count + 1. + /// + /// The position for checking footer. + [EditorBrowsable(EditorBrowsableState.Never)] + bool IsFooter(int position); + } + + /// + /// Base interface for encapsulated data source with group structure in CollectionView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IGroupableItemSource : IItemSource + { + /// + /// Boolean checker for position is group header or not + /// + /// The position for checking group header. + [EditorBrowsable(EditorBrowsableState.Never)] + bool IsGroupHeader(int position); + + /// + /// Boolean checker for position is group footer or not + /// + /// The position for checking group footer. + [EditorBrowsable(EditorBrowsableState.Never)] + bool IsGroupFooter(int position); + + /// + /// Boolean checker for position is group footer or not + /// + /// The position for checking group footer. + [EditorBrowsable(EditorBrowsableState.Never)] + object GetGroupParent(int position); + + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ItemsSourceFactory.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ItemsSourceFactory.cs new file mode 100644 index 0000000..f3df899 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ItemsSourceFactory.cs @@ -0,0 +1,62 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +//using AndroidX.RecyclerView.Widget; ??? need to find whot it needs? adapter? + +namespace Tizen.NUI.Components +{ + internal static class ItemsSourceFactory + { + public static IItemSource Create(IEnumerable itemsSource, ICollectionChangedNotifier notifier) + { + if (itemsSource == null) + { + return new EmptySource(); + } + + switch (itemsSource) + { + case IList list when itemsSource is INotifyCollectionChanged: + return new ObservableItemSource(new MarshalingObservableCollection(list), notifier); + case IEnumerable _ when itemsSource is INotifyCollectionChanged: + return new ObservableItemSource(itemsSource, notifier); + case IEnumerable generic: + return new ListSource(generic); + } + + return new ListSource(itemsSource); + } + + public static IItemSource Create(RecyclerView recyclerView) + { + return Create(recyclerView.ItemsSource, recyclerView); + } + + public static IGroupableItemSource Create(CollectionView colView) + { + var source = colView.ItemsSource; + + if (colView.IsGrouped && source != null) + return new ObservableGroupedSource(colView, colView); + + else + return new UngroupedItemSource(Create(colView.ItemsSource, colView)); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ListSource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ListSource.cs new file mode 100644 index 0000000..41835ad --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ListSource.cs @@ -0,0 +1,154 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Tizen.NUI.Components +{ + sealed class ListSource : IItemSource, IList + { + IList _itemsSource; + + public ListSource() + { + } + + public ListSource(IEnumerable enumerable) + { + _itemsSource = new List(enumerable); + } + + public ListSource(IEnumerable enumerable) + { + _itemsSource = new List(); + + if (enumerable == null) + return; + + foreach (object item in enumerable) + { + _itemsSource.Add(item); + } + } + + public int Count => _itemsSource.Count + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0); + + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + + public bool IsReadOnly => _itemsSource.IsReadOnly; + + public bool IsFixedSize => _itemsSource.IsFixedSize; + + public object SyncRoot => _itemsSource.SyncRoot; + + public bool IsSynchronized => _itemsSource.IsSynchronized; + + object IList.this[int index] { get => _itemsSource[index]; set => _itemsSource[index] = value; } + + public void Dispose() + { + + } + + public bool IsFooter(int index) + { + return HasFooter && index == Count - 1; + } + + public bool IsHeader(int index) + { + return HasHeader && index == 0; + } + + public int GetPosition(object item) + { + for (int n = 0; n < _itemsSource.Count; n++) + { + var elementByIndex = _itemsSource[n]; + var isEqual = elementByIndex == item || (elementByIndex != null && item != null && elementByIndex.Equals(item)); + + if (isEqual) + { + return AdjustPosition(n); + } + } + + return -1; + } + + public object GetItem(int position) + { + return _itemsSource[AdjustIndexRequest(position)]; + } + + int AdjustIndexRequest(int index) + { + return index - (HasHeader ? 1 : 0); + } + + int AdjustPosition(int index) + { + return index + (HasHeader ? 1 : 0); + } + public int Add(object value) + { + return _itemsSource.Add(value); + } + + public bool Contains(object value) + { + return _itemsSource.Contains(value); + } + + public void Clear() + { + _itemsSource.Clear(); + } + + public int IndexOf(object value) + { + return _itemsSource.IndexOf(value); + } + + public void Insert(int index, object value) + { + _itemsSource.Insert(index, value); + } + + public void Remove(object value) + { + _itemsSource.Remove(value); + } + + public void RemoveAt(int index) + { + _itemsSource.RemoveAt(index); + } + + public void CopyTo(Array array, int index) + { + _itemsSource.CopyTo(array, index); + } + + public IEnumerator GetEnumerator() + { + return _itemsSource.GetEnumerator(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/MarshalingObservableCollection.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/MarshalingObservableCollection.cs new file mode 100644 index 0000000..92fac69 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/MarshalingObservableCollection.cs @@ -0,0 +1,176 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace Tizen.NUI.Components +{ + // Wraps a List which implements INotifyCollectionChanged (usually an ObservableCollection) + // and marshals all of the list modifications to the main thread. Modifications to the underlying + // collection which are made off of the main thread remain invisible to consumers on the main thread + // until they have been processed by the main thread. + + internal class MarshalingObservableCollection : List, INotifyCollectionChanged + { + readonly IList internalCollection; + + public MarshalingObservableCollection(IList list) + { + if (!(list is INotifyCollectionChanged incc)) + { + throw new ArgumentException($"{nameof(list)} must implement {nameof(INotifyCollectionChanged)}"); + } + + internalCollection = list; + incc.CollectionChanged += InternalCollectionChanged; + + foreach (var item in internalCollection) + { + Add(item); + } + } + + class ResetNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs + { + public IList Items { get; } + public ResetNotifyCollectionChangedEventArgs(IList items) + : base(NotifyCollectionChangedAction.Reset) => Items = items; + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + CollectionChanged?.Invoke(this, args); + } + + void InternalCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (args.Action == NotifyCollectionChangedAction.Reset) + { + var items = new List(); + for (int n = 0; n < internalCollection.Count; n++) + { + items.Add(internalCollection[n]); + } + + args = new ResetNotifyCollectionChangedEventArgs(items); + } +/* + if (Device.IsInvokeRequired) + { + Device.BeginInvokeOnMainThread(() => HandleCollectionChange(args)); + } + else + { + HandleCollectionChange(args); + } +*/ + + HandleCollectionChange(args); + } + + void HandleCollectionChange(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + Add(args); + break; + case NotifyCollectionChangedAction.Move: + Move(args); + break; + case NotifyCollectionChangedAction.Remove: + Remove(args); + break; + case NotifyCollectionChangedAction.Replace: + Replace(args); + break; + case NotifyCollectionChangedAction.Reset: + Reset(args); + break; + } + } + + void Move(NotifyCollectionChangedEventArgs args) + { + var count = args.OldItems.Count; + + for (int n = 0; n < count; n++) + { + var toMove = this[args.OldStartingIndex]; + RemoveAt(args.OldStartingIndex); + Insert(args.NewStartingIndex, toMove); + } + + OnCollectionChanged(args); + } + + void Remove(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.OldStartingIndex + args.OldItems.Count - 1; + for (int n = startIndex; n >= args.OldStartingIndex; n--) + { + RemoveAt(n); + } + + OnCollectionChanged(args); + } + + void Replace(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.NewStartingIndex; + foreach (var item in args.NewItems) + { + this[startIndex] = item; + startIndex += 1; + } + + OnCollectionChanged(args); + } + + void Add(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.NewStartingIndex; + foreach (var item in args.NewItems) + { + Insert(startIndex, item); + startIndex += 1; + } + + OnCollectionChanged(args); + } + + void Reset(NotifyCollectionChangedEventArgs args) + { + if (!(args is ResetNotifyCollectionChangedEventArgs resetArgs)) + { + throw new InvalidOperationException($"Cannot guarantee collection accuracy for Resets which do not use {nameof(ResetNotifyCollectionChangedEventArgs)}"); + } + + Clear(); + foreach (var item in resetArgs.Items) + { + Add(item); + } + + OnCollectionChanged(args); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableGroupedSource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableGroupedSource.cs new file mode 100644 index 0000000..0602f87 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableGroupedSource.cs @@ -0,0 +1,466 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace Tizen.NUI.Components +{ + internal class ObservableGroupedSource : IGroupableItemSource, ICollectionChangedNotifier + { + readonly ICollectionChangedNotifier notifier; + readonly IList groupSource; + readonly List groups = new List(); + readonly bool hasGroupHeaders; + readonly bool hasGroupFooters; + bool disposed; + + public int Count + { + get + { + var groupContents = 0; + + for (int n = 0; n < groups.Count; n++) + { + groupContents += groups[n].Count; + } + + return (HasHeader ? 1 : 0) + + (HasFooter ? 1 : 0) + + groupContents; + } + } + + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + + public ObservableGroupedSource(CollectionView colView, ICollectionChangedNotifier changedNotifier) + { + var source = colView.ItemsSource; + + notifier = changedNotifier; + groupSource = source as IList ?? new ListSource(source); + + hasGroupFooters = colView.GroupFooterTemplate != null; + hasGroupHeaders = colView.GroupHeaderTemplate != null; + HasHeader = colView.Header != null; + HasFooter = colView.Footer != null; + + if (groupSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged += CollectionChanged; + } + + UpdateGroupTracking(); + } + + public void Dispose() + { + Dispose(true); + } + + public bool IsFooter(int position) + { + if (!HasFooter) + { + return false; + } + + return position == Count - 1; + } + + public bool IsHeader(int position) + { + return HasHeader && position == 0; + } + + public bool IsGroupHeader(int position) + { + if (IsFooter(position) || IsHeader(position)) + { + return false; + } + + var (group, inGroup) = GetGroupAndIndex(position); + + return groups[group].IsHeader(inGroup); + } + + public bool IsGroupFooter(int position) + { + if (IsFooter(position) || IsHeader(position)) + { + return false; + } + + var (group, inGroup) = GetGroupAndIndex(position); + + return groups[group].IsFooter(inGroup); + } + + public int GetPosition(object item) + { + int previousGroupsOffset = 0; + + for (int groupIndex = 0; groupIndex < groupSource.Count; groupIndex++) + { + if (groupSource[groupIndex].Equals(item)) + { + return AdjustPositionForHeader(groupIndex); + } + + var group = groups[groupIndex]; + var inGroup = group.GetPosition(item); + + if (inGroup > -1) + { + return AdjustPositionForHeader(previousGroupsOffset + inGroup); + } + + previousGroupsOffset += group.Count; + } + + return -1; + } + + public object GetItem(int position) + { + var (group, inGroup) = GetGroupAndIndex(position); + + if (IsGroupFooter(position) || IsGroupHeader(position)) + { + // This is looping to find the group/index twice, need to make it less inefficient + return groupSource[group]; + } + + return groups[group].GetItem(inGroup); + } + + public object GetGroupParent(int position) + { + var (group, inGroup) = GetGroupAndIndex(position); + return groupSource[group]; + } + + // The ICollectionChangedNotifier methods are called by child observable items sources (i.e., the groups) + // This class can then translate their local changes into global positions for upstream notification + // (e.g., to the actual RecyclerView.Adapter, so that it can notify the RecyclerView and handle animating + // the changes) + public void NotifyDataSetChanged() + { + Reload(); + } + + public void NotifyItemChanged(IItemSource group, int localIndex) + { + localIndex = GetAbsolutePosition(group, localIndex); + notifier.NotifyItemChanged(this, localIndex); + } + + public void NotifyItemInserted(IItemSource group, int localIndex) + { + localIndex = GetAbsolutePosition(group, localIndex); + notifier.NotifyItemInserted(this, localIndex); + } + + public void NotifyItemMoved(IItemSource group, int localFromIndex, int localToIndex) + { + localFromIndex = GetAbsolutePosition(group, localFromIndex); + localToIndex = GetAbsolutePosition(group, localToIndex); + notifier.NotifyItemMoved(this, localFromIndex, localToIndex); + } + + public void NotifyItemRangeChanged(IItemSource group, int localStartIndex, int localEndIndex) + { + localStartIndex = GetAbsolutePosition(group, localStartIndex); + localEndIndex = GetAbsolutePosition(group, localEndIndex); + notifier.NotifyItemRangeChanged(this, localStartIndex, localEndIndex); + } + + public void NotifyItemRangeInserted(IItemSource group, int localIndex, int count) + { + localIndex = GetAbsolutePosition(group, localIndex); + notifier.NotifyItemRangeInserted(this, localIndex, count); + } + + public void NotifyItemRangeRemoved(IItemSource group, int localIndex, int count) + { + localIndex = GetAbsolutePosition(group, localIndex); + notifier.NotifyItemRangeRemoved(this, localIndex, count); + } + + public void NotifyItemRemoved(IItemSource group, int localIndex) + { + localIndex = GetAbsolutePosition(group, localIndex); + notifier.NotifyItemRemoved(this, localIndex); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + disposed = true; + + if (disposing) + { + ClearGroupTracking(); + + if (groupSource is INotifyCollectionChanged notifyCollectionChanged) + { + notifyCollectionChanged.CollectionChanged -= CollectionChanged; + } + if (groupSource is IDisposable dispoableSource) dispoableSource.Dispose(); + } + } + + void UpdateGroupTracking() + { + ClearGroupTracking(); + + for (int n = 0; n < groupSource.Count; n++) + { + var source = ItemsSourceFactory.Create(groupSource[n] as IEnumerable, this); + source.HasFooter = hasGroupFooters; + source.HasHeader = hasGroupHeaders; + groups.Add(source); + } + } + + void ClearGroupTracking() + { + for (int n = groups.Count - 1; n >= 0; n--) + { + groups[n].Dispose(); + groups.RemoveAt(n); + } + } + + void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + {/* + if (Device.IsInvokeRequired) + { + Device.BeginInvokeOnMainThread(() => CollectionChanged(args)); + } + else + { + */ + CollectionChanged(args); + //} + } + + void CollectionChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + Add(args); + break; + case NotifyCollectionChangedAction.Remove: + Remove(args); + break; + case NotifyCollectionChangedAction.Replace: + Replace(args); + break; + case NotifyCollectionChangedAction.Move: + Move(args); + break; + case NotifyCollectionChangedAction.Reset: + Reload(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(args)); + } + } + + void Reload() + { + UpdateGroupTracking(); + notifier.NotifyDataSetChanged(); + } + + void Add(NotifyCollectionChangedEventArgs args) + { + var groupIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : groupSource.IndexOf(args.NewItems[0]); + var groupCount = args.NewItems.Count; + + UpdateGroupTracking(); + + // Determine the absolute starting position and the number of items in the groups being added + var absolutePosition = GetAbsolutePosition(groups[groupIndex], 0); + var itemCount = CountItemsInGroups(groupIndex, groupCount); + + if (itemCount == 1) + { + notifier.NotifyItemInserted(this, absolutePosition); + return; + } + + notifier.NotifyItemRangeInserted(this, absolutePosition, itemCount); + } + + void Remove(NotifyCollectionChangedEventArgs args) + { + var groupIndex = args.OldStartingIndex; + + if (groupIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed groups was in the + // collection. So the best we can do is a full reload. + Reload(); + return; + } + + // If we have a start index, we can be more clever about removing the group(s) (and get the nifty animations) + var groupCount = args.OldItems.Count; + + var absolutePosition = GetAbsolutePosition(groups[groupIndex], 0); + + // Figure out how many items are in the groups we're removing + var itemCount = CountItemsInGroups(groupIndex, groupCount); + + if (itemCount == 1) + { + notifier.NotifyItemRemoved(this, absolutePosition); + + UpdateGroupTracking(); + + return; + } + + notifier.NotifyItemRangeRemoved(this, absolutePosition, itemCount); + + UpdateGroupTracking(); + } + + void Replace(NotifyCollectionChangedEventArgs args) + { + var groupCount = args.NewItems.Count; + + if (groupCount != args.OldItems.Count) + { + // The original and replacement sets are of unequal size; this means that most everything currently in + // view will have to be updated. So just reload the whole thing. + Reload(); + return; + } + + var newStartIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : groupSource.IndexOf(args.NewItems[0]); + var oldStartIndex = args.OldStartingIndex > -1 ? args.OldStartingIndex : groupSource.IndexOf(args.OldItems[0]); + + var newItemCount = CountItemsInGroups(newStartIndex, groupCount); + var oldItemCount = CountItemsInGroups(oldStartIndex, groupCount); + + if (newItemCount != oldItemCount) + { + // The original and replacement sets are of unequal size; this means that most everything currently in + // view will have to be updated. So just reload the whole thing. + Reload(); + return; + } + + // We are replacing one set of items with a set of equal size; we can do a simple item or range notification + var firstGroupIndex = Math.Min(newStartIndex, oldStartIndex); + var absolutePosition = GetAbsolutePosition(groups[firstGroupIndex], 0); + + if (newItemCount == 1) + { + notifier.NotifyItemChanged(this, absolutePosition); + UpdateGroupTracking(); + } + else + { + notifier.NotifyItemRangeChanged(this, absolutePosition, newItemCount * 2); + UpdateGroupTracking(); + } + } + + void Move(NotifyCollectionChangedEventArgs args) + { + var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex); + var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + args.NewItems.Count; + + var itemCount = CountItemsInGroups(start, end - start); + var absolutePosition = GetAbsolutePosition(groups[start], 0); + + notifier.NotifyItemRangeChanged(this, absolutePosition, itemCount); + + UpdateGroupTracking(); + } + + int GetAbsolutePosition(IItemSource group, int indexInGroup) + { + var groupIndex = groups.IndexOf(group); + + var runningIndex = 0; + + for (int n = 0; n < groupIndex; n++) + { + runningIndex += groups[n].Count; + } + + return AdjustPositionForHeader(runningIndex + indexInGroup); + } + + (int, int) GetGroupAndIndex(int absolutePosition) + { + absolutePosition = AdjustIndexForHeader(absolutePosition); + + var group = 0; + var localIndex = 0; + + while (absolutePosition > 0) + { + localIndex += 1; + + if (localIndex == groups[group].Count) + { + group += 1; + localIndex = 0; + } + + absolutePosition -= 1; + } + + return (group, localIndex); + } + + int AdjustIndexForHeader(int index) + { + return index - (HasHeader ? 1 : 0); + } + + int AdjustPositionForHeader(int position) + { + return position + (HasHeader ? 1 : 0); + } + + int CountItemsInGroups(int groupStartIndex, int groupCount) + { + var itemCount = 0; + for (int n = 0; n < groupCount; n++) + { + itemCount += groups[groupStartIndex + n].Count; + } + return itemCount; + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableItemSource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableItemSource.cs new file mode 100644 index 0000000..f5581a4 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/ObservableItemSource.cs @@ -0,0 +1,271 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.Collections; +using System.Collections.Specialized; + +namespace Tizen.NUI.Components +{ + internal class ObservableItemSource : IItemSource + { + readonly IEnumerable itemsSource; + readonly ICollectionChangedNotifier notifier; + bool disposed; + + public ObservableItemSource(IEnumerable itemSource, ICollectionChangedNotifier changedNotifier) + { + itemsSource = itemSource as IList ?? itemSource as IEnumerable; + notifier = changedNotifier; + + ((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged; + } + + + internal event NotifyCollectionChangedEventHandler CollectionItemsSourceChanged; + + public int Count => ItemsCount() + (HasHeader ? 1 : 0) + (HasFooter ? 1 : 0); + + public bool HasHeader { get; set; } + public bool HasFooter { get; set; } + + public void Dispose() + { + Dispose(true); + } + + public bool IsFooter(int index) + { + return HasFooter && index == Count - 1; + } + + public bool IsHeader(int index) + { + return HasHeader && index == 0; + } + + public int GetPosition(object item) + { + for (int n = 0; n < ItemsCount(); n++) + { + var elementByIndex = ElementAt(n); + var isEqual = elementByIndex == item || (elementByIndex != null && item != null && elementByIndex.Equals(item)); + + if (isEqual) + { + return AdjustPositionForHeader(n); + } + } + + return -1; + } + + public object GetItem(int position) + { + return ElementAt(AdjustIndexForHeader(position)); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + disposed = true; + + if (disposing) + { + ((INotifyCollectionChanged)itemsSource).CollectionChanged -= CollectionChanged; + } + } + + int AdjustIndexForHeader(int index) + { + return index - (HasHeader ? 1 : 0); + } + + int AdjustPositionForHeader(int position) + { + return position + (HasHeader ? 1 : 0); + } + + void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + {/* + if (Device.IsInvokeRequired) + { + Device.BeginInvokeOnMainThread(() => CollectionChanged(args)); + } + else + {*/ + CollectionChanged(args); + //} + + } + + void CollectionChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + Add(args); + break; + case NotifyCollectionChangedAction.Remove: + Remove(args); + break; + case NotifyCollectionChangedAction.Replace: + Replace(args); + break; + case NotifyCollectionChangedAction.Move: + Move(args); + break; + case NotifyCollectionChangedAction.Reset: + notifier.NotifyDataSetChanged(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(args)); + } + CollectionItemsSourceChanged?.Invoke(this, args); + } + + void Move(NotifyCollectionChangedEventArgs args) + { + var count = args.NewItems.Count; + + if (count == 1) + { + // For a single item, we can use NotifyItemMoved and get the animation + notifier.NotifyItemMoved(this, AdjustPositionForHeader(args.OldStartingIndex), AdjustPositionForHeader(args.NewStartingIndex)); + return; + } + + var start = AdjustPositionForHeader(Math.Min(args.OldStartingIndex, args.NewStartingIndex)); + var end = AdjustPositionForHeader(Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count); + notifier.NotifyItemRangeChanged(this, start, end); + } + + void Add(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + startIndex = AdjustPositionForHeader(startIndex); + var count = args.NewItems.Count; + + if (count == 1) + { + notifier.NotifyItemInserted(this, startIndex); + return; + } + + notifier.NotifyItemRangeInserted(this, startIndex, count); + } + + void Remove(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.OldStartingIndex; + + if (startIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed items were in the + // collection. So the best we can do is a NotifyDataSetChanged() + notifier.NotifyDataSetChanged(); + return; + } + + startIndex = AdjustPositionForHeader(startIndex); + + // If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations) + var count = args.OldItems.Count; + + if (count == 1) + { + notifier.NotifyItemRemoved(this, startIndex); + return; + } + + notifier.NotifyItemRangeRemoved(this, startIndex, count); + } + + void Replace(NotifyCollectionChangedEventArgs args) + { + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + startIndex = AdjustPositionForHeader(startIndex); + var newCount = args.NewItems.Count; + + if (newCount == args.OldItems.Count) + { + // We are replacing one set of items with a set of equal size; we can do a simple item or range + // notification to the adapter + if (newCount == 1) + { + notifier.NotifyItemChanged(this, startIndex); + } + else + { + notifier.NotifyItemRangeChanged(this, startIndex, newCount); + } + + return; + } + + // The original and replacement sets are of unequal size; this means that everything currently in view will + // have to be updated. So we just have to use NotifyDataSetChanged and let the RecyclerView update everything + notifier.NotifyDataSetChanged(); + } + + internal int ItemsCount() + { + if (itemsSource is IList list) + return list.Count; + + int count = 0; + foreach (var item in itemsSource) + count++; + return count; + } + + internal object ElementAt(int index) + { + if (itemsSource is IList list) + return list[index]; + + int count = 0; + foreach (var item in itemsSource) + { + if (count == index) + return item; + count++; + } + + return -1; + } + + internal int IndexOf(object item) + { + if (itemsSource is IList list) + return list.IndexOf(item); + + int count = 0; + foreach (var i in itemsSource) + { + if (i == item) + return count; + count++; + } + + return -1; + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/UngroupedItemSource.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/UngroupedItemSource.cs new file mode 100644 index 0000000..32f0340 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/ItemSource/UngroupedItemSource.cs @@ -0,0 +1,73 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +namespace Tizen.NUI.Components +{ + internal class UngroupedItemSource : IGroupableItemSource + { + readonly IItemSource source; + + public UngroupedItemSource(IItemSource itemSource) + { + source = itemSource; + } + + public int Count => source.Count; + + public bool HasHeader { get => source.HasHeader; set => source.HasHeader = value; } + public bool HasFooter { get => source.HasFooter; set => source.HasFooter = value; } + + public void Dispose() + { + source.Dispose(); + } + + public object GetItem(int position) + { + return source.GetItem(position); + } + + public int GetPosition(object item) + { + return source.GetPosition(item); + } + + public bool IsFooter(int position) + { + return source.IsFooter(position); + } + + public bool IsGroupFooter(int position) + { + return false; + } + + public bool IsGroupHeader(int position) + { + return false; + } + + public bool IsHeader(int position) + { + return source.IsHeader(position); + } + + public object GetGroupParent(int position) + { + return null; + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/GridLayouter.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/GridLayouter.cs new file mode 100644 index 0000000..7e98751 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/GridLayouter.cs @@ -0,0 +1,724 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Tizen.NUI.BaseComponents; + +namespace Tizen.NUI.Components +{ + /// + /// This class implements a grid box layout. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class GridLayouter : ItemsLayouter + { + private CollectionView colView; + private Size2D sizeCandidate; + private int spanSize = 1; + private float align = 0.5f; + private bool hasHeader; + private float headerSize; + private bool hasFooter; + private float footerSize; + private bool isGrouped; + private readonly List groups = new List(); + private float groupHeaderSize; + private float groupFooterSize; + private GroupInfo Visited; + + /// + /// Clean up ItemsLayouter. + /// + /// ItemsView of layouter. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void Initialize(RecyclerView view) + { + colView = view as CollectionView; + if (colView == null) + { + throw new ArgumentException("GridLayouter only can be applied CollectionView.", nameof(view)); + } + + // 1. Clean Up + foreach (RecyclerViewItem item in VisibleItems) + { + colView.UnrealizeItem(item, false); + } + VisibleItems.Clear(); + groups.Clear(); + + FirstVisible = 0; + LastVisible = 0; + + IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal); + + RecyclerViewItem header = colView?.Header; + RecyclerViewItem footer = colView?.Footer; + float width, height; + int count = colView.InternalItemSource.Count; + int pureCount = count - (header ? 1 : 0) - (footer ? 1 : 0); + + // 2. Get the header / footer and size deligated item and measure the size. + if (header != null) + { + MeasureChild(colView, header); + + width = header.Layout != null ? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; + height = header.Layout != null ? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; + + headerSize = IsHorizontal ? width : height; + hasHeader = true; + + colView.UnrealizeItem(header); + } + + if (footer != null) + { + MeasureChild(colView, footer); + + width = footer.Layout != null ? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; + height = footer.Layout != null ? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; + + footerSize = IsHorizontal ? width : height; + footer.Index = count - 1; + hasFooter = true; + + colView.UnrealizeItem(footer); + } + + int firstIndex = header ? 1 : 0; + + if (colView.IsGrouped) + { + isGrouped = true; + + if (colView.GroupHeaderTemplate != null) + { + while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++; + //must be always true + if (colView.InternalItemSource.IsGroupHeader(firstIndex)) + { + RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex); + firstIndex++; + + if (groupHeader == null) throw new Exception("[" + firstIndex + "] Group Header failed to realize!"); + + // Need to Set proper hieght or width on scroll direciton. + if (groupHeader.Layout == null) + { + width = groupHeader.WidthSpecification; + height = groupHeader.HeightSpecification; + } + else + { + MeasureChild(colView, groupHeader); + + width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height); + // pick the StepCandidate. + groupHeaderSize = IsHorizontal ? width : height; + colView.UnrealizeItem(groupHeader); + } + } + else + { + groupHeaderSize = 0F; + } + + if (colView.GroupFooterTemplate != null) + { + int firstFooter = firstIndex; + while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++; + //must be always true + if (colView.InternalItemSource.IsGroupFooter(firstFooter)) + { + RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter); + + if (groupFooter == null) throw new Exception("[" + firstFooter + "] Group Footer failed to realize!"); + // Need to Set proper hieght or width on scroll direciton. + if (groupFooter.Layout == null) + { + width = groupFooter.WidthSpecification; + height = groupFooter.HeightSpecification; + } + else + { + MeasureChild(colView, groupFooter); + + width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + // pick the StepCandidate. + groupFooterSize = IsHorizontal ? width : height; + + colView.UnrealizeItem(groupFooter); + } + } + else + { + groupFooterSize = 0F; + } + } + else isGrouped = false; + + bool failed = false; + //Final Check of FirstIndex + while (colView.InternalItemSource.IsHeader(firstIndex) || + colView.InternalItemSource.IsGroupHeader(firstIndex) || + colView.InternalItemSource.IsGroupFooter(firstIndex)) + { + if (colView.InternalItemSource.IsFooter(firstIndex)) + { + StepCandidate = 0F; + failed = true; + break; + } + firstIndex++; + } + + sizeCandidate = new Size2D(0, 0); + if (!failed) + { + // Get Size Deligate. FIXME if group exist index must be changed. + RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex); + if (sizeDeligate == null) + { + throw new Exception("Cannot create content from DatTemplate."); + } + sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex); + + // Need to Set proper hieght or width on scroll direciton. + if (sizeDeligate.Layout == null) + { + width = sizeDeligate.WidthSpecification; + height = sizeDeligate.HeightSpecification; + } + else + { + MeasureChild(colView, sizeDeligate); + + width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + //Console.WriteLine("[NUI] item Size {0} :{1}", width, height); + + // pick the StepCandidate. + StepCandidate = IsHorizontal ? width : height; + spanSize = IsHorizontal ? Convert.ToInt32(Math.Truncate((double)(colView.Size.Height / height))) : + Convert.ToInt32(Math.Truncate((double)(colView.Size.Width / width))); + + sizeCandidate = new Size2D(Convert.ToInt32(width), Convert.ToInt32(height)); + + colView.UnrealizeItem(sizeDeligate); + } + + if (StepCandidate < 1) StepCandidate = 1; + if (spanSize < 1) spanSize = 1; + + if (isGrouped) + { + float Current = 0.0F; + IGroupableItemSource source = colView.InternalItemSource; + GroupInfo currentGroup = null; + + for (int i = 0; i < count; i++) + { + if (i == 0 && hasHeader) + { + Current += headerSize; + } + else if (i == count - 1 && hasFooter) + { + Current += footerSize; + } + else + { + //GroupHeader must always exist in group usage. + if (source.IsGroupHeader(i)) + { + currentGroup = new GroupInfo() + { + GroupParent = source.GetGroupParent(i), + StartIndex = i, + Count = 1, + GroupSize = groupHeaderSize, + GroupPosition = Current + }; + groups.Add(currentGroup); + Current += groupHeaderSize; + } + //optional + else if (source.IsGroupFooter(i)) + { + //currentGroup.hasFooter = true; + currentGroup.Count++; + currentGroup.GroupSize += groupFooterSize; + Current += groupFooterSize; + } + else + { + currentGroup.Count++; + int index = i - currentGroup.StartIndex - 1; // groupHeader must always exist. + if ((index % spanSize) == 0) + { + currentGroup.GroupSize += StepCandidate; + Current += StepCandidate; + } + } + } + } + ScrollContentSize = Current; + } + else + { + // 3. Measure the scroller content size. + ScrollContentSize = StepCandidate * Convert.ToInt32(Math.Ceiling((double)pureCount / (double)spanSize)); + if (hasHeader) ScrollContentSize += headerSize; + if (hasFooter) ScrollContentSize += footerSize; + } + + if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize; + else colView.ContentContainer.SizeHeight = ScrollContentSize; + + base.Initialize(colView); + //Console.WriteLine("Init Done, StepCnadidate{0}, spanSize{1}, Scroll{2}", StepCandidate, spanSize, ScrollContentSize); + } + + /// + /// This is called to find out where items are lain out according to current scroll position. + /// + /// Scroll position which is calculated by ScrollableBase + /// boolean force flag to layouting forcely. + public override void RequestLayout(float scrollPosition, bool force = false) + { + // Layouting is only possible after once it intialized. + if (!IsInitialized) return; + int LastIndex = colView.InternalItemSource.Count; + + if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return; + PrevScrollPosition = Math.Abs(scrollPosition); + + int prevFirstVisible = FirstVisible; + int prevLastVisible = LastVisible; + bool IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal); + + (float X, float Y) visibleArea = (PrevScrollPosition, + PrevScrollPosition + (IsHorizontal ? colView.Size.Width : colView.Size.Height) + ); + + //Console.WriteLine("[NUI] itemsView [{0},{1}] [{2},{3}]", colView.Size.Width, colView.Size.Height, colView.ContentContainer.Size.Width, colView.ContentContainer.Size.Height); + + // 1. Set First/Last Visible Item Index. + (int start, int end) = FindVisibleItems(visibleArea); + FirstVisible = start; + LastVisible = end; + + //Console.WriteLine("[NUI] {0} :visibleArea before [{1},{2}] after [{3},{4}]", scrollPosition, prevFirstVisible, prevLastVisible, FirstVisible, LastVisible); + + // 2. Unrealize invisible items. + List unrealizedItems = new List(); + foreach (RecyclerViewItem item in VisibleItems) + { + if (item.Index < FirstVisible || item.Index > LastVisible) + { + //Console.WriteLine("[NUI] Unrealize{0}!", item.Index); + unrealizedItems.Add(item); + colView.UnrealizeItem(item); + } + } + VisibleItems.RemoveAll(unrealizedItems.Contains); + + //Console.WriteLine("Realize Begin [{0} to {1}]", FirstVisible, LastVisible); + // 3. Realize and placing visible items. + for (int i = FirstVisible; i <= LastVisible; i++) + { + //Console.WriteLine("[NUI] Realize!"); + RecyclerViewItem item = null; + // 4. Get item if visible or realize new. + if (i >= prevFirstVisible && i <= prevLastVisible) + { + item = GetVisibleItem(i); + if (item) continue; + } + if (item == null) item = colView.RealizeItem(i); + VisibleItems.Add(item); + + (float x, float y) = GetItemPosition(i); + // 5. Placing item. + item.Position = new Position(x, y); + //Console.WriteLine("[NUI] ["+item.Index+"] ["+item.Position.X+", "+item.Position.Y+" ==== \n"); + } + //Console.WriteLine("Realize Done"); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override (float X, float Y) GetItemPosition(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + if (colView == null) return (0, 0); + + return GetItemPosition(colView.InternalItemSource.GetPosition(item)); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override (float X, float Y) GetItemSize(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + if (sizeCandidate == null) return (0, 0); + + if (isGrouped) + { + int index = colView.InternalItemSource.GetPosition(item); + float view = (IsHorizontal ? colView.Size.Height : colView.Size.Width); + + if (colView.InternalItemSource.IsGroupHeader(index)) + { + return (IsHorizontal ? (groupHeaderSize, view) : (view, groupHeaderSize)); + } + else if (colView.InternalItemSource.IsGroupFooter(index)) + { + return (IsHorizontal ? (groupFooterSize, view) : (view, groupFooterSize)); + } + } + + return (sizeCandidate.Width, sizeCandidate.Height); + } + + /// + public override void NotifyItemSizeChanged(RecyclerViewItem item) + { + // All Item size need to be same in grid! + // if you want to change item size, change dataTemplate to re-initing. + return; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override float CalculateLayoutOrientationSize() + { + //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize); + return ScrollContentSize; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override float CalculateCandidateScrollPosition(float scrollPosition) + { + //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize); + return scrollPosition; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + { + if (currentFocusedView == null) + throw new ArgumentNullException(nameof(currentFocusedView)); + + View nextFocusedView = null; + int targetSibling = -1; + bool IsHorizontal = colView.ScrollingDirection == ScrollableBase.Direction.Horizontal; + + switch (direction) + { + case View.FocusDirection.Left: + { + targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling; + break; + } + case View.FocusDirection.Right: + { + targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling; + break; + } + case View.FocusDirection.Up: + { + targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1; + break; + } + case View.FocusDirection.Down: + { + targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1; + break; + } + } + + if (targetSibling > -1 && targetSibling < Container.Children.Count) + { + RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem; + if (candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count) + { + nextFocusedView = candidate; + } + } + return nextFocusedView; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea) + { + int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0); + int adds = spanSize * 2; + int skipGroup = -1; + (int start, int end) found = (0, 0); + + // Header is Showing + if (hasHeader && visibleArea.X < headerSize) + { + found.start = 0; + } + else + { + if (isGrouped) + { + bool failed = true; + foreach (GroupInfo gInfo in groups) + { + skipGroup++; + // in the Group + if (gInfo.GroupPosition <= visibleArea.X && + gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X) + { + if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.X) + { + found.start = gInfo.StartIndex - adds; + failed = false; + } + //can be step in spanSize... + for (int i = 1; i < gInfo.Count; i++) + { + if (!failed) break; + // Reach last index of group. + if (i == (gInfo.Count - 1)) + { + found.start = gInfo.StartIndex + i - adds; + failed = false; + break; + + } + else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.X - gInfo.GroupPosition - groupHeaderSize) + { + found.start = gInfo.StartIndex + i - adds; + failed = false; + break; + } + } + } + } + //footer only shows? + if (failed) + { + found.start = MaxIndex; + } + } + else + { + float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0); + found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - 1) * spanSize; + if (hasHeader) found.start += 1; + } + if (found.start < 0) found.start = 0; + } + + if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize) + { + found.end = MaxIndex + 1; + } + else + { + if (isGrouped) + { + bool failed = true; + // can it be start from founded group...? + //foreach(GroupInfo gInfo in groups.Skip(skipGroup)) + foreach (GroupInfo gInfo in groups) + { + // in the Group + if (gInfo.GroupPosition <= visibleArea.Y && + gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y) + { + if (gInfo.GroupPosition + groupHeaderSize >= visibleArea.Y) + { + found.end = gInfo.StartIndex + adds; + failed = false; + } + //can be step in spanSize... + for (int i = 1; i < gInfo.Count; i++) + { + if (!failed) break; + // Reach last index of group. + if (i == (gInfo.Count - 1)) + { + found.end = gInfo.StartIndex + i + adds; + failed = false; + break; + } + else if ((((i - 1) / spanSize) * StepCandidate) + StepCandidate >= visibleArea.Y - gInfo.GroupPosition - groupHeaderSize) + { + found.end = gInfo.StartIndex + i + adds; + failed = false; + break; + } + } + } + } + //footer only shows? + if (failed) + { + found.start = MaxIndex; + } + } + else + { + float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0); + //Need to Consider GroupHeight!!!! + found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + 1) * spanSize + adds; + if (hasHeader) found.end += 1; + } + if (found.end > (MaxIndex)) found.end = MaxIndex; + } + return found; + } + + private (float X, float Y) GetItemPosition(int index) + { + float xPos, yPos; + if (sizeCandidate == null) return (0, 0); + + if (hasHeader && index == 0) + { + return (0, 0); + } + if (hasFooter && index == colView.InternalItemSource.Count - 1) + { + xPos = IsHorizontal ? ScrollContentSize - footerSize : 0; + yPos = IsHorizontal ? 0 : ScrollContentSize - footerSize; + return (xPos, yPos); + } + if (isGrouped) + { + GroupInfo myGroup = GetGroupInfo(index); + if (colView.InternalItemSource.IsGroupHeader(index)) + { + xPos = IsHorizontal ? myGroup.GroupPosition : 0; + yPos = IsHorizontal ? 0 : myGroup.GroupPosition; + } + else if (colView.InternalItemSource.IsGroupFooter(index)) + { + xPos = IsHorizontal ? myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize : 0; + yPos = IsHorizontal ? 0 : myGroup.GroupPosition + myGroup.GroupSize - groupFooterSize; + } + else + { + int pureIndex = index - myGroup.StartIndex - 1; + int division = pureIndex / spanSize; + int remainder = pureIndex % spanSize; + int emptyArea = IsHorizontal ? (int)(colView.Size.Height - (sizeCandidate.Height * spanSize)) : + (int)(colView.Size.Width - (sizeCandidate.Width * spanSize)); + if (division < 0) division = 0; + if (remainder < 0) remainder = 0; + + xPos = IsHorizontal ? division * sizeCandidate.Width + myGroup.GroupPosition + groupHeaderSize : emptyArea * align + remainder * sizeCandidate.Width; + yPos = IsHorizontal ? emptyArea * align + remainder * sizeCandidate.Height : division * sizeCandidate.Height + myGroup.GroupPosition + groupHeaderSize; + } + } + else + { + int pureIndex = index - (colView.Header ? 1 : 0); + // int convert must be truncate value. + int division = pureIndex / spanSize; + int remainder = pureIndex % spanSize; + int emptyArea = IsHorizontal ? (int)(colView.Size.Height - (sizeCandidate.Height * spanSize)) : + (int)(colView.Size.Width - (sizeCandidate.Width * spanSize)); + if (division < 0) division = 0; + if (remainder < 0) remainder = 0; + + xPos = IsHorizontal ? division * sizeCandidate.Width + (hasHeader ? headerSize : 0) : emptyArea * align + remainder * sizeCandidate.Width; + yPos = IsHorizontal ? emptyArea * align + remainder * sizeCandidate.Height : division * sizeCandidate.Height + (hasHeader ? headerSize : 0); + } + + return (xPos, yPos); + } + + private RecyclerViewItem GetVisibleItem(int index) + { + foreach (RecyclerViewItem item in VisibleItems) + { + if (item.Index == index) return item; + } + + return null; + } + + private GroupInfo GetGroupInfo(int index) + { + if (Visited != null) + { + if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) + return Visited; + } + if (hasHeader && index == 0) return null; + foreach (GroupInfo group in groups) + { + if (group.StartIndex <= index && group.StartIndex + group.Count > index) + { + Visited = group; + return group; + } + } + Visited = null; + return null; + } + + /* + private object GetGroupParent(int index) + { + if (Visited != null) + { + if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) + return Visited.GroupParent; + } + if (hasHeader && index == 0) return null; + foreach (GroupInfo group in groups) + { + if (group.StartIndex <= index && group.StartIndex + group.Count > index) + { + Visited = group; + return group.GroupParent; + } + } + Visited = null; + return null; + } + */ + + class GroupInfo + { + public object GroupParent; + public int StartIndex; + public int Count; + public float GroupSize; + public float GroupPosition; + //Items relative position from the GroupPosition + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/ItemsLayouter.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/ItemsLayouter.cs new file mode 100644 index 0000000..19b1a19 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/ItemsLayouter.cs @@ -0,0 +1,350 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using Tizen.NUI.BaseComponents; +using System.Collections.Generic; +using System.ComponentModel; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Components +{ + /// + /// Default layout manager for CollectionView. + /// Lay out ViewItem and recycle ViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class ItemsLayouter : ICollectionChangedNotifier, IDisposable + { + private bool disposed = false; + + /// + /// Container which contains ViewItems. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected View Container{ get ; set; } + + /// + /// Parent ItemsView. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected RecyclerView ItemsView{ get; set; } + + /// + /// The last scrolled position which is calculated by ScrollableBase. The value should be updated in the Recycle() method. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected float PrevScrollPosition { get; set; } + + /// + /// First index of visible items. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected int FirstVisible { get; set; } = -1; + + /// + /// Last index of visible items. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected int LastVisible { get; set; } = -1; + + /// + /// Visible ViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected List VisibleItems { get; } = new List(); + + /// + /// Flag of layouter initialization. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected bool IsInitialized { get; set; } = false; + + /// + /// Candidate item step size for scroll size measure. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected float StepCandidate { get; set; } + + /// + /// Content size of scrollable. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected float ScrollContentSize { get; set; } + + /// + /// boolean flag of scrollable horizontal direction. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected bool IsHorizontal { get; set; } + + /// + /// Clean up ItemsLayouter. + /// + /// ItemsView of layouter. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void Initialize(RecyclerView view) + { + ItemsView = view ?? throw new ArgumentNullException(nameof(view)); + Container = view.ContentContainer; + PrevScrollPosition = 0.0f; + + IsHorizontal = (view.ScrollingDirection == ScrollableBase.Direction.Horizontal); + + IsInitialized = true; + } + + /// + /// This is called to find out where items are lain out according to current scroll position. + /// + /// Scroll position which is calculated by ScrollableBase + /// boolean force flag to layouting forcely. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void RequestLayout(float scrollPosition, bool force = false) + { + // Layouting Items in scrollPosition. + } + + /// + /// Clear the current screen and all properties. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void Clear() + { + foreach (RecyclerViewItem item in VisibleItems) + { + if (ItemsView != null) ItemsView.UnrealizeItem(item, false); + } + VisibleItems.Clear(); + ItemsView = null; + Container = null; + } + + /// + /// Position of layouting item. + /// + /// item of dataset. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual (float X, float Y) GetItemPosition(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + // Layouting Items in scrollPosition. + return (0, 0); + } + + /// + /// Size of layouting item. + /// + /// item of dataset. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual (float X, float Y) GetItemSize(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + // Layouting Items in scrollPosition. + return (0, 0); + } + + /// + /// This is called to find out how much container size can be. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual float CalculateLayoutOrientationSize() + { + return 0.0f; + } + + /// + /// Adjust scrolling position by own scrolling rules. + /// + /// Scroll position which is calculated by ScrollableBase + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual float CalculateCandidateScrollPosition(float scrollPosition) + { + return scrollPosition; + } + + /// + /// Notify the relayout of ViewItem. + /// + /// updated ViewItem. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemSizeChanged(RecyclerViewItem item) + { + } + + /// + /// Notify the dataset is Changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyDataSetChanged() + { + Initialize(ItemsView); + } + + /// + /// Notify the observable item in startIndex is changed. + /// + /// Dataset source. + /// Changed item index. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemChanged(IItemSource source, int startIndex) + { + } + + /// + /// Notify the observable item is inserted in dataset. + /// + /// Dataset source. + /// Inserted item index. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemInserted(IItemSource source, int startIndex) + { + } + + /// + /// Notify the observable item is moved from fromPosition to ToPosition. + /// + /// Dataset source. + /// Previous item position. + /// Moved item position. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition) + { + } + + /// + /// Notify the range of observable items from start to end are changed. + /// + /// Dataset source. + /// Start index of changed items range. + /// End index of changed items range. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemRangeChanged(IItemSource source, int startRange, int endRange) + { + } + + /// + /// Notify the count range of observable items are inserted in startIndex. + /// + /// Dataset source. + /// Start index of inserted items range. + /// The number of inserted items. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemRangeInserted(IItemSource source, int startIndex, int count) + { + } + + /// + /// Notify the count range of observable items from the startIndex are removed. + /// + /// Dataset source. + /// Start index of removed items range. + /// The number of removed items + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count) + { + } + + /// + /// Notify the observable item in startIndex is removed. + /// + /// Dataset source. + /// Index of removed item. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void NotifyItemRemoved(IItemSource source, int startIndex) + { + } + + /// + /// Gets the next keyboard focusable view in this control towards the given direction.
+ /// A control needs to override this function in order to support two dimensional keyboard navigation.
+ ///
+ /// The current focused view. + /// The direction to move the focus towards. + /// Whether the focus movement should be looped within the control. + /// The next keyboard focusable view in this control or an empty handle if no view can be focused. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + { + return null; + } + + /// + /// Dispose ItemsLayouter and all children on it. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Measure the size of chlid ViewItem manually. + /// + /// Parent ItemsView. + /// Child ViewItem to Measure() + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void MeasureChild(RecyclerView parent, RecyclerViewItem child) + { + if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (child == null) throw new ArgumentNullException(nameof(child)); + + if (child.Layout == null) return; + + //FIXME: This measure can be restricted size of child to be less than parent size. + // but in some multiple-line TextLabel can be long enough to over the it's parent size. + + MeasureSpecification childWidthMeasureSpec = LayoutGroup.GetChildMeasureSpecification( + new MeasureSpecification(new LayoutLength(parent.Size.Width), MeasureSpecification.ModeType.Exactly), + new LayoutLength(0), + new LayoutLength(child.WidthSpecification)); + + MeasureSpecification childHeightMeasureSpec = LayoutGroup.GetChildMeasureSpecification( + new MeasureSpecification(new LayoutLength(parent.Size.Height), MeasureSpecification.ModeType.Exactly), + new LayoutLength(0), + new LayoutLength(child.HeightSpecification)); + + child.Layout.Measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + /// + /// Find consecutive visible items index. + /// + /// float turple of visible area start position to end position. + /// int turple of start index to end index + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual (int start, int end) FindVisibleItems((float X, float Y) visibleArea) + { + return (0, 0); + } + + /// + /// Dispose ItemsLayouter and all children on it. + /// + /// true when it disposed by Dispose(). + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + disposed = true; + if (disposing) Clear(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/LinearLayouter.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/LinearLayouter.cs new file mode 100644 index 0000000..a247d0e --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/LinearLayouter.cs @@ -0,0 +1,743 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using System.Linq; +using Tizen.NUI.BaseComponents; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Components +{ + + + /// + /// [Draft] This class implements a linear box layout. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class LinearLayouter : ItemsLayouter + { + private readonly List ItemPosition = new List(); + private readonly List ItemSize = new List(); + private int ItemSizeChanged = -1; + private CollectionView colView; + private bool hasHeader; + private float headerSize; + private bool hasFooter; + private float footerSize; + private bool isGrouped; + private readonly List groups = new List(); + private float groupHeaderSize; + private float groupFooterSize; + private GroupInfo Visited; + + /// + /// Clean up ItemsLayouter. + /// + /// ItemsView of layouter. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void Initialize(RecyclerView view) + { + colView = view as CollectionView; + if (colView == null) + { + throw new ArgumentException("LinearLayouter only can be applied CollectionView.", nameof(view)); + } + // 1. Clean Up + foreach (RecyclerViewItem item in VisibleItems) + { + colView.UnrealizeItem(item, false); + } + VisibleItems.Clear(); + ItemPosition.Clear(); + ItemSize.Clear(); + groups.Clear(); + + FirstVisible = 0; + LastVisible = 0; + + IsHorizontal = (colView.ScrollingDirection == ScrollableBase.Direction.Horizontal); + + RecyclerViewItem header = colView?.Header; + RecyclerViewItem footer = colView?.Footer; + float width, height; + int count = colView.InternalItemSource.Count; + + if (header != null) + { + MeasureChild(colView, header); + + width = header.Layout != null ? header.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; + height = header.Layout != null ? header.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; + + headerSize = IsHorizontal ? width : height; + hasHeader = true; + + colView.UnrealizeItem(header); + } + else hasHeader = false; + + if (footer != null) + { + MeasureChild(colView, footer); + + width = footer.Layout != null ? footer.Layout.MeasuredWidth.Size.AsRoundedValue() : 0; + height = footer.Layout != null ? footer.Layout.MeasuredHeight.Size.AsRoundedValue() : 0; + + footerSize = IsHorizontal ? width : height; + footer.Index = count - 1; + hasFooter = true; + + colView.UnrealizeItem(footer); + } + else hasFooter = false; + + //No Internal Source exist. + if (count == (hasHeader? (hasFooter? 2 : 1) : 0)) return; + + int firstIndex = hasHeader ? 1 : 0; + + if (colView.IsGrouped) + { + isGrouped = true; + + if (colView.GroupHeaderTemplate != null) + { + while (!colView.InternalItemSource.IsGroupHeader(firstIndex)) firstIndex++; + //must be always true + if (colView.InternalItemSource.IsGroupHeader(firstIndex)) + { + RecyclerViewItem groupHeader = colView.RealizeItem(firstIndex); + firstIndex++; + + if (groupHeader == null) throw new Exception("["+firstIndex+"] Group Header failed to realize!"); + + // Need to Set proper hieght or width on scroll direciton. + if (groupHeader.Layout == null) + { + width = groupHeader.WidthSpecification; + height = groupHeader.HeightSpecification; + } + else + { + MeasureChild(colView, groupHeader); + + width = groupHeader.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = groupHeader.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + //Console.WriteLine("[NUI] GroupHeader Size {0} :{0}", width, height); + // pick the StepCandidate. + groupHeaderSize = IsHorizontal ? width : height; + colView.UnrealizeItem(groupHeader); + } + } + else + { + groupHeaderSize = 0F; + } + + if (colView.GroupFooterTemplate != null) + { + int firstFooter = firstIndex; + while (!colView.InternalItemSource.IsGroupFooter(firstFooter)) firstFooter++; + //must be always true + if (colView.InternalItemSource.IsGroupFooter(firstFooter)) + { + RecyclerViewItem groupFooter = colView.RealizeItem(firstFooter); + + if (groupFooter == null) throw new Exception("["+firstFooter+"] Group Footer failed to realize!"); + // Need to Set proper hieght or width on scroll direciton. + if (groupFooter.Layout == null) + { + width = groupFooter.WidthSpecification; + height = groupFooter.HeightSpecification; + } + else + { + MeasureChild(colView, groupFooter); + + width = groupFooter.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = groupFooter.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + // pick the StepCandidate. + groupFooterSize = IsHorizontal ? width : height; + + colView.UnrealizeItem(groupFooter); + } + } + else + { + groupFooterSize = 0F; + } + } + else isGrouped = false; + + bool failed = false; + //Final Check of FirstIndex + while (colView.InternalItemSource.IsHeader(firstIndex) || + colView.InternalItemSource.IsGroupHeader(firstIndex) || + colView.InternalItemSource.IsGroupFooter(firstIndex)) + { + if (colView.InternalItemSource.IsFooter(firstIndex)) + { + StepCandidate = 0F; + failed = true; + break; + } + firstIndex++; + } + + if (!failed) + { + RecyclerViewItem sizeDeligate = colView.RealizeItem(firstIndex); + if (sizeDeligate == null) + { + // error ! + throw new Exception("Cannot create content from DatTemplate."); + } + + sizeDeligate.BindingContext = colView.InternalItemSource.GetItem(firstIndex); + + // Need to Set proper hieght or width on scroll direciton. + if (sizeDeligate.Layout == null) + { + width = sizeDeligate.WidthSpecification; + height = sizeDeligate.HeightSpecification; + } + else + { + MeasureChild(colView, sizeDeligate); + + width = sizeDeligate.Layout.MeasuredWidth.Size.AsRoundedValue(); + height = sizeDeligate.Layout.MeasuredHeight.Size.AsRoundedValue(); + } + //Console.WriteLine("[NUI] Layout Size {0} :{0}", width, height); + // pick the StepCandidate. + StepCandidate = IsHorizontal ? width : height; + if (StepCandidate == 0) StepCandidate = 1; //???? + + colView.UnrealizeItem(sizeDeligate); + } + + float Current = 0.0F; + IGroupableItemSource source = colView.InternalItemSource; + GroupInfo currentGroup = null; + for (int i = 0; i < count; i++) + { + if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll) + { + if (i == 0 && hasHeader) + ItemSize.Add(headerSize); + else if (i == count -1 && hasFooter) + ItemSize.Add(footerSize); + else if (source.IsGroupHeader(i)) + ItemSize.Add(groupHeaderSize); + else if (source.IsGroupFooter(i)) + ItemSize.Add(groupFooterSize); + else ItemSize.Add(StepCandidate); + } + if (isGrouped) + { + if (i == 0 && hasHeader) + { + //ItemPosition.Add(Current); + Current += headerSize; + } + else if (i == count -1 && hasFooter) + { + //ItemPosition.Add(Current); + Current += footerSize; + } + else + { + //GroupHeader must always exist in group usage. + if (source.IsGroupHeader(i)) + { + currentGroup = new GroupInfo() + { + GroupParent = source.GetGroupParent(i), + //hasHeader = true, + //hasFooter = false, + StartIndex = i, + Count = 1, + GroupSize = groupHeaderSize, + GroupPosition = Current + }; + currentGroup.ItemPosition.Add(0); + groups.Add(currentGroup); + Current += groupHeaderSize; + } + //optional + else if (source.IsGroupFooter(i)) + { + //currentGroup.hasFooter = true; + currentGroup.Count++; + currentGroup.GroupSize += groupFooterSize; + currentGroup.ItemPosition.Add(Current - currentGroup.GroupPosition); + Current += groupFooterSize; + } + else + { + currentGroup.Count++; + currentGroup.GroupSize += StepCandidate; + currentGroup.ItemPosition.Add(Current - currentGroup.GroupPosition); + Current += StepCandidate; + } + } + } + else + { + ItemPosition.Add(Current); + + if (i == 0 && hasHeader) Current += headerSize; + else if (i == count -1 && hasFooter) Current += footerSize; + else Current += StepCandidate; + } + } + + ScrollContentSize = Current; + if (IsHorizontal) colView.ContentContainer.SizeWidth = ScrollContentSize; + else colView.ContentContainer.SizeHeight = ScrollContentSize; + + + base.Initialize(view); + //Console.WriteLine("[NUI] Init Done, StepCnadidate{0}, Scroll{1}", StepCandidate, ScrollContentSize); + } + + /// + /// This is called to find out where items are lain out according to current scroll position. + /// + /// Scroll position which is calculated by ScrollableBase + /// boolean force flag to layouting forcely. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void RequestLayout(float scrollPosition, bool force = false) + { + // Layouting is only possible after once it intialized. + if (!IsInitialized) return; + int LastIndex = colView.InternalItemSource.Count -1; + + if (!force && PrevScrollPosition == Math.Abs(scrollPosition)) return; + PrevScrollPosition = Math.Abs(scrollPosition); + + if (ItemSizeChanged >= 0) + { + for (int i = ItemSizeChanged; i <= LastIndex; i++) + UpdatePosition(i); + ScrollContentSize = ItemPosition[LastIndex - 1] + GetItemSize(LastIndex); + } + + int prevFirstVisible = FirstVisible; + int prevLastVisible = LastVisible; + + (float X, float Y) visibleArea = (PrevScrollPosition, + PrevScrollPosition + ( IsHorizontal ? colView.Size.Width : colView.Size.Height) + ); + + // 1. Set First/Last Visible Item Index. + (int start, int end) = FindVisibleItems(visibleArea); + FirstVisible = start; + LastVisible = end; + + // 2. Unrealize invisible items. + List unrealizedItems = new List(); + foreach (RecyclerViewItem item in VisibleItems) + { + if (item.Index < FirstVisible || item.Index > LastVisible) + { + //Console.WriteLine("[NUI] Unrealize{0}!", item.Index); + unrealizedItems.Add(item); + colView.UnrealizeItem(item); + } + } + VisibleItems.RemoveAll(unrealizedItems.Contains); + + // 3. Realize and placing visible items. + for (int i = FirstVisible; i <= LastVisible; i++) + { + RecyclerViewItem item = null; + // 4. Get item if visible or realize new. + if (i >= prevFirstVisible && i <= prevLastVisible) + { + item = GetVisibleItem(i); + if (item) continue; + } + if (item == null) item = colView.RealizeItem(i); + + VisibleItems.Add(item); + + // 5. Placing item. + float posX = 0F, posY = 0F; + if (isGrouped) + { + //isHeader? + if (colView.Header == item) + { + posX = 0F; + posY = 0F; + } + else if (colView.Footer == item) + { + posX = (IsHorizontal ? ScrollContentSize - item.SizeWidth : 0F); + posY =(IsHorizontal ? 0F : ScrollContentSize - item.SizeHeight); + } + else + { + GroupInfo gInfo = GetGroupInfo(i); + posX = (IsHorizontal ? gInfo.GroupPosition + gInfo.ItemPosition[i - gInfo.StartIndex] : 0F); + posY = (IsHorizontal ? 0F : gInfo.GroupPosition + gInfo.ItemPosition[i - gInfo.StartIndex]); + } + } + else + { + posX = (IsHorizontal ? ItemPosition[i] : 0F); + posY = (IsHorizontal ? 0F : ItemPosition[i]); + } + + item.Position = new Position(posX, posY); + //Console.WriteLine("[NUI] ["+item+"]["+item.Index+"] :: ["+item.Position.X+", "+item.Position.Y+"] ==== \n"); + } + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override (float X, float Y) GetItemPosition(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + // Layouting Items in scrollPosition. + float pos = ItemPosition[colView.InternalItemSource.GetPosition(item)]; + + return (IsHorizontal ? (pos, 0.0F) : (0.0F, pos)); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override (float X, float Y) GetItemSize(object item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + // Layouting Items in scrollPosition. + float size = GetItemSize(colView.InternalItemSource.GetPosition(item)); + float view = (IsHorizontal ? colView.Size.Height : colView.Size.Width); + + return (IsHorizontal ? (size, view) : (view, size)); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void NotifyItemSizeChanged(RecyclerViewItem item) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + if (!IsInitialized || + (colView.SizingStrategy == ItemSizingStrategy.MeasureFirst && + item.Index != 0) || + (item.Index < 0)) + return; + + float PrevSize, CurrentSize; + if (item.Index == (colView.InternalItemSource.Count-1)) + { + PrevSize = ScrollContentSize - ItemPosition[item.Index]; + } + else + { + PrevSize = ItemPosition[item.Index + 1] - ItemPosition[item.Index]; + } + + CurrentSize = (IsHorizontal ? item.Size.Width : item.Size.Height); + + if (CurrentSize != PrevSize) + { + if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll) + ItemSize[item.Index] = CurrentSize; + else + StepCandidate = CurrentSize; + } + if (ItemSizeChanged == -1) ItemSizeChanged = item.Index; + else ItemSizeChanged = Math.Min(ItemSizeChanged, item.Index); + + //ScrollContentSize += Diff; UpdateOnce? + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override float CalculateLayoutOrientationSize() + { + //Console.WriteLine("[NUI] Calculate Layout ScrollContentSize {0}", ScrollContentSize); + return ScrollContentSize; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override float CalculateCandidateScrollPosition(float scrollPosition) + { + //Console.WriteLine("[NUI] Calculate Candidate ScrollContentSize {0}", ScrollContentSize); + return scrollPosition; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override View RequestNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + { + if (currentFocusedView == null) + throw new ArgumentNullException(nameof(currentFocusedView)); + + View nextFocusedView = null; + int targetSibling = -1; + + switch(direction) + { + case View.FocusDirection.Left : + { + targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder - 1 : targetSibling; + break; + } + case View.FocusDirection.Right : + { + targetSibling = IsHorizontal ? currentFocusedView.SiblingOrder + 1 : targetSibling; + break; + } + case View.FocusDirection.Up : + { + targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder - 1; + break; + } + case View.FocusDirection.Down : + { + targetSibling = IsHorizontal ? targetSibling : currentFocusedView.SiblingOrder + 1; + break; + } + } + + if(targetSibling > -1 && targetSibling < Container.Children.Count) + { + RecyclerViewItem candidate = Container.Children[targetSibling] as RecyclerViewItem; + if(candidate.Index >= 0 && candidate.Index < colView.InternalItemSource.Count) + { + nextFocusedView = candidate; + } + } + + return nextFocusedView; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected override (int start, int end) FindVisibleItems((float X, float Y) visibleArea) + { + int MaxIndex = colView.InternalItemSource.Count - 1 - (hasFooter ? 1 : 0); + int adds = 5; + int skipGroup = -2; + (int start, int end) found = (0, 0); + + // 1. Find the start index. + // Header is Showing + if (hasHeader && visibleArea.X <= headerSize) + { + found.start = 0; + } + else + { + if (isGrouped) + { + bool failed = true; + foreach(GroupInfo gInfo in groups) + { + skipGroup++; + // in the Group + if (gInfo.GroupPosition <= visibleArea.X && + gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.X) + { + for (int i = 0; i < gInfo.Count; i++) + { + // Reach last index of group. + if (i == (gInfo.Count - 1)) + { + found.start = gInfo.StartIndex + i - adds; + failed = false; + break; + + } + else if (gInfo.ItemPosition[i] <= visibleArea.X - gInfo.GroupPosition && + gInfo.ItemPosition[i + 1] >= visibleArea.X - gInfo.GroupPosition) + { + found.start = gInfo.StartIndex + i - adds; + failed = false; + break; + } + } + } + } + //footer only shows? + if (failed) + { + found.start = MaxIndex; + } + } + else + { + float visibleAreaX = visibleArea.X - (hasHeader ? headerSize : 0); + found.start = (Convert.ToInt32(Math.Abs(visibleAreaX / StepCandidate)) - adds); + } + + if (found.start < 0) found.start = 0; + } + + if (hasFooter && visibleArea.Y > ScrollContentSize - footerSize) + { + found.end = MaxIndex + 1; + } + else + { + if (isGrouped) + { + bool failed = true; + // can it be start from founded group...? + //foreach(GroupInfo gInfo in groups.Skip(skipGroup)) + foreach(GroupInfo gInfo in groups) + { + // in the Group + if (gInfo.GroupPosition <= visibleArea.Y && + gInfo.GroupPosition + gInfo.GroupSize >= visibleArea.Y) + { + for (int i = 0; i < gInfo.Count; i++) + { + if (i == (gInfo.Count - 1)) + { + //Sould be groupFooter! + found.end = gInfo.StartIndex + i + adds; + failed = false; + break; + + } + else if (gInfo.ItemPosition[i] <= visibleArea.Y - gInfo.GroupPosition && + gInfo.ItemPosition[i + 1] >= visibleArea.Y - gInfo.GroupPosition) + { + found.end = gInfo.StartIndex + i + adds; + failed = false; + break; + } + } + } + } + if (failed) found.end = MaxIndex; + } + else + { + float visibleAreaY = visibleArea.Y - (hasHeader ? headerSize : 0); + found.end = (Convert.ToInt32(Math.Abs(visibleAreaY / StepCandidate)) + adds); + if (hasHeader) found.end += 1; + } + if (found.end > (MaxIndex)) found.end = MaxIndex; + } + return found; + } + + private float GetItemSize(int index) + { + if (colView.SizingStrategy == ItemSizingStrategy.MeasureAll) + { + return ItemSize[index]; + } + else + { + if (index == 0 && hasHeader) + return headerSize; + if (index == colView.InternalItemSource.Count - 1 && hasFooter) + return footerSize; + return StepCandidate; + } + } + + private void UpdatePosition(int index) + { + bool IsGroup = (colView.InternalItemSource is IGroupableItemSource); + + if (index <= 0) return; + if (index >= colView.InternalItemSource.Count) + + if (IsGroup) + { + //IsGroupHeader = (colView.InternalItemSource as IGroupableItemSource).IsGroupHeader(index); + //IsGroupFooter = (colView.InternalItemSource as IGroupableItemSource).IsGroupFooter(index); + //Do Something + } + + ItemPosition[index] = ItemPosition[index-1] + GetItemSize(index-1); + } + + private RecyclerViewItem GetVisibleItem(int index) + { + foreach (RecyclerViewItem item in VisibleItems) + { + if (item.Index == index) return item; + } + return null; + } + + private GroupInfo GetGroupInfo(int index) + { + if (Visited != null) + { + if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) + return Visited; + } + if (hasHeader && index == 0) return null; + foreach (GroupInfo group in groups) + { + if (group.StartIndex <= index && group.StartIndex + group.Count > index) + { + Visited = group; + return group; + } + } + Visited = null; + return null; + } +/* + private object GetGroupParent(int index) + { + if (Visited != null) + { + if (Visited.StartIndex <= index && Visited.StartIndex + Visited.Count > index) + return Visited.GroupParent; + } + if (hasHeader && index == 0) return null; + foreach (GroupInfo group in groups) + { + if (group.StartIndex <= index && group.StartIndex + group.Count > index) + { + Visited = group; + return group.GroupParent; + } + } + Visited = null; + return null; + } +*/ + class GroupInfo + { + public object GroupParent; + public int StartIndex; + public int Count; + public float GroupSize; + public float GroupPosition; + //Items relative position from the GroupPosition + public List ItemPosition = new List(); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/RecyclerView.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/RecyclerView.cs index 6cd7779..548aea0 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/RecyclerView.cs +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/RecyclerView.cs @@ -1,4 +1,4 @@ -/* Copyright (c) 2020 Samsung Electronics Co., Ltd. +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,219 +14,263 @@ * */ using System; -using Tizen.NUI.BaseComponents; +using System.Linq; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; namespace Tizen.NUI.Components { /// - /// [Draft] This class provides a View that can recycle items to improve performance. + /// [Draft] This class provides a View that can layouting items in list and grid with high performance. /// - /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API. [EditorBrowsable(EditorBrowsableState.Never)] - public class RecyclerView : ScrollableBase + public abstract class RecyclerView : ScrollableBase, ICollectionChangedNotifier { - private RecycleAdapter adapter; - private RecycleLayoutManager layoutManager; - private int totalItemCount = 15; - private List notifications = new List(); - + /// + /// Base Constructor + /// + [EditorBrowsable(EditorBrowsableState.Never)] public RecyclerView() : base() { - Initialize(new RecycleAdapter(), new RecycleLayoutManager()); + Scrolling += OnScrolling; } /// - /// Default constructor. + /// Item's source data. /// - /// Recycle adapter of RecyclerView. - /// Recycle layoutManager of RecyclerView. - /// 8 - /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API [EditorBrowsable(EditorBrowsableState.Never)] - public RecyclerView(RecycleAdapter adapter, RecycleLayoutManager layoutManager) - { - Initialize(adapter, layoutManager); - } + public virtual IEnumerable ItemsSource { get; set; } - private void Initialize(RecycleAdapter adapter, RecycleLayoutManager layoutManager) - { - FocusGroup = true; - SetKeyboardNavigationSupport(true); - Scrolling += OnScrolling; + /// + /// DataTemplate for items. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual DataTemplate ItemTemplate { get; set; } - this.adapter = adapter; - this.adapter.OnDataChanged += OnAdapterDataChanged; + /// + /// Internal encapsulated items data source. + /// + internal IItemSource InternalItemSource { get; set;} - this.layoutManager = layoutManager; - this.layoutManager.Container = ContentContainer; - this.layoutManager.ItemSize = this.adapter.CreateRecycleItem().Size; - this.layoutManager.DataCount = this.adapter.Data.Count; + /// + /// RecycleCache of ViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected List RecycleCache { get; } = new List(); - InitializeItems(); - } + /// + /// Internal Items Layouter. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected ItemsLayouter InternalItemsLayouter {get; set; } - private void OnItemSizeChanged(object source, PropertyNotification.NotifyEventArgs args) - { - layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y); - } + /// + /// Max size of RecycleCache. Default is 50. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected int CacheMax { get; set; } = 50; - public int TotalItemCount + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void OnRelayout(Vector2 size, RelayoutContainer container) { - get - { - return totalItemCount; - } - set + //Console.WriteLine("[NUI] On ReLayout [{0} {0}]", size.X, size.Y); + base.OnRelayout(size, container); + if (InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null) { - totalItemCount = value; - InitializeItems(); + InternalItemsLayouter.Initialize(this); + InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y, true); } } - private void InitializeItems() + /// + /// Notify Dataset is Changed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyDataSetChanged() { - for (int i = Children.Count - 1; i > -1; i--) - { - Children[i].Unparent(); - notifications[i].Notified -= OnItemSizeChanged; - notifications.RemoveAt(i); - } - - for (int i = 0; i < totalItemCount; i++) + //Need to update view. + if (InternalItemsLayouter != null) { - RecycleItem item = adapter.CreateRecycleItem(); - item.DataIndex = i; - item.Name = "[" + i + "] recycle"; - - if (i < adapter.Data.Count) + InternalItemsLayouter.NotifyDataSetChanged(); + if (ScrollingDirection == Direction.Horizontal) { - adapter.BindData(item); + ContentContainer.SizeWidth = + InternalItemsLayouter.CalculateLayoutOrientationSize(); + } + else + { + ContentContainer.SizeHeight = + InternalItemsLayouter.CalculateLayoutOrientationSize(); } - Add(item); - - PropertyNotification noti = item.AddPropertyNotification("size", PropertyCondition.Step(0.1f)); - noti.Notified += OnItemSizeChanged; - notifications.Add(noti); } + } - layoutManager.Layout(0.0f); - - if (ScrollingDirection == Direction.Horizontal) - { - ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize(); - } - else + /// + /// Notify observable item is changed. + /// + /// Dataset source. + /// Changed item index. + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyItemChanged(IItemSource source, int startIndex) + { + if (InternalItemsLayouter != null) { - ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize(); + InternalItemsLayouter.NotifyItemChanged(source, startIndex); } } - - public new Direction ScrollingDirection + /// + /// Notify observable item is inserted in dataset. + /// + /// Dataset source. + /// Inserted item index. + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyItemInserted(IItemSource source, int startIndex) { - get + if (InternalItemsLayouter != null) { - return base.ScrollingDirection; - } - set - { - base.ScrollingDirection = value; - - if (ScrollingDirection == Direction.Horizontal) - { - ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize(); - } - else - { - ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize(); - } + InternalItemsLayouter.NotifyItemInserted(source, startIndex); } } /// - /// Recycler adpater. + /// Notify observable item is moved from fromPosition to ToPosition. /// - /// 8 - /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + /// Dataset source. + /// Previous item position. + /// Moved item position. [EditorBrowsable(EditorBrowsableState.Never)] - public RecycleAdapter Adapter + public void NotifyItemMoved(IItemSource source, int fromPosition, int toPosition) { - get + if (InternalItemsLayouter != null) { - return adapter; + InternalItemsLayouter.NotifyItemMoved(source, fromPosition, toPosition); } - set - { - if (adapter != null) - { - adapter.OnDataChanged -= OnAdapterDataChanged; - } + } - adapter = value; - adapter.OnDataChanged += OnAdapterDataChanged; - layoutManager.ItemSize = adapter.CreateRecycleItem().Size; - layoutManager.DataCount = adapter.Data.Count; - InitializeItems(); + /// + /// Notify range of observable items from start to end are changed. + /// + /// Dataset source. + /// Start index of changed items range. + /// End index of changed items range. + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyItemRangeChanged(IItemSource source, int start, int end) + { + if (InternalItemsLayouter != null) + { + InternalItemsLayouter.NotifyItemRangeChanged(source, start, end); } } /// - /// Recycler layoutManager. + /// Notify count range of observable count items are inserted in startIndex. /// - /// 8 - /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + /// Dataset source. + /// Start index of inserted items range. + /// The number of inserted items. [EditorBrowsable(EditorBrowsableState.Never)] - public RecycleLayoutManager LayoutManager + public void NotifyItemRangeInserted(IItemSource source, int startIndex, int count) { - get + if (InternalItemsLayouter != null) { - return layoutManager; + InternalItemsLayouter.NotifyItemRangeInserted(source, startIndex, count); } - set + } + + /// + /// Notify the count range of observable items from the startIndex are removed. + /// + /// Dataset source. + /// Start index of removed items range. + /// The number of removed items + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyItemRangeRemoved(IItemSource source, int startIndex, int count) + { + if (InternalItemsLayouter != null) { - layoutManager = value; - layoutManager.Container = ContentContainer; - layoutManager.ItemSize = adapter.CreateRecycleItem().Size; - layoutManager.DataCount = adapter.Data.Count; - InitializeItems(); + InternalItemsLayouter.NotifyItemRangeRemoved(source, startIndex, count); } } - private void OnScrolling(object source, ScrollEventArgs args) + /// + /// Notify the observable item in startIndex is removed. + /// + /// Dataset source. + /// Index of removed item. + [EditorBrowsable(EditorBrowsableState.Never)] + public void NotifyItemRemoved(IItemSource source, int startIndex) { - layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y); - List recycledItemList = layoutManager.Recycle(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y); - BindData(recycledItemList); + if (InternalItemsLayouter != null) + { + InternalItemsLayouter.NotifyItemRemoved(source, startIndex); + } } - private void OnAdapterDataChanged(object source, EventArgs args) + /// + /// Realize indexed item. + /// + /// Index position of realizing item + internal virtual RecyclerViewItem RealizeItem(int index) { - List changedData = new List(); + object context = InternalItemSource.GetItem(index); + // Check DataTemplate is Same! + if (ItemTemplate is DataTemplateSelector) + { + // Need to implements for caching of selector! + } + else + { + // pop item + RecyclerViewItem item = PopRecycleCache(ItemTemplate); + if (item != null) + { + DecorateItem(item, index, context); + return item; + } + } - foreach (RecycleItem item in Children) + object content = DataTemplateExtensions.CreateContent(ItemTemplate, context, (BindableObject)this) ?? throw new Exception("Template return null object."); + if (content is RecyclerViewItem) { - changedData.Add(item); + RecyclerViewItem item = (RecyclerViewItem)content; + ContentContainer.Add(item); + DecorateItem(item, index, context); + return item; + } + else + { + throw new Exception("Template content must be type of ViewItem"); } - BindData(changedData); } - private void BindData(List changedData) + /// + /// Unrealize indexed item. + /// + /// Target item for unrealizing + /// Allow recycle. default is true + internal virtual void UnrealizeItem(RecyclerViewItem item, bool recycle = true) { - foreach (RecycleItem item in changedData) + item.Index = -1; + item.ParentItemsView = null; + // Remove BindingContext null set for performance improving. + //item.BindingContext = null; + item.IsPressed = false; + item.IsSelected = false; + item.IsEnabled = true; + // Remove Update Style on default for performance improving. + //item.UpdateState(); + item.Relayout -= OnItemRelayout; + + if (!recycle || !PushRecycleCache(item)) { - if (item.DataIndex > -1 && item.DataIndex < adapter.Data.Count) - { - item.Show(); - item.Name = "[" + item.DataIndex + "]"; - adapter.BindData(item); - } - else - { - item.Hide(); - } + //ContentContainer.Remove(item); + Utility.Dispose(item); } } @@ -234,138 +278,116 @@ namespace Tizen.NUI.Components /// Adjust scrolling position by own scrolling rules. /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item) /// - /// Scroll position which is calculated by ScrollableBase + /// Scroll position which is calculated by ScrollableBase. /// Adjusted scroll destination - /// 8 - /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API [EditorBrowsable(EditorBrowsableState.Never)] protected override float AdjustTargetPositionOfScrollAnimation(float position) { // Destination is depending on implementation of layout manager. // Get destination from layout manager. - return layoutManager.CalculateCandidateScrollPosition(position); + return InternalItemsLayouter.CalculateCandidateScrollPosition(position); } - private View focusedView; - private int prevFocusedDataIndex = 0; + /// + /// Push the item into the recycle cache. this item will be reused in view update. + /// + /// Target item to push into recycle cache. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual bool PushRecycleCache(RecyclerViewItem item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + if (RecycleCache.Count >= CacheMax) return false; + if (item.Template == null) return false; + item.Hide(); + item.Index = -1; + RecycleCache.Add(item); + return true; + } - public override View GetNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + /// + /// Pop the item from the recycle cache. + /// + /// Template of wanted item. + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual RecyclerViewItem PopRecycleCache(DataTemplate Template) { - View nextFocusedView = null; + for (int i = 0; i < RecycleCache.Count; i++) + { + RecyclerViewItem item = RecycleCache[i]; + if (item.Template == Template) + { + RecycleCache.Remove(item); + item.Show(); + return item; + } + } + return null; + } - if (!focusedView) + /// + /// On scroll event callback. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected virtual void OnScrolling(object source, ScrollEventArgs args) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (!disposed && InternalItemsLayouter != null && ItemsSource != null && ItemTemplate != null) { - // If focusedView is null, find child which has previous data index - if (Children.Count > 0 && Adapter.Data.Count > 0) - { - for (int i = 0; i < Children.Count; i++) - { - RecycleItem item = Children[i] as RecycleItem; - if (item.DataIndex == prevFocusedDataIndex) - { - nextFocusedView = item; - break; - } - } - } + //Console.WriteLine("[NUI] On Scrolling! {0} => {1}", ScrollPosition.Y, args.Position.Y); + InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y); } - else + } + + /// + /// Dispose ItemsView and all children on it. + /// + /// Dispose type. + [EditorBrowsable(EditorBrowsableState.Never)] + protected override void Dispose(DisposeTypes type) + { + if (disposed) { - // If this is not first focus, request next focus to LayoutManager - if (LayoutManager != null) - { - nextFocusedView = LayoutManager.RequestNextFocusableView(currentFocusedView, direction, loopEnabled); - } + return; } - if (nextFocusedView != null) + if (type == DisposeTypes.Explicit) { - // Check next focused view is inside of visible area. - // If it is not, move scroll position to make it visible. - Position scrollPosition = ContentContainer.CurrentPosition; - float targetPosition = -(ScrollingDirection == Direction.Horizontal ? scrollPosition.X : scrollPosition.Y); - - float left = nextFocusedView.Position.X; - float right = nextFocusedView.Position.X + nextFocusedView.Size.Width; - float top = nextFocusedView.Position.Y; - float bottom = nextFocusedView.Position.Y + nextFocusedView.Size.Height; - - float visibleRectangleLeft = -scrollPosition.X; - float visibleRectangleRight = -scrollPosition.X + Size.Width; - float visibleRectangleTop = -scrollPosition.Y; - float visibleRectangleBottom = -scrollPosition.Y + Size.Height; - - if (ScrollingDirection == Direction.Horizontal) - { - if ((direction == View.FocusDirection.Left || direction == View.FocusDirection.Up) && left < visibleRectangleLeft) - { - targetPosition = left; - } - else if ((direction == View.FocusDirection.Right || direction == View.FocusDirection.Down) && right > visibleRectangleRight) - { - targetPosition = right - Size.Width; - } - } - else + disposed = true; + // call the clear! + if (RecycleCache != null) { - if ((direction == View.FocusDirection.Up || direction == View.FocusDirection.Left) && top < visibleRectangleTop) - { - targetPosition = top; - } - else if ((direction == View.FocusDirection.Down || direction == View.FocusDirection.Right) && bottom > visibleRectangleBottom) + foreach (RecyclerViewItem item in RecycleCache) { - targetPosition = bottom - Size.Height; + //ContentContainer.Remove(item); + Utility.Dispose(item); } + RecycleCache.Clear(); } - - focusedView = nextFocusedView; - if ((nextFocusedView as RecycleItem) != null) - { - prevFocusedDataIndex = (nextFocusedView as RecycleItem).DataIndex; - } - - ScrollTo(targetPosition, true); + InternalItemsLayouter.Clear(); + InternalItemsLayouter = null; + ItemsSource = null; + ItemTemplate = null; + if (InternalItemSource != null) InternalItemSource.Dispose(); + // } - else - { - // If nextView is null, it means that we should move focus to outside of Control. - // Return FocusableView depending on direction. - switch (direction) - { - case View.FocusDirection.Left: - { - nextFocusedView = LeftFocusableView; - break; - } - case View.FocusDirection.Right: - { - nextFocusedView = RightFocusableView; - break; - } - case View.FocusDirection.Up: - { - nextFocusedView = UpFocusableView; - break; - } - case View.FocusDirection.Down: - { - nextFocusedView = DownFocusableView; - break; - } - } - if (nextFocusedView) - { - focusedView = null; - } - else - { - //If FocusableView doesn't exist, not move focus. - nextFocusedView = focusedView; - } - } + base.Dispose(type); + } - return nextFocusedView; + private void OnItemRelayout(object sender, EventArgs e) + { + //FIXME: we need to skip the first relayout and only call size changed when real size change happen. + //InternalItemsLayouter.NotifyItemSizeChanged((sender as ViewItem)); + //InternalItemsLayouter.RequestLayout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y); + } + + private void DecorateItem(RecyclerViewItem item, int index, object context) + { + item.Index = index; + item.ParentItemsView = this; + item.Template = (ItemTemplate as DataTemplateSelector)?.SelectDataTemplate(InternalItemSource.GetItem(index), this) ?? ItemTemplate; + item.BindingContext = context; + item.Relayout += OnItemRelayout; } } } diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionChangedEventArgs.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionChangedEventArgs.cs new file mode 100644 index 0000000..6041b22 --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionChangedEventArgs.cs @@ -0,0 +1,56 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.ComponentModel; +using System.Collections.Generic; + +namespace Tizen.NUI.Components +{ + /// + /// Selection changed event. this might be deprecated. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class SelectionChangedEventArgs : EventArgs + { + static readonly IReadOnlyList selectEmpty = new List(0); + + /// + /// Previous selection list. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IReadOnlyList PreviousSelection { get; } + + /// + /// Current selection list. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IReadOnlyList CurrentSelection { get; } + + + internal SelectionChangedEventArgs(object previousSelection, object currentSelection) + { + PreviousSelection = previousSelection != null ? new List(1) { previousSelection } : selectEmpty; + CurrentSelection = currentSelection != null ? new List(1) { currentSelection } : selectEmpty; + } + + internal SelectionChangedEventArgs(IList previousSelection, IList currentSelection) + { + PreviousSelection = new List(previousSelection ?? throw new ArgumentNullException(nameof(previousSelection))); + CurrentSelection = new List(currentSelection ?? throw new ArgumentNullException(nameof(currentSelection))); + } + } +} diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionList.cs b/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionList.cs new file mode 100644 index 0000000..c328fca --- /dev/null +++ b/src/Tizen.NUI.Components/Controls/RecyclerView/SelectionList.cs @@ -0,0 +1,157 @@ +/* Copyright (c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace Tizen.NUI.Components +{ + // Used by the CollectionView to keep track of (and respond to changes in) the SelectedItems property + internal class SelectionList : IList + { + static readonly IList selectEmpty = new List(0); + readonly CollectionView ColView; + readonly IList internalList; + IList shadowList; + bool externalChange; + + public SelectionList(CollectionView colView, IList items = null) + { + ColView = colView ?? throw new ArgumentNullException(nameof(colView)); + internalList = items ?? new List(); + shadowList = Copy(); + + if (items is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + } + } + + public object this[int index] { get => internalList[index]; set => internalList[index] = value; } + + public int Count => internalList.Count; + + public bool IsReadOnly => false; + + public void Add(object item) + { + externalChange = true; + internalList.Add(item); + externalChange = false; + + ColView.SelectedItemsPropertyChanged(shadowList, internalList); + shadowList.Add(item); + } + + public void Clear() + { + externalChange = true; + internalList.Clear(); + externalChange = false; + + ColView.SelectedItemsPropertyChanged(shadowList, selectEmpty); + shadowList.Clear(); + } + + public bool Contains(object item) + { + return internalList.Contains(item); + } + + public void CopyTo(object[] array, int arrayIndex) + { + internalList.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return internalList.GetEnumerator(); + } + + public int IndexOf(object item) + { + return internalList.IndexOf(item); + } + + public void Insert(int index, object item) + { + externalChange = true; + internalList.Insert(index, item); + externalChange = false; + + ColView.SelectedItemsPropertyChanged(shadowList, internalList); + shadowList.Insert(index, item); + } + + public bool Remove(object item) + { + externalChange = true; + var removed = internalList.Remove(item); + externalChange = false; + + if (removed) + { + ColView.SelectedItemsPropertyChanged(shadowList, internalList); + shadowList.Remove(item); + } + + return removed; + } + + public void RemoveAt(int index) + { + externalChange = true; + internalList.RemoveAt(index); + externalChange = false; + + ColView.SelectedItemsPropertyChanged(shadowList, internalList); + shadowList.RemoveAt(index); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return internalList.GetEnumerator(); + } + + List Copy() + { + var items = new List(); + for (int n = 0; n < internalList.Count; n++) + { + items.Add(internalList[n]); + } + + return items; + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (externalChange) + { + // If this change was initiated by a renderer or direct manipulation of ColllectionView.SelectedItems, + // we don't need to send a selection change notification + return; + } + + // This change is coming from a bound viewmodel property + // Emit a selection change notification, then bring the shadow copy up-to-date + ColView.SelectedItemsPropertyChanged(shadowList, internalList); + shadowList = Copy(); + } + } +} diff --git a/src/Tizen.NUI.Components/Style/DefaultGridItemStyle.cs b/src/Tizen.NUI.Components/Style/DefaultGridItemStyle.cs new file mode 100644 index 0000000..1149b28 --- /dev/null +++ b/src/Tizen.NUI.Components/Style/DefaultGridItemStyle.cs @@ -0,0 +1,88 @@ +/* + * Copyright(c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultGridItemStyle is a class which saves DefaultLinearItem's ux data. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultGridItemStyle : RecyclerViewItemStyle + { + + static DefaultGridItemStyle() { } + + /// + /// Creates a new instance of a DefaultGridItemStyle. + /// + /// 8 + public DefaultGridItemStyle() : base() + { + } + + /// + /// Creates a new instance of a DefaultGridItemStyle with style. + /// + /// Create DefaultGridItemStyle by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultGridItemStyle(DefaultGridItemStyle style) : base(style) + { + } + + + /// + /// Label Text's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabelStyle Caption { get; set; } = new TextLabelStyle(); + + /// + /// Icon's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ImageViewStyle Image { get; set; } = new ImageViewStyle(); + + /// + /// Extra's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Badge { get; set; } = new ViewStyle(); + + /// + /// Style's clone function. + /// + /// The style that need to copy. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void CopyFrom(BindableObject bindableObject) + { + base.CopyFrom(bindableObject); + + if (bindableObject is DefaultGridItemStyle RecyclerViewItemStyle) + { + Caption.CopyFrom(RecyclerViewItemStyle.Caption); + Image.CopyFrom(RecyclerViewItemStyle.Image); + Badge.CopyFrom(RecyclerViewItemStyle.Badge); + //Border.CopyFrom(RecyclerViewItemStyle.Border); + } + } + } +} diff --git a/src/Tizen.NUI.Components/Style/DefaultLinearItemStyle.cs b/src/Tizen.NUI.Components/Style/DefaultLinearItemStyle.cs new file mode 100644 index 0000000..f990a7c --- /dev/null +++ b/src/Tizen.NUI.Components/Style/DefaultLinearItemStyle.cs @@ -0,0 +1,99 @@ +/* + * Copyright(c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultLinearItemStyle is a class which saves DefaultLinearItem's ux data. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultLinearItemStyle : RecyclerViewItemStyle + { + static DefaultLinearItemStyle() { } + + /// + /// Creates a new instance of a DefaultLinearItemStyle. + /// + /// 8 + public DefaultLinearItemStyle() : base() + { + } + + /// + /// Creates a new instance of a DefaultLinearItemStyle with style. + /// + /// Create DefaultLinearItemStyle by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultLinearItemStyle(DefaultLinearItemStyle style) : base(style) + { + } + + /// + /// Label Text's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabelStyle Label { get; set; } = new TextLabelStyle(); + + /// + /// Sublabel Text's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabelStyle SubLabel { get; set; } = new TextLabelStyle(); + + /// + /// Icon's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Icon { get; set; } = new ViewStyle(); + + /// + /// Extra's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Extra { get; set; } = new ViewStyle(); + + /// + /// Seperator's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Seperator { get; set; } = new ViewStyle(); + + /// + /// Style's clone function. + /// + /// The style that need to copy. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void CopyFrom(BindableObject bindableObject) + { + base.CopyFrom(bindableObject); + + if (bindableObject is DefaultLinearItemStyle RecyclerViewItemStyle) + { + Label.CopyFrom(RecyclerViewItemStyle.Label); + SubLabel.CopyFrom(RecyclerViewItemStyle.SubLabel); + Icon.CopyFrom(RecyclerViewItemStyle.Icon); + Extra.CopyFrom(RecyclerViewItemStyle.Extra); + Seperator.CopyFrom(RecyclerViewItemStyle.Seperator); + } + } + } +} diff --git a/src/Tizen.NUI.Components/Style/DefaultTitleItemStyle.cs b/src/Tizen.NUI.Components/Style/DefaultTitleItemStyle.cs new file mode 100644 index 0000000..d68c2b9 --- /dev/null +++ b/src/Tizen.NUI.Components/Style/DefaultTitleItemStyle.cs @@ -0,0 +1,85 @@ +/* + * Copyright(c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; + +namespace Tizen.NUI.Components +{ + /// + /// DefaultTitleItemStyle is a class which saves DefaultLinearItem's ux data. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class DefaultTitleItemStyle : RecyclerViewItemStyle + { + static DefaultTitleItemStyle() { } + + /// + /// Creates a new instance of a DefaultTitleItemStyle. + /// + /// 8 + public DefaultTitleItemStyle() : base() + { + } + + /// + /// Creates a new instance of a DefaultTitleItemStyle with style. + /// + /// Create DefaultTitleItemStyle by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public DefaultTitleItemStyle(DefaultTitleItemStyle style) : base(style) + { + } + + /// + /// Label Text's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TextLabelStyle Label { get; set; } = new TextLabelStyle(); + + /// + /// Icon's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Icon { get; set; } = new ViewStyle(); + + /// + /// Seperator's style. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public ViewStyle Seperator { get; set; } = new ViewStyle(); + + /// + /// Style's clone function. + /// + /// The style that need to copy. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void CopyFrom(BindableObject bindableObject) + { + base.CopyFrom(bindableObject); + + if (bindableObject is DefaultTitleItemStyle RecyclerViewItemStyle) + { + Label.CopyFrom(RecyclerViewItemStyle.Label); + Icon.CopyFrom(RecyclerViewItemStyle.Icon); + Seperator.CopyFrom(RecyclerViewItemStyle.Seperator); + } + } + } +} diff --git a/src/Tizen.NUI.Components/Style/RecyclerViewItemStyle.cs b/src/Tizen.NUI.Components/Style/RecyclerViewItemStyle.cs new file mode 100644 index 0000000..8fa6311 --- /dev/null +++ b/src/Tizen.NUI.Components/Style/RecyclerViewItemStyle.cs @@ -0,0 +1,138 @@ +/* + * Copyright(c) 2021 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +using System.ComponentModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; +using Tizen.NUI.Components.Extension; + +namespace Tizen.NUI.Components +{ + /// + /// RecyclerViewItemStyle is a class which saves RecyclerViewItem's ux data. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class RecyclerViewItemStyle : ControlStyle + { + /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsSelectableProperty = BindableProperty.Create(nameof(IsSelectable), typeof(bool?), typeof(RecyclerViewItemStyle), null, propertyChanged: (bindable, oldValue, newValue) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + RecyclerViewItemStyle.isSelectable = (bool?)newValue; + }, + defaultValueCreator: (bindable) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + return RecyclerViewItemStyle.isSelectable; + }); + /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool?), typeof(RecyclerViewItemStyle), null, propertyChanged: (bindable, oldValue, newValue) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + RecyclerViewItemStyle.isSelected = (bool?)newValue; + }, + defaultValueCreator: (bindable) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + return RecyclerViewItemStyle.isSelected; + }); + /// This will be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API. + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool?), typeof(RecyclerViewItemStyle), null, propertyChanged: (bindable, oldValue, newValue) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + RecyclerViewItemStyle.isEnabled = (bool?)newValue; + }, + defaultValueCreator: (bindable) => + { + var RecyclerViewItemStyle = (RecyclerViewItemStyle)bindable; + return RecyclerViewItemStyle.isEnabled; + }); + + private bool? isSelectable; + private bool? isSelected; + private bool? isEnabled; + + static RecyclerViewItemStyle() { } + + /// + /// Creates a new instance of a RecyclerViewItemStyle. + /// + /// 8 + public RecyclerViewItemStyle() : base() + { + } + + /// + /// Creates a new instance of a RecyclerViewItemStyle with style. + /// + /// Create RecyclerViewItemStyle by style customized by user. + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerViewItemStyle(RecyclerViewItemStyle style) : base(style) + { + } + + /// + /// Flag to decide RecyclerViewItem can be selected or not. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool? IsSelectable + { + get => (bool?)GetValue(IsSelectableProperty); + set => SetValue(IsSelectableProperty, value); + } + + /// + /// Flag to decide selected state in RecyclerViewItem. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool? IsSelected + { + get => (bool?)GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + /// + /// Flag to decide RecyclerViewItem can be selected or not. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool? IsEnabled + { + get => (bool?)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// + /// Style's clone function. + /// + /// The style that need to copy. + [EditorBrowsable(EditorBrowsableState.Never)] + public override void CopyFrom(BindableObject bindableObject) + { + base.CopyFrom(bindableObject); + + /* + if (bindableObject is RecyclerViewItemStyle RecyclerViewItemStyle) + { + // + } + */ + } + } +} diff --git a/src/Tizen.NUI.Components/Theme/DefaultTheme.cs b/src/Tizen.NUI.Components/Theme/DefaultTheme.cs index d8dcc42..9910996 100644 --- a/src/Tizen.NUI.Components/Theme/DefaultTheme.cs +++ b/src/Tizen.NUI.Components/Theme/DefaultTheme.cs @@ -97,6 +97,11 @@ namespace Tizen.NUI.Components ["PaginationIndicatorImageUrlSelected"] = FrameworkInformation.ResourcePath + "nui_component_default_pagination_focus_dot.png", ["ScrollbarTrackColor"] = new Color(1, 1, 1, 0.15f), ["ScrollbarThumbColor"] = new Color(0.6f, 0.6f, 0.6f, 1.0f), + ["RecyclerViewItemBackgroundColorNormal"] = new Color(1, 1, 1, 1), + ["RecyclerViewItemBackgroundColorPressed"] = new Color(0.85f, 0.85f, 0.85f, 1), + ["RecyclerViewItemBackgroundColorDisabled"] = new Color(0.70f, 0.70f, 0.70f, 1), + ["RecyclerViewItemBackgroundColorSelected"] = new Color(0.701f, 0.898f, 0.937f, 1), + ["TitleBackgroundColorNormal"] = new Color(0.78f, 0.78f, 0.78f, 1), }; public Theme Create() => Create(null); @@ -372,6 +377,90 @@ namespace Tizen.NUI.Components TrackPadding = 4 }); + theme.AddStyleWithoutClone("Tizen.NUI.Components.RecyclerViewItem", new RecyclerViewItemStyle() + { + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["RecyclerViewItemBackgroundColorNormal"], + Pressed = (Color)theme.Resources["RecyclerViewItemBackgroundColorPressed"], + Disabled = (Color)theme.Resources["RecyclerViewItemBackgroundColorDisabled"], + Selected = (Color)theme.Resources["RecyclerViewItemBackgroundColorSelected"], + }, + }); + + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultLinearItem", new DefaultLinearItemStyle() + { + SizeHeight = 130, + Padding = new Extents(20, 20, 5, 5), + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["RecyclerViewItemBackgroundColorNormal"], + Pressed = (Color)theme.Resources["RecyclerViewItemBackgroundColorPressed"], + Disabled = (Color)theme.Resources["RecyclerViewItemBackgroundColorDisabled"], + Selected = (Color)theme.Resources["RecyclerViewItemBackgroundColorSelected"], + }, + Label = new TextLabelStyle() + { + PointSize = 10, + Ellipsis = true, + }, + SubLabel = new TextLabelStyle() + { + PointSize = 6, + Ellipsis = true, + }, + Icon = new ViewStyle() + { + Margin = new Extents(0, 20, 0, 0) + }, + Extra = new ViewStyle() + { + Margin = new Extents(20, 0, 0, 0) + }, + Seperator = new ViewStyle() + { + Margin = new Extents(5, 5, 0, 0), + BackgroundColor = new Color(0.78f, 0.78f, 0.78f, 1), + }, + }); + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultGridItem", new DefaultGridItemStyle() + { + Padding = new Extents(5, 5, 5, 5), + Caption = new TextLabelStyle() + { + PointSize = 9, + Ellipsis = true, + }, + Badge = new ViewStyle() + { + Margin = new Extents(5, 5, 5, 5), + }, + }); + + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultTitleItem", new DefaultTitleItemStyle() + { + SizeHeight = 90, + Padding = new Extents(10, 10, 5, 5), + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["TitleBackgroundColorNormal"], + }, + Label = new TextLabelStyle() + { + PointSize = 10, + Ellipsis = true, + }, + Icon = new ViewStyle() + { + Margin = new Extents(10, 0, 0, 0) + }, + Seperator = new ViewStyle() + { + Margin = new Extents(0, 0, 0, 0), + BackgroundColor = new Color(0.85f, 0.85f, 0.85f, 1), + }, + }); + return theme; } } diff --git a/src/Tizen.NUI.Components/Theme/DefaultThemeMobile.cs b/src/Tizen.NUI.Components/Theme/DefaultThemeMobile.cs index 4a83806..ddc52c1 100644 --- a/src/Tizen.NUI.Components/Theme/DefaultThemeMobile.cs +++ b/src/Tizen.NUI.Components/Theme/DefaultThemeMobile.cs @@ -92,6 +92,11 @@ namespace Tizen.NUI.Components ["PaginationIndicatorImageUrlSelected"] = FrameworkInformation.ResourcePath + "nui_component_default_pagination_focus_dot.png", ["ScrollbarTrackColor"] = new Color(1, 1, 1, 0.15f), ["ScrollbarThumbColor"] = new Color(0.6f, 0.6f, 0.6f, 1.0f), + ["RecyclerViewItemBackgroundColorNormal"] = new Color(1, 1, 1, 1), + ["RecyclerViewItemBackgroundColorPressed"] = new Color(0.85f, 0.85f, 0.85f, 1), + ["RecyclerViewItemBackgroundColorDisabled"] = new Color(0.70f, 0.70f, 0.70f, 1), + ["RecyclerViewItemBackgroundColorSelected"] = new Color(0.701f, 0.898f, 0.937f, 1), + ["TitleBackgroundColorNormal"] = new Color(0.78f, 0.78f, 0.78f, 1), }; public Theme Create() => Create(null); @@ -366,6 +371,90 @@ namespace Tizen.NUI.Components TrackPadding = 4 }); + theme.AddStyleWithoutClone("Tizen.NUI.Components.RecyclerViewItem", new RecyclerViewItemStyle() + { + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["RecyclerViewItemBackgroundColorNormal"], + Pressed = (Color)theme.Resources["RecyclerViewItemBackgroundColorPressed"], + Disabled = (Color)theme.Resources["RecyclerViewItemBackgroundColorDisabled"], + Selected = (Color)theme.Resources["RecyclerViewItemBackgroundColorSelected"], + }, + }); + + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultLinearItem", new DefaultLinearItemStyle() + { + SizeHeight = 160, + Padding = new Extents(10, 10, 20, 20), + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["RecyclerViewItemBackgroundColorNormal"], + Pressed = (Color)theme.Resources["RecyclerViewItemBackgroundColorPressed"], + Disabled = (Color)theme.Resources["RecyclerViewItemBackgroundColorDisabled"], + Selected = (Color)theme.Resources["RecyclerViewItemBackgroundColorSelected"], + }, + Label = new TextLabelStyle() + { + PointSize = 20, + Ellipsis = true, + }, + SubLabel = new TextLabelStyle() + { + PointSize = 12, + Ellipsis = true, + }, + Icon = new ViewStyle() + { + Margin = new Extents(0, 10, 0, 0) + }, + Extra = new ViewStyle() + { + Margin = new Extents(10, 0, 0, 0) + }, + Seperator = new ViewStyle() + { + Margin = new Extents(5, 5, 0, 0), + BackgroundColor = new Color(0.78f, 0.78f, 0.78f, 1), + }, + }); + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultGridItem", new DefaultGridItemStyle() + { + Padding = new Extents(5, 5, 5, 5), + Caption = new TextLabelStyle() + { + PointSize = 9, + Ellipsis = true, + }, + Badge = new ViewStyle() + { + Margin = new Extents(5, 5, 5, 5), + }, + }); + + theme.AddStyleWithoutClone("Tizen.NUI.Components.DefaultTitleItem", new DefaultTitleItemStyle() + { + SizeHeight = 50, + Padding = new Extents(10, 10, 5, 5), + BackgroundColor = new Selector() + { + Normal = (Color)theme.Resources["TitleBackgroundColorNormal"], + }, + Label = new TextLabelStyle() + { + PointSize = 15, + Ellipsis = true, + }, + Icon = new ViewStyle() + { + Margin = new Extents(10, 0, 0, 0) + }, + Seperator = new ViewStyle() + { + Margin = new Extents(0, 0, 0, 0), + BackgroundColor = new Color(0.85f, 0.85f, 0.85f, 1), + }, + }); + return theme; } } diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/GridRecycleLayoutManager.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/GridRecycleLayoutManager.cs similarity index 99% rename from src/Tizen.NUI.Components/Controls/RecyclerView/GridRecycleLayoutManager.cs rename to src/Tizen.NUI.Wearable/src/public/RecyclerView/GridRecycleLayoutManager.cs index 9583c82..85224e1 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/GridRecycleLayoutManager.cs +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/GridRecycleLayoutManager.cs @@ -15,10 +15,11 @@ */ using System; using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; using System.Collections.Generic; using System.ComponentModel; -namespace Tizen.NUI.Components +namespace Tizen.NUI.Wearable { /// /// [Draft] This class implements a grid box layout. diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/LinearRecycleLayoutManager.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/LinearRecycleLayoutManager.cs similarity index 99% rename from src/Tizen.NUI.Components/Controls/RecyclerView/LinearRecycleLayoutManager.cs rename to src/Tizen.NUI.Wearable/src/public/RecyclerView/LinearRecycleLayoutManager.cs index 07107a2..55860c5 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/LinearRecycleLayoutManager.cs +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/LinearRecycleLayoutManager.cs @@ -15,10 +15,11 @@ */ using System; using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; using System.Collections.Generic; using System.ComponentModel; -namespace Tizen.NUI.Components +namespace Tizen.NUI.Wearable { /// /// [Draft] This class implements a linear box layout. diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleAdapter.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleAdapter.cs similarity index 98% rename from src/Tizen.NUI.Components/Controls/RecyclerView/RecycleAdapter.cs rename to src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleAdapter.cs index f092867..eddf2d9 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleAdapter.cs +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleAdapter.cs @@ -16,8 +16,9 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using Tizen.NUI.Components; -namespace Tizen.NUI.Components +namespace Tizen.NUI.Wearable { /// /// [Draft] Defalt adapter for RecyclerView. diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleItem.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleItem.cs similarity index 96% rename from src/Tizen.NUI.Components/Controls/RecyclerView/RecycleItem.cs rename to src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleItem.cs index 8533562..9037734 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleItem.cs +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleItem.cs @@ -14,8 +14,9 @@ * */ using System.ComponentModel; +using Tizen.NUI.Components; -namespace Tizen.NUI.Components +namespace Tizen.NUI.Wearable { /// /// [Draft] This class provides a basic item for RecyclerView. diff --git a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleLayoutManager.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleLayoutManager.cs similarity index 99% rename from src/Tizen.NUI.Components/Controls/RecyclerView/RecycleLayoutManager.cs rename to src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleLayoutManager.cs index 6105314..bf6c589 100755 --- a/src/Tizen.NUI.Components/Controls/RecyclerView/RecycleLayoutManager.cs +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecycleLayoutManager.cs @@ -14,10 +14,11 @@ * */ using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; using System.Collections.Generic; using System.ComponentModel; -namespace Tizen.NUI.Components +namespace Tizen.NUI.Wearable { /// /// [Draft] Defalt layout manager for RecyclerView. diff --git a/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecyclerView.cs b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecyclerView.cs new file mode 100755 index 0000000..903d9d3 --- /dev/null +++ b/src/Tizen.NUI.Wearable/src/public/RecyclerView/RecyclerView.cs @@ -0,0 +1,372 @@ +/* Copyright (c) 2020 Samsung Electronics Co., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +using System; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Tizen.NUI.Wearable +{ + /// + /// [Draft] This class provides a View that can recycle items to improve performance. + /// + /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API. + [EditorBrowsable(EditorBrowsableState.Never)] + public class RecyclerView : ScrollableBase + { + private RecycleAdapter adapter; + private RecycleLayoutManager layoutManager; + private int totalItemCount = 15; + private List notifications = new List(); + + public RecyclerView() : base() + { + Initialize(new RecycleAdapter(), new RecycleLayoutManager()); + } + + /// + /// Default constructor. + /// + /// Recycle adapter of RecyclerView. + /// Recycle layoutManager of RecyclerView. + /// 8 + /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + [EditorBrowsable(EditorBrowsableState.Never)] + public RecyclerView(RecycleAdapter adapter, RecycleLayoutManager layoutManager) + { + Initialize(adapter, layoutManager); + } + + private void Initialize(RecycleAdapter adapter, RecycleLayoutManager layoutManager) + { + FocusGroup = true; + SetKeyboardNavigationSupport(true); + Scrolling += OnScrolling; + + this.adapter = adapter; + this.adapter.OnDataChanged += OnAdapterDataChanged; + + this.layoutManager = layoutManager; + this.layoutManager.Container = ContentContainer; + this.layoutManager.ItemSize = this.adapter.CreateRecycleItem().Size; + this.layoutManager.DataCount = this.adapter.Data.Count; + + InitializeItems(); + } + + private void OnItemSizeChanged(object source, PropertyNotification.NotifyEventArgs args) + { + layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? ContentContainer.CurrentPosition.X : ContentContainer.CurrentPosition.Y); + } + + public int TotalItemCount + { + get + { + return totalItemCount; + } + set + { + totalItemCount = value; + InitializeItems(); + } + } + + private void InitializeItems() + { + for (int i = Children.Count - 1; i > -1; i--) + { + Children[i].Unparent(); + notifications[i].Notified -= OnItemSizeChanged; + notifications.RemoveAt(i); + } + + for (int i = 0; i < totalItemCount; i++) + { + RecycleItem item = adapter.CreateRecycleItem(); + item.DataIndex = i; + item.Name = "[" + i + "] recycle"; + + if (i < adapter.Data.Count) + { + adapter.BindData(item); + } + Add(item); + + PropertyNotification noti = item.AddPropertyNotification("size", PropertyCondition.Step(0.1f)); + noti.Notified += OnItemSizeChanged; + notifications.Add(noti); + } + + layoutManager.Layout(0.0f); + + if (ScrollingDirection == Direction.Horizontal) + { + ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize(); + } + else + { + ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize(); + } + } + + + public new Direction ScrollingDirection + { + get + { + return base.ScrollingDirection; + } + set + { + base.ScrollingDirection = value; + + if (ScrollingDirection == Direction.Horizontal) + { + ContentContainer.SizeWidth = layoutManager.CalculateLayoutOrientationSize(); + } + else + { + ContentContainer.SizeHeight = layoutManager.CalculateLayoutOrientationSize(); + } + } + } + + /// + /// Recycler adpater. + /// + /// 8 + /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + [EditorBrowsable(EditorBrowsableState.Never)] + public RecycleAdapter Adapter + { + get + { + return adapter; + } + set + { + if (adapter != null) + { + adapter.OnDataChanged -= OnAdapterDataChanged; + } + + adapter = value; + adapter.OnDataChanged += OnAdapterDataChanged; + layoutManager.ItemSize = adapter.CreateRecycleItem().Size; + layoutManager.DataCount = adapter.Data.Count; + InitializeItems(); + } + } + + /// + /// Recycler layoutManager. + /// + /// 8 + /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + [EditorBrowsable(EditorBrowsableState.Never)] + public RecycleLayoutManager LayoutManager + { + get + { + return layoutManager; + } + set + { + layoutManager = value; + layoutManager.Container = ContentContainer; + layoutManager.ItemSize = adapter.CreateRecycleItem().Size; + layoutManager.DataCount = adapter.Data.Count; + InitializeItems(); + } + } + + private void OnScrolling(object source, ScrollEventArgs args) + { + layoutManager.Layout(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y); + List recycledItemList = layoutManager.Recycle(ScrollingDirection == Direction.Horizontal ? args.Position.X : args.Position.Y); + BindData(recycledItemList); + } + + private void OnAdapterDataChanged(object source, EventArgs args) + { + List changedData = new List(); + + foreach (RecycleItem item in Children) + { + changedData.Add(item); + } + + BindData(changedData); + } + + private void BindData(List changedData) + { + foreach (RecycleItem item in changedData) + { + if (item.DataIndex > -1 && item.DataIndex < adapter.Data.Count) + { + item.Show(); + item.Name = "[" + item.DataIndex + "]"; + adapter.BindData(item); + } + else + { + item.Hide(); + } + } + } + + /// + /// Adjust scrolling position by own scrolling rules. + /// Override this function when developer wants to change destination of flicking.(e.g. always snap to center of item) + /// + /// Scroll position which is calculated by ScrollableBase + /// Adjusted scroll destination + /// 8 + /// This may be public opened in tizen_6.0 after ACR done. Before ACR, need to be hidden as inhouse API + [EditorBrowsable(EditorBrowsableState.Never)] + protected override float AdjustTargetPositionOfScrollAnimation(float position) + { + // Destination is depending on implementation of layout manager. + // Get destination from layout manager. + return layoutManager.CalculateCandidateScrollPosition(position); + } + + private View focusedView; + private int prevFocusedDataIndex = 0; + + public override View GetNextFocusableView(View currentFocusedView, View.FocusDirection direction, bool loopEnabled) + { + View nextFocusedView = null; + + if (!focusedView) + { + // If focusedView is null, find child which has previous data index + if (Children.Count > 0 && Adapter.Data.Count > 0) + { + for (int i = 0; i < Children.Count; i++) + { + RecycleItem item = Children[i] as RecycleItem; + if (item.DataIndex == prevFocusedDataIndex) + { + nextFocusedView = item; + break; + } + } + } + } + else + { + // If this is not first focus, request next focus to LayoutManager + if (LayoutManager != null) + { + nextFocusedView = LayoutManager.RequestNextFocusableView(currentFocusedView, direction, loopEnabled); + } + } + + if (nextFocusedView != null) + { + // Check next focused view is inside of visible area. + // If it is not, move scroll position to make it visible. + Position scrollPosition = ContentContainer.CurrentPosition; + float targetPosition = -(ScrollingDirection == Direction.Horizontal ? scrollPosition.X : scrollPosition.Y); + + float left = nextFocusedView.Position.X; + float right = nextFocusedView.Position.X + nextFocusedView.Size.Width; + float top = nextFocusedView.Position.Y; + float bottom = nextFocusedView.Position.Y + nextFocusedView.Size.Height; + + float visibleRectangleLeft = -scrollPosition.X; + float visibleRectangleRight = -scrollPosition.X + Size.Width; + float visibleRectangleTop = -scrollPosition.Y; + float visibleRectangleBottom = -scrollPosition.Y + Size.Height; + + if (ScrollingDirection == Direction.Horizontal) + { + if ((direction == View.FocusDirection.Left || direction == View.FocusDirection.Up) && left < visibleRectangleLeft) + { + targetPosition = left; + } + else if ((direction == View.FocusDirection.Right || direction == View.FocusDirection.Down) && right > visibleRectangleRight) + { + targetPosition = right - Size.Width; + } + } + else + { + if ((direction == View.FocusDirection.Up || direction == View.FocusDirection.Left) && top < visibleRectangleTop) + { + targetPosition = top; + } + else if ((direction == View.FocusDirection.Down || direction == View.FocusDirection.Right) && bottom > visibleRectangleBottom) + { + targetPosition = bottom - Size.Height; + } + } + + focusedView = nextFocusedView; + if ((nextFocusedView as RecycleItem) != null) + { + prevFocusedDataIndex = (nextFocusedView as RecycleItem).DataIndex; + } + + ScrollTo(targetPosition, true); + } + else + { + // If nextView is null, it means that we should move focus to outside of Control. + // Return FocusableView depending on direction. + switch (direction) + { + case View.FocusDirection.Left: + { + nextFocusedView = LeftFocusableView; + break; + } + case View.FocusDirection.Right: + { + nextFocusedView = RightFocusableView; + break; + } + case View.FocusDirection.Up: + { + nextFocusedView = UpFocusableView; + break; + } + case View.FocusDirection.Down: + { + nextFocusedView = DownFocusableView; + break; + } + } + + if (nextFocusedView) + { + focusedView = null; + } + else + { + //If FocusableView doesn't exist, not move focus. + nextFocusedView = focusedView; + } + } + + return nextFocusedView; + } + } +} diff --git a/src/Tizen.NUI/src/internal/XamlBinding/Internals/IDataTemplate.cs b/src/Tizen.NUI/src/internal/XamlBinding/Internals/IDataTemplate.cs deleted file mode 100755 index 168ed7e..0000000 --- a/src/Tizen.NUI/src/internal/XamlBinding/Internals/IDataTemplate.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Tizen.NUI.Binding.Internals -{ - internal interface IDataTemplate - { - Func LoadTemplate { get; set; } - } -} diff --git a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplate.cs b/src/Tizen.NUI/src/public/Template/DataTemplate.cs similarity index 71% rename from src/Tizen.NUI/src/internal/XamlBinding/DataTemplate.cs rename to src/Tizen.NUI/src/public/Template/DataTemplate.cs index 2854104..639b525 100755 --- a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplate.cs +++ b/src/Tizen.NUI/src/public/Template/DataTemplate.cs @@ -1,26 +1,45 @@ using System; +using System.ComponentModel; using System.Collections.Generic; namespace Tizen.NUI.Binding { - internal class DataTemplate : ElementTemplate + [EditorBrowsable(EditorBrowsableState.Never)] + public class DataTemplate : ElementTemplate { + /// + /// Base constructor. + /// + [EditorBrowsable(EditorBrowsableState.Never)] public DataTemplate() { } + /// + /// Base constructor with specific Type. + /// + /// The Type of content. + [EditorBrowsable(EditorBrowsableState.Never)] public DataTemplate(Type type) : base(type) { } + /// + /// Base constructor with loadTemplate function. + /// + /// The function of loading templated object. + [EditorBrowsable(EditorBrowsableState.Never)] public DataTemplate(Func loadTemplate) : base(loadTemplate) { } + [EditorBrowsable(EditorBrowsableState.Never)] public IDictionary Bindings { get; } = new Dictionary(); + [EditorBrowsable(EditorBrowsableState.Never)] public IDictionary Values { get; } = new Dictionary(); + [EditorBrowsable(EditorBrowsableState.Never)] public void SetBinding(BindableProperty property, BindingBase binding) { if (property == null) @@ -32,6 +51,7 @@ namespace Tizen.NUI.Binding Bindings[property] = binding; } + [EditorBrowsable(EditorBrowsableState.Never)] public void SetValue(BindableProperty property, object value) { if (property == null) diff --git a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplateExtensions.cs b/src/Tizen.NUI/src/public/Template/DataTemplateExtensions.cs similarity index 80% rename from src/Tizen.NUI/src/internal/XamlBinding/DataTemplateExtensions.cs rename to src/Tizen.NUI/src/public/Template/DataTemplateExtensions.cs index 36e48a1..3df7b95 100755 --- a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplateExtensions.cs +++ b/src/Tizen.NUI/src/public/Template/DataTemplateExtensions.cs @@ -3,8 +3,9 @@ namespace Tizen.NUI.Binding { [EditorBrowsable(EditorBrowsableState.Never)] - internal static class DataTemplateExtensions + public static class DataTemplateExtensions { + [EditorBrowsable(EditorBrowsableState.Never)] public static DataTemplate SelectDataTemplate(this DataTemplate self, object item, BindableObject container) { var selector = self as DataTemplateSelector; @@ -14,6 +15,7 @@ namespace Tizen.NUI.Binding return selector.SelectTemplate(item, container); } + [EditorBrowsable(EditorBrowsableState.Never)] public static object CreateContent(this DataTemplate self, object item, BindableObject container) { return self.SelectDataTemplate(item, container).CreateContent(); diff --git a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplateSelector.cs b/src/Tizen.NUI/src/public/Template/DataTemplateSelector.cs similarity index 78% rename from src/Tizen.NUI/src/internal/XamlBinding/DataTemplateSelector.cs rename to src/Tizen.NUI/src/public/Template/DataTemplateSelector.cs index c7afa2c..1037b73 100755 --- a/src/Tizen.NUI/src/internal/XamlBinding/DataTemplateSelector.cs +++ b/src/Tizen.NUI/src/public/Template/DataTemplateSelector.cs @@ -1,12 +1,15 @@ using System; +using System.ComponentModel; using System.Collections.Generic; namespace Tizen.NUI.Binding { - internal abstract class DataTemplateSelector : DataTemplate + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class DataTemplateSelector : DataTemplate { Dictionary _dataTemplates = new Dictionary(); + [EditorBrowsable(EditorBrowsableState.Never)] public DataTemplate SelectTemplate(object item, BindableObject container) { DataTemplate dataTemplate = null; @@ -23,6 +26,7 @@ namespace Tizen.NUI.Binding return dataTemplate; } + [EditorBrowsable(EditorBrowsableState.Never)] protected abstract DataTemplate OnSelectTemplate(object item, BindableObject container); } } diff --git a/src/Tizen.NUI/src/internal/XamlBinding/ElementTemplate.cs b/src/Tizen.NUI/src/public/Template/ElementTemplate.cs similarity index 96% rename from src/Tizen.NUI/src/internal/XamlBinding/ElementTemplate.cs rename to src/Tizen.NUI/src/public/Template/ElementTemplate.cs index 401862b..fc828fc 100755 --- a/src/Tizen.NUI/src/internal/XamlBinding/ElementTemplate.cs +++ b/src/Tizen.NUI/src/public/Template/ElementTemplate.cs @@ -9,7 +9,7 @@ namespace Tizen.NUI.Binding /// Base class for DataTemplate and ControlTemplate classes. /// [EditorBrowsable(EditorBrowsableState.Never)] - internal class ElementTemplate : IElement, IDataTemplate + public class ElementTemplate : IElement, IDataTemplate { List> _changeHandlers; Element _parent; @@ -80,6 +80,7 @@ namespace Tizen.NUI.Binding /// Used by the XAML infrastructure to load data templates and set up the content of the resulting UI. /// /// + [EditorBrowsable(EditorBrowsableState.Never)] public object CreateContent() { if (LoadTemplate == null) diff --git a/src/Tizen.NUI/src/public/Template/IDataTemplate.cs b/src/Tizen.NUI/src/public/Template/IDataTemplate.cs new file mode 100755 index 0000000..99ba11f --- /dev/null +++ b/src/Tizen.NUI/src/public/Template/IDataTemplate.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel; + +namespace Tizen.NUI.Binding +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public interface IDataTemplate + { + Func LoadTemplate { get; set; } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewGridSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewGridSample.cs new file mode 100755 index 0000000..36b2992 --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewGridSample.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Samples +{ + public class CollectionViewGridSample : IExample + { + CollectionView colView; + int itemCount = 500; + int selectedCount; + ItemSelectionMode selMode; + + public void Activate() + { + Window window = NUIApplication.GetDefaultWindow(); + + var myViewModelSource = new GalleryViewModel(itemCount); + selMode = ItemSelectionMode.MultipleSelections; + DefaultTitleItem myTitle = new DefaultTitleItem(); + myTitle.Text = "Grid Sample Count["+itemCount+"] Selected["+selectedCount+"]"; + //Set Width Specification as MatchParent to fit the Item width with parent View. + myTitle.WidthSpecification = LayoutParamPolicies.MatchParent; + + colView = new CollectionView() + { + ItemsSource = myViewModelSource, + ItemsLayouter = new GridLayouter(), + ItemTemplate = new DataTemplate(() => + { + DefaultGridItem item = new DefaultGridItem(); + item.WidthSpecification = 180; + item.HeightSpecification = 240; + //Decorate Label + item.Caption.SetBinding(TextLabel.TextProperty, "ViewLabel"); + item.Caption.HorizontalAlignment = HorizontalAlignment.Center; + //Decorate Image + item.Image.SetBinding(ImageView.ResourceUrlProperty, "ImageUrl"); + item.Image.WidthSpecification = 170; + item.Image.HeightSpecification = 170; + //Decorate Badge checkbox. + //[NOTE] This is sample of CheckBox usage in CollectionView. + // Checkbox change their selection by IsSelectedProperty bindings with + // SelectionChanged event with MulitpleSelections ItemSelectionMode of CollectionView. + item.Badge = new CheckBox(); + //FIXME : SetBinding in RadioButton crashed as Sensitive Property is disposed. + //item.Badge.SetBinding(CheckBox.IsSelectedProperty, "Selected"); + item.Badge.WidthSpecification = 30; + item.Badge.HeightSpecification = 30; + + return item; + }), + Header = myTitle, + ScrollingDirection = ScrollableBase.Direction.Vertical, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + SelectionMode = selMode + }; + colView.SelectionChanged += SelectionEvt; + + window.Add(colView); + } + + public void SelectionEvt(object sender, SelectionChangedEventArgs ev) + { + List oldSel = new List(ev.PreviousSelection); + List newSel = new List(ev.CurrentSelection); + + foreach (object item in oldSel) + { + if (item != null && item is Gallery) + { + Gallery galItem = (Gallery)item; + if (!(newSel.Contains(item))) + { + galItem.Selected = false; + Tizen.Log.Debug("Unselected: {0}", galItem.ViewLabel); + selectedCount--; + } + } + else continue; + } + foreach (object item in newSel) + { + if (item != null && item is Gallery) + { + Gallery galItem = (Gallery)item; + if (!(oldSel.Contains(item))) + { + galItem.Selected = true; + Tizen.Log.Debug("Selected: {0}", galItem.ViewLabel); + selectedCount++; + } + } + else continue; + } + if (colView.Header != null && colView.Header is DefaultTitleItem) + { + DefaultTitleItem title = (DefaultTitleItem)colView.Header; + title.Text = "Grid Sample Count["+itemCount+"] Selected["+selectedCount+"]"; + } + } + + public void Deactivate() + { + if (colView != null) + { + colView.Dispose(); + } + } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewLinearSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewLinearSample.cs new file mode 100755 index 0000000..693cf1e --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/CollectionViewLinearSample.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using Tizen.NUI.Binding; +using System.ComponentModel; +using System; + +namespace Tizen.NUI.Samples +{ + public class CollectionViewLinearSample : IExample + { + CollectionView colView; + int itemCount = 500; + string selectedItem; + ItemSelectionMode selMode; + + public void Activate() + { + Window window = NUIApplication.GetDefaultWindow(); + + var myViewModelSource = new GalleryViewModel(itemCount); + selMode = ItemSelectionMode.SingleSelection; + DefaultTitleItem myTitle = new DefaultTitleItem(); + myTitle.Text = "Linear Sample Count["+itemCount+"]"; + //Set Width Specification as MatchParent to fit the Item width with parent View. + myTitle.WidthSpecification = LayoutParamPolicies.MatchParent; + + colView = new CollectionView() + { + ItemsSource = myViewModelSource, + ItemsLayouter = new LinearLayouter(), + ItemTemplate = new DataTemplate(() => + { + var rand = new Random(); + RecyclerViewItem item = new RecyclerViewItem(); + item.WidthSpecification = LayoutParamPolicies.MatchParent; + item.HeightSpecification = 100; + item.BackgroundColor = new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), 1); + /* + DefaultLinearItem item = new DefaultLinearItem(); + //Set Width Specification as MatchParent to fit the Item width with parent View. + item.WidthSpecification = LayoutParamPolicies.MatchParent; + //Decorate Label + item.Label.SetBinding(TextLabel.TextProperty, "ViewLabel"); + item.Label.HorizontalAlignment = HorizontalAlignment.Begin; + //Decorate Icon + item.Icon.SetBinding(ImageView.ResourceUrlProperty, "ImageUrl"); + item.Icon.WidthSpecification = 80; + item.Icon.HeightSpecification = 80; + //Decorate Extra RadioButton. + //[NOTE] This is sample of RadioButton usage in CollectionView. + // RadioButton change their selection by IsSelectedProperty bindings with + // SelectionChanged event with SingleSelection ItemSelectionMode of CollectionView. + // be aware of there are no RadioButtonGroup. + item.Extra = new RadioButton(); + //FIXME : SetBinding in RadioButton crashed as Sensitive Property is disposed. + //item.Extra.SetBinding(RadioButton.IsSelectedProperty, "Selected"); + item.Extra.WidthSpecification = 80; + item.Extra.HeightSpecification = 80; + */ + + return item; + }), + Header = myTitle, + ScrollingDirection = ScrollableBase.Direction.Vertical, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + SelectionMode = selMode + }; + colView.SelectionChanged += SelectionEvt; + + window.Add(colView); + + } + + public void SelectionEvt(object sender, SelectionChangedEventArgs ev) + { + //Tizen.Log.Debug("NUI", "LSH :: SelectionEvt called"); + + //SingleSelection Only have 1 or nil object in the list. + foreach (object item in ev.PreviousSelection) + { + if (item == null) break; + Gallery unselItem = (Gallery)item; + + unselItem.Selected = false; + selectedItem = null; + //Tizen.Log.Debug("NUI", "LSH :: Unselected: {0}", unselItem.ViewLabel); + } + foreach (object item in ev.CurrentSelection) + { + if (item == null) break; + Gallery selItem = (Gallery)item; + selItem.Selected = true; + selectedItem = selItem.Name; + //Tizen.Log.Debug("NUI", "LSH :: Selected: {0}", selItem.ViewLabel); + } + if (colView.Header != null && colView.Header is DefaultTitleItem) + { + DefaultTitleItem title = (DefaultTitleItem)colView.Header; + title.Text = "Linear Sample Count[" + itemCount + (selectedItem != null ? "] Selected [" + selectedItem + "]" : "]"); + } + } + public void Deactivate() + { + if (colView != null) + { + colView.Dispose(); + } + } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Gallery.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Gallery.cs new file mode 100644 index 0000000..4a08d75 --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Gallery.cs @@ -0,0 +1,208 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using Tizen.NUI.Binding; + + + +class Gallery : INotifyPropertyChanged +{ + string sourceDir = Tizen.NUI.Samples.CommonResource.GetDaliResourcePath()+"ItemViewDemo/gallery/gallery-medium-"; + private int index; + private string name; + private bool selected; + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyyChanged(string propertyName) + { + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public Gallery(int galleryIndex, string galleryName) + { + index = galleryIndex; + name = galleryName; + } + + public string Name { + get + { + return name; + } + set + { + name = value; + OnPropertyyChanged("Name"); + OnPropertyyChanged("ViewLabel"); + } + } + public string ViewLabel + { + get + { + return "[" + index + "] : " + name; + } + } + + public string ImageUrl + { + get + { + return sourceDir+(index%20)+".jpg"; + } + } + + public bool Selected { + get + { + return selected; + } + set + { + selected = value; + OnPropertyyChanged("Selected"); + } + } +} + +class Album : ObservableCollection +{ + private int index; + private string name; + private DateTime date; + + public Album(int albumIndex, string albumName, DateTime albumDate) + { + index = albumIndex; + name = albumName; + date = albumDate; + } + + public string Title + { + get + { + return "[" + index + "] " + name; + } + } + + public string Date + { + get + { + return date.ToLongDateString(); + } + } +} + +class GalleryViewModel : ObservableCollection +{ + string[] namePool = { + "Cat", + "Boy", + "Arm muscle", + "Girl", + "House", + "Cafe", + "Statue", + "Sea", + "hosepipe", + "Police", + "Rainbow", + "Icicle", + "Tower with the Moon", + "Giraffe", + "Camel", + "Zebra", + "Red Light", + "Banana", + "Lion", + "Espresso", + }; + public GalleryViewModel(int count) + { + CreateData(this, count); + } + + public ObservableCollection CreateData(ObservableCollection result , int count) + { + for (int i = 0; i < count; i++) + { + result.Add(new Gallery(i, namePool[i%20])); + } + return result; + } +} + +class AlbumViewModel : ObservableCollection +{ + string[] namePool = { + "Cat", + "Boy", + "Arm muscle", + "Girl", + "House", + "Cafe", + "Statue", + "Sea", + "hosepipe", + "Police", + "Rainbow", + "Icicle", + "Tower with the Moon", + "Giraffe", + "Camel", + "Zebra", + "Red Light", + "Banana", + "Lion", + "Espresso", + }; + + (string name, DateTime date)[] titlePool = { + ("House Move", new DateTime(2021, 2, 26)), + ("Covid 19", new DateTime(2020, 1, 20)), + ("Porto Trip", new DateTime(2019, 11, 23)), + ("Granada Trip", new DateTime(2019, 11, 20)), + ("Barcelona Trip", new DateTime(2019, 11, 17)), + ("Developer's Day", new DateTime(2019, 11, 16)), + ("Tokyo Trip", new DateTime(2019, 7, 5)), + ("Otaru Trip", new DateTime(2019, 3, 2)), + ("Sapporo Trip", new DateTime(2019, 2, 28)), + ("Hakodate Trip", new DateTime(2019, 2, 26)), + ("Friend's Wedding", new DateTime(2018, 11, 23)), + ("Grandpa Birthday", new DateTime(2018, 9, 14)), + ("Family Jeju Trip", new DateTime(2018, 7, 15)), + ("HongKong Trip", new DateTime(2018, 3, 30)), + ("Mom's Birthday", new DateTime(2017, 12, 21)), + ("Buy new Car", new DateTime(2017, 10, 18)), + ("Graduation", new DateTime(2017, 6, 30)), + }; + + + public AlbumViewModel() + { + CreateData(this); + } + + public ObservableCollection CreateData(ObservableCollection result) + { + for (int i = 0; i < titlePool.Length; i++) + { + (string name, DateTime date) = titlePool[i]; + Album cur = new Album(i, name, date); + for (int j = 0; j < 20; j++) + { + cur.Add(new Gallery(j, namePool[j])); + } + result.Add(cur); + } + return result; + } + +} \ No newline at end of file diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewGridGroupSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewGridGroupSample.cs new file mode 100644 index 0000000..5aa0b9c --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewGridGroupSample.cs @@ -0,0 +1,128 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Samples +{ + public class CollectionViewGridGroupSample : IExample + { + CollectionView colView; + int selectedCount; + ItemSelectionMode selMode; + ObservableCollection groupSource; + + public void Activate() + { + Window window = NUIApplication.GetDefaultWindow(); + + groupSource = new AlbumViewModel(); + selMode = ItemSelectionMode.MultipleSelections; + DefaultTitleItem myTitle = new DefaultTitleItem(); + myTitle.Text = "Grid Sample Count["+ groupSource.Count+"] Selected["+selectedCount+"]"; + //Set Width Specification as MatchParent to fit the Item width with parent View. + myTitle.WidthSpecification = LayoutParamPolicies.MatchParent; + + colView = new CollectionView() + { + ItemsSource = groupSource, + ItemsLayouter = new GridLayouter(), + ItemTemplate = new DataTemplate(() => + { + DefaultGridItem item = new DefaultGridItem(); + item.WidthSpecification = 180; + item.HeightSpecification = 240; + //Decorate Label + item.Caption.SetBinding(TextLabel.TextProperty, "ViewLabel"); + item.Caption.HorizontalAlignment = HorizontalAlignment.Center; + //Decorate Image + item.Image.SetBinding(ImageView.ResourceUrlProperty, "ImageUrl"); + item.Image.WidthSpecification = 170; + item.Image.HeightSpecification = 170; + //Decorate Badge checkbox. + //[NOTE] This is sample of CheckBox usage in CollectionView. + // Checkbox change their selection by IsSelectedProperty bindings with + // SelectionChanged event with MulitpleSelections ItemSelectionMode of CollectionView. + item.Badge = new CheckBox(); + //FIXME : SetBinding in RadioButton crashed as Sensitive Property is disposed. + //item.Badge.SetBinding(CheckBox.IsSelectedProperty, "Selected"); + item.Badge.WidthSpecification = 30; + item.Badge.HeightSpecification = 30; + + return item; + }), + GroupHeaderTemplate = new DataTemplate(() => + { + DefaultTitleItem group = new DefaultTitleItem(); + //Set Width Specification as MatchParent to fit the Item width with parent View. + group.WidthSpecification = LayoutParamPolicies.MatchParent; + + group.Label.SetBinding(TextLabel.TextProperty, "Date"); + group.Label.HorizontalAlignment = HorizontalAlignment.Begin; + + return group; + }), + Header = myTitle, + IsGrouped = true, + ScrollingDirection = ScrollableBase.Direction.Vertical, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + SelectionMode = selMode + }; + colView.SelectionChanged += SelectionEvt; + + window.Add(colView); + } + + public void SelectionEvt(object sender, SelectionChangedEventArgs ev) + { + List oldSel = new List(ev.PreviousSelection); + List newSel = new List(ev.CurrentSelection); + + foreach (object item in oldSel) + { + if (item != null && item is Gallery) + { + Gallery galItem = (Gallery)item; + if (!(newSel.Contains(item))) + { + galItem.Selected = false; + Tizen.Log.Debug("Unselected: {0}", galItem.ViewLabel); + selectedCount--; + } + } + else continue; + } + foreach (object item in newSel) + { + if (item != null && item is Gallery) + { + Gallery galItem = (Gallery)item; + if (!(oldSel.Contains(item))) + { + galItem.Selected = true; + Tizen.Log.Debug("Selected: {0}", galItem.ViewLabel); + selectedCount++; + } + } + else continue; + } + if (colView.Header != null && colView.Header is DefaultTitleItem) + { + DefaultTitleItem title = (DefaultTitleItem)colView.Header; + title.Text = "Grid Sample Count["+ groupSource.Count + "] Selected["+selectedCount+"]"; + } + } + + public void Deactivate() + { + if (colView != null) + { + colView.Dispose(); + } + } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewLinearGroupSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewLinearGroupSample.cs new file mode 100644 index 0000000..f8a759b --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/CollectionViewDemo/Group/CollectionViewLinearGroupSample.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; +using Tizen.NUI.Binding; + +namespace Tizen.NUI.Samples +{ + public class CollectionViewLinearGroupSample : IExample + { + CollectionView colView; + string selectedItem; + ItemSelectionMode selMode; + ObservableCollection groupSource; + + public void Activate() + { + Window window = NUIApplication.GetDefaultWindow(); + + groupSource = new AlbumViewModel(); + selMode = ItemSelectionMode.SingleSelection; + DefaultTitleItem myTitle = new DefaultTitleItem(); + //To Bind the Count property changes, need to create custom property for count. + myTitle.Text = "Linear Sample Group["+ groupSource.Count+"]"; + //Set Width Specification as MatchParent to fit the Item width with parent View. + myTitle.WidthSpecification = LayoutParamPolicies.MatchParent; + + colView = new CollectionView() + { + ItemsSource = groupSource, + ItemsLayouter = new LinearLayouter(), + ItemTemplate = new DataTemplate(() => + { + var rand = new Random(); + RecyclerViewItem item = new RecyclerViewItem(); + item.WidthSpecification = LayoutParamPolicies.MatchParent; + item.HeightSpecification = 100; + item.BackgroundColor = new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), 1); + /* + DefaultLinearItem item = new DefaultLinearItem(); + //Set Width Specification as MatchParent to fit the Item width with parent View. + item.WidthSpecification = LayoutParamPolicies.MatchParent; + //Decorate Label + item.Label.SetBinding(TextLabel.TextProperty, "ViewLabel"); + item.Label.HorizontalAlignment = HorizontalAlignment.Begin; + //Decorate Icon + item.Icon.SetBinding(ImageView.ResourceUrlProperty, "ImageUrl"); + item.Icon.WidthSpecification = 80; + item.Icon.HeightSpecification = 80; + //Decorate Extra RadioButton. + //[NOTE] This is sample of RadioButton usage in CollectionView. + // RadioButton change their selection by IsSelectedProperty bindings with + // SelectionChanged event with SingleSelection ItemSelectionMode of CollectionView. + // be aware of there are no RadioButtonGroup. + item.Extra = new RadioButton(); + //FIXME : SetBinding in RadioButton crashed as Sensitive Property is disposed. + //item.Extra.SetBinding(RadioButton.IsSelectedProperty, "Selected"); + item.Extra.WidthSpecification = 80; + item.Extra.HeightSpecification = 80; + */ + return item; + }), + GroupHeaderTemplate = new DataTemplate(() => + { + var rand = new Random(); + RecyclerViewItem item = new RecyclerViewItem(); + item.WidthSpecification = LayoutParamPolicies.MatchParent; + item.HeightSpecification = 50; + item.BackgroundColor = new Color(0, 0, 0, 1); + /* + DefaultTitleItem group = new DefaultTitleItem(); + //Set Width Specification as MatchParent to fit the Item width with parent View. + group.WidthSpecification = LayoutParamPolicies.MatchParent; + + group.Label.SetBinding(TextLabel.TextProperty, "Date"); + group.Label.HorizontalAlignment = HorizontalAlignment.Begin; + */ + return item; + }), + Header = myTitle, + IsGrouped = true, + ScrollingDirection = ScrollableBase.Direction.Vertical, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + SelectionMode = selMode + }; + colView.SelectionChanged += SelectionEvt; + + window.Add(colView); + + } + + public void SelectionEvt(object sender, SelectionChangedEventArgs ev) + { + //Tizen.Log.Debug("NUI", "LSH :: SelectionEvt called"); + + //SingleSelection Only have 1 or nil object in the list. + foreach (object item in ev.PreviousSelection) + { + if (item == null) break; + Gallery unselItem = (Gallery)item; + + unselItem.Selected = false; + selectedItem = null; + //Tizen.Log.Debug("NUI", "LSH :: Unselected: {0}", unselItem.ViewLabel); + } + foreach (object item in ev.CurrentSelection) + { + if (item == null) break; + Gallery selItem = (Gallery)item; + selItem.Selected = true; + selectedItem = selItem.Name; + //Tizen.Log.Debug("NUI", "LSH :: Selected: {0}", selItem.ViewLabel); + } + if (colView.Header != null && colView.Header is DefaultTitleItem) + { + DefaultTitleItem title = (DefaultTitleItem)colView.Header; + title.Text = "Linear Sample Count[" + groupSource + (selectedItem != null ? "] Selected [" + selectedItem + "]" : "]"); + } + } + public void Deactivate() + { + if (colView != null) + { + colView.Dispose(); + } + } + } +}