From 7a3db39f40d2dd110fe28fa731bb1453ff513c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Zitte?= Date: Mon, 17 Sep 2018 16:29:05 -0400 Subject: [PATCH 1/5] Updated Android SimpleOrientationSensor calculations based on SensorType.Gravity or based on single angle orientation when the device does not have a Gyroscope. Internal Work Item: https://nventive.visualstudio.com/Umbrella/_workitems/edit/129762 --- Doc/ReleaseNotes/_ReleaseNotes.md | 11 + .../SimpleOrientationSensor.Android.cs | 194 +++++++++++++++++- 2 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 Doc/ReleaseNotes/_ReleaseNotes.md diff --git a/Doc/ReleaseNotes/_ReleaseNotes.md b/Doc/ReleaseNotes/_ReleaseNotes.md new file mode 100644 index 000000000000..931b0f0d0ab9 --- /dev/null +++ b/Doc/ReleaseNotes/_ReleaseNotes.md @@ -0,0 +1,11 @@ +# Release notes + +## Next version + +### Features + +### Breaking changes + +### Bug fixes + + * 129762 - Updated Android SimpleOrientationSensor calculations based on SensorType.Gravity or based on single angle orientation when the device does not have a Gyroscope. diff --git a/src/Uno.UWP/Devices/Sensors/SimpleOrientationSensor.Android.cs b/src/Uno.UWP/Devices/Sensors/SimpleOrientationSensor.Android.cs index b8f8467e555f..596f2dac010c 100644 --- a/src/Uno.UWP/Devices/Sensors/SimpleOrientationSensor.Android.cs +++ b/src/Uno.UWP/Devices/Sensors/SimpleOrientationSensor.Android.cs @@ -1,31 +1,92 @@ #if __ANDROID__ +using System; using Android.App; using Android.Content; +using Android.Content.Res; using Android.Hardware; using Android.Runtime; +using Android.Views; +using Uno.UI; +using Android.Provider; +using static Android.Provider.Settings; +using Android.Database; +using Android.OS; namespace Windows.Devices.Sensors { public partial class SimpleOrientationSensor : Java.Lang.Object, ISensorEventListener { + #region Static + + private static Orientation _defaultDeviceOrientation; + + private static Orientation DefaultDeviceOrientation + { + get + { + if (_defaultDeviceOrientation == Orientation.Undefined) + { + var context = ContextHelper.Current; + + if (context != null) + { + var windowManager = context.GetSystemService(Context.WindowService).JavaCast(); + var config = context.Resources.Configuration; + var rotation = windowManager.DefaultDisplay.Rotation; + + _defaultDeviceOrientation = + ((rotation == SurfaceOrientation.Rotation0 || rotation == SurfaceOrientation.Rotation180) && config.Orientation == Orientation.Landscape) || + ((rotation == SurfaceOrientation.Rotation90 || rotation == SurfaceOrientation.Rotation270) && config.Orientation == Orientation.Portrait) + ? Orientation.Landscape + : Orientation.Portrait; + } + } + + return _defaultDeviceOrientation; + } + } + + #endregion + + private SimpleOrientationEventListener _orientationListener; + private SettingsContentObserver _contentObserver; private SensorManager _sensorManager; // Threshold, in meters per second squared, closely equivalent to an angle of 25 degrees which correspond to the value when Android detect new screen orientation private const double _threshold = 4.55; - private const Android.Hardware.SensorType _sensorType = Android.Hardware.SensorType.Gravity; + private const Android.Hardware.SensorType _gravitySensorType = Android.Hardware.SensorType.Gravity; partial void Initialize() { _sensorManager = (SensorManager)Application.Context.GetSystemService(Context.SensorService); - _sensorManager.RegisterListener(this, _sensorManager.GetDefaultSensor(_sensorType), SensorDelay.Normal); + var gravitySensor = _sensorManager.GetDefaultSensor(_gravitySensorType); + + // If the the device has a gyroscope we will use the SensorType.Gravity, if not we will use single angle orientation calculations instead + if (gravitySensor != null) + { + _sensorManager.RegisterListener(this, _sensorManager.GetDefaultSensor(_gravitySensorType), SensorDelay.Normal); + } + else + { + _orientationListener = new SimpleOrientationEventListener(orientation => OnOrientationChanged(orientation)); + _contentObserver = new SettingsContentObserver(new Handler(Looper.MainLooper), () => OnIsAccelerometerRotationEnabledChanged(IsAccelerometerRotationEnabled)); + + ContextHelper.Current.ContentResolver.RegisterContentObserver(Settings.System.GetUriFor(Settings.System.AccelerometerRotation), true, _contentObserver); + if (_orientationListener.CanDetectOrientation() && IsAccelerometerRotationEnabled) + { + _orientationListener.Enable(); + } + } } + #region GraviySensorType Methods + public void OnAccuracyChanged(Sensor sensor, [GeneratedEnum] SensorStatus accuracy) { } public void OnSensorChanged(SensorEvent e) { - if (e.Sensor.Type != _sensorType) + if (e.Sensor.Type != _gravitySensorType) { return; } @@ -38,6 +99,131 @@ public void OnSensorChanged(SensorEvent e) var simpleOrientation = ToSimpleOrientation(gravityX, gravityY, gravityZ, _threshold, _currentOrientation); SetCurrentOrientation(simpleOrientation); } + + #endregion + + #region OrientationSensorType Methods and Classes + + private void OnOrientationChanged(int angle) + { + var simpleOrientation = ToSimpleOrientationRelativeToPortrait(angle, _currentOrientation); + SetCurrentOrientation(simpleOrientation); + } + + private static SimpleOrientation ToSimpleOrientationRelativeToPortrait(int orientation, SimpleOrientation previousOrientation) + { + // https://developer.android.com/reference/android/view/OrientationEventListener.html + // orientation parameter is in degrees, ranging from 0 to 359. + // orientation is: + // - 0 degrees when the device is oriented in its natural position + // - 90 degrees when its left side is at the top + // - 180 degrees when it is upside down + // - 270 degrees when its right side is to the top + // - ORIENTATION_UNKNOWN when the device is close to flat and the orientation cannot be determined. + + if (orientation == OrientationEventListener.OrientationUnknown) + { + // device is close to flat then we push a face-up by default. + return SimpleOrientation.Faceup; + } + + if (DefaultDeviceOrientation == Orientation.Landscape) + { + // we offset the rotation by 270 degrees because + // we want an orientation relative to Portrait + orientation = (orientation + 270) % 360; + } + + // Ensures orientation only changes when within close range to new orientation. + // Empirical testing on an Android 6.0 device indicates that orientation changes + // when within about 22.5° (90° / 4) of a new orientation (0°, 90°, 180°, 270°). + var threshold = 22.5; + + if (Math.Abs(orientation - 0) < threshold || Math.Abs(orientation - 360) < threshold) + { + // natural position + return SimpleOrientation.NotRotated; + } + else if (Math.Abs(orientation - 90) < threshold) + { + // left side is at the top + return SimpleOrientation.Rotated270DegreesCounterclockwise; + } + else if (Math.Abs(orientation - 180) < threshold) + { + // upside down + return SimpleOrientation.Rotated180DegreesCounterclockwise; + } + else if (Math.Abs(orientation - 270) < threshold) + { + // right side is to the top + return SimpleOrientation.Rotated90DegreesCounterclockwise; + } + else + { + return previousOrientation; + } + } + + private void OnIsAccelerometerRotationEnabledChanged(bool isAccelerometerRotationEnabled) + { + if (isAccelerometerRotationEnabled) + { + _orientationListener.Enable(); + } + else + { + _orientationListener.Disable(); + SetCurrentOrientation(SimpleOrientation.NotRotated); + } + } + + private static bool IsAccelerometerRotationEnabled + { + get + { + try + { + return Settings.System.GetInt(ContextHelper.Current.ContentResolver, Settings.System.AccelerometerRotation, 0) == 1; + } + catch (SettingNotFoundException) + { + return true; // If it can't be disabled in the settings, we assume it's enabled. + } + } + } + + private class SettingsContentObserver : ContentObserver + { + Action _onChanged; + + public SettingsContentObserver(Handler handler, Action onChange) : base(handler) + { + _onChanged = onChange; + } + + public override bool DeliverSelfNotifications() => true; + + public override void OnChange(bool selfChange) + { + base.OnChange(selfChange); + _onChanged(); + } + } + + private class SimpleOrientationEventListener : OrientationEventListener + { + private Action _orientationChanged; + + public SimpleOrientationEventListener(Action orientationChanged) : base(ContextHelper.Current, SensorDelay.Normal) + { + _orientationChanged = orientationChanged; + } + + public override void OnOrientationChanged(int orientation) => _orientationChanged(orientation); + } + + #endregion } } -#endif \ No newline at end of file +#endif From e094f97d08cee59957ad10d5b7a371873c076e19 Mon Sep 17 00:00:00 2001 From: David Oliver Date: Fri, 21 Sep 2018 09:03:02 -0400 Subject: [PATCH 2/5] [iOS] ListView.ScrollIntoView - support Alignment=Leading, fix for ItemTemplateSelector Handle ScrollIntoView manually rather than via native method, this allows both ScrollIntoViewAlignments to be supported. Refactor materialization logic and update it when ScrollIntoView is called to correctly support ScrollIntoView when ItemTemplateSelector is set. --- .../ListViewBase/ListViewBaseSource.iOS.cs | 40 ++++++++++-- .../ListViewBase/NativeListViewBase.iOS.cs | 11 +++- .../VirtualizingPanelLayout.Android.cs | 7 ++- .../VirtualizingPanelLayout.iOS.cs | 61 +++++++++++++++++++ 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs index d2858f35f1c7..d44dcde623d9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs @@ -74,6 +74,10 @@ public static class TraceProvider private DataTemplateSelector _currentSelector; private Dictionary _templateCache = new Dictionary(DataTemplate.FrameworkTemplateEqualityComparer.Default); private Dictionary _templateCells = new Dictionary(DataTemplate.FrameworkTemplateEqualityComparer.Default); + /// + /// The furthest item in the source which has already been materialized. Items up to this point can safely be retrieved. + /// + private NSIndexPath _lastMaterializedItem = NSIndexPath.FromRowSection(0, 0); /// /// Is the UICollectionView currently undergoing animated scrolling, either user-initiated or programmatic. @@ -198,10 +202,7 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS // is used during the calculation of the layout. // This is required for paged lists, so that the layout calculation // does not eagerly get all the items of the ItemsSource. - if (!_materializedItems.Contains(indexPath)) - { - _materializedItems.Add(indexPath); - } + UpdateLastMaterializedItem(indexPath); var index = Owner?.XamlParent?.GetIndexFromIndexPath(IndexPath.FromNSIndexPath(indexPath)) ?? -1; @@ -240,6 +241,29 @@ public override UICollectionViewCell GetCell(UICollectionView collectionView, NS } } + /// + /// Update record of the furthest item materialized. + /// + /// Item currently being materialized. + /// True if the value has changed and the layout would change. + internal bool UpdateLastMaterializedItem(NSIndexPath newItem) + { + if (newItem.Compare(_lastMaterializedItem) > 0) + { + _lastMaterializedItem = newItem; + return Owner.ItemTemplateSelector != null; ; + } + else + { + return false; + } + } + + /// + /// Is item in the range of already-materialized items? + /// + private bool IsMaterialized(NSIndexPath itemPath) => itemPath.Compare(_lastMaterializedItem) >= 0; + public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath) { var index = Owner?.XamlParent?.GetIndexFromIndexPath(IndexPath.FromNSIndexPath(indexPath)) ?? -1; @@ -418,6 +442,11 @@ public override void WillEndDragging(UIScrollView scrollView, CGPoint velocity, #endregion + internal void ReloadData() + { + _lastMaterializedItem = NSIndexPath.FromRowSection(0, 0); + } + /// /// Get item container corresponding to an element kind (header, footer, list item, etc) /// @@ -453,7 +482,6 @@ internal CGSize GetSectionHeaderSize() return (Owner.GroupStyle?.HeaderTemplate).SelectOrDefault(ht => GetTemplateSize(ht, NativeListViewBase.ListViewSectionHeaderElementKindNS), CGSize.Empty); } - HashSet _materializedItems = new HashSet(); public virtual CGSize GetItemSize(UICollectionView collectionView, NSIndexPath indexPath) { @@ -483,7 +511,7 @@ public virtual CGSize GetItemSize(UICollectionView collectionView, NSIndexPath i private DataTemplate GetTemplateForItem(NSIndexPath indexPath) { - if (_materializedItems.Contains(indexPath)) + if (IsMaterialized(indexPath)) { return Owner?.ResolveItemTemplate(Owner.XamlParent.GetDisplayItemFromIndexPath(indexPath.ToIndexPath())); } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.iOS.cs index 3634aef4fb2d..7b867b9a65f4 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.iOS.cs @@ -554,7 +554,15 @@ void ScrollInner() { //Scroll to individual item, We set the UICollectionViewScrollPosition to None to have the same behavior as windows. //We can potentially customize this By using ConvertScrollAlignment and use different alignments. - ScrollToItem(index, ConvertSnapPointsAlignmentToScrollPosition(), AnimateScrollIntoView); + var needsMaterialize = Source.UpdateLastMaterializedItem(index); + if (needsMaterialize) + { + NativeLayout.InvalidateLayout(); + } + + var offset = NativeLayout.GetTargetScrollOffset(index, alignment); + SetContentOffset(offset, AnimateScrollIntoView); + NativeLayout.UpdateStickyHeaderPositions(); } } } @@ -680,6 +688,7 @@ internal void ReloadDataIfNeeded() _needsLayoutAfterReloadData = true; ReloadData(); + Source?.ReloadData(); NativeLayout?.ReloadData(); _listEmptyLastRefresh = XamlParent?.NumberOfItems == 0; diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs index 5b1e7c11a993..da5cd7043dd7 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs @@ -226,7 +226,7 @@ internal void ScrollToPosition(int position, ScrollIntoViewAlignment alignment) private void ApplyScrollToPosition(int targetPosition, ScrollIntoViewAlignment alignment, RecyclerView.Recycler recycler, RecyclerView.State state) { int offsetToApply = 0; - bool shouldSnapToStart = false; + bool shouldSnapToStart = false; //Initial values: if the item is fully visible, it shouldn't snap (alignment = default) bool shouldSnapToEnd = false; // 1. Incrementally scroll until target position lies within range of visible positions @@ -234,14 +234,14 @@ private void ApplyScrollToPosition(int targetPosition, ScrollIntoViewAlignment a int appliedOffset = 0; while (targetPosition > GetLastVisibleDisplayPosition() && GetNextUnmaterializedItem(FillDirection.Forward) != null) { - shouldSnapToEnd = true; + shouldSnapToEnd = true; //If the item is below the viewport, it should be snapped to the bottom of the viewport (alignment = default) appliedOffset += GetScrollConsumptionIncrement(FillDirection.Forward); offsetToApply += ScrollByInner(appliedOffset, recycler, state); } //While target position is before first visible position, scroll backward while (targetPosition < GetFirstVisibleDisplayPosition() && GetNextUnmaterializedItem(FillDirection.Back) != null) { - shouldSnapToStart = true; + shouldSnapToStart = true; //If the item is above the viewport, it should be snapped to the bottom of the viewport (alignment = default) appliedOffset -= GetScrollConsumptionIncrement(FillDirection.Back); offsetToApply += ScrollByInner(appliedOffset, recycler, state); } @@ -251,6 +251,7 @@ private void ApplyScrollToPosition(int targetPosition, ScrollIntoViewAlignment a if (alignment == ScrollIntoViewAlignment.Leading) { + // 'Leading' means that the item always snaps to the top of the viewport no matter what shouldSnapToStart = true; shouldSnapToEnd = false; } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs index 97141eb3139e..8d33315ac031 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs @@ -862,6 +862,18 @@ public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds) return false; } + /// + /// Triggers an update of the sticky group header positions. + /// + public void UpdateStickyHeaderPositions() + { + if (AreStickyGroupHeadersEnabled) + { + _invalidatingHeadersOnBoundsChange = true; + InvalidateLayout(); + } + } + public override void InvalidateLayout() { //Called in response to INotifyCollectionChanged operation, update layout reusing already-calculated databound element sizes @@ -1208,6 +1220,55 @@ private float AdjustOffsetForSnapPointsAlignment(nfloat contentOffset) } } + /// + /// Get offset to apply to scroll item into view + /// + internal CGPoint GetTargetScrollOffset(NSIndexPath item, ScrollIntoViewAlignment alignment) + { + var baseOffsetExtent = GetBaseOffset(); + var snapTo = GetSnapTo(scrollVelocity: 0, (float)baseOffsetExtent); + if (snapTo.HasValue) + { + return GetSnapToAsOffset(snapTo.Value); + } + else + { + return SetExtentOffset(CGPoint.Empty, baseOffsetExtent); + } + + nfloat GetBaseOffset() + { + var frame = LayoutAttributesForItem(item).Frame; + var frameExtentStart = GetExtentStart(frame); + if (alignment == ScrollIntoViewAlignment.Leading) + { + // Alignment=Leading snaps item to same position no matter where it currently is + return frameExtentStart; + } + else // Alignment=Default + { + var currentOffset = GetExtent(Owner.ContentOffset); + var targetOffset = currentOffset; + var frameExtentEnd = GetExtentEnd(frame); + var viewportHeight = GetExtent(CollectionView.Bounds.Size); + + if (frameExtentStart < currentOffset) + { + // Start of item is above viewport, it should snap to start of viewport + targetOffset = frameExtentStart; + } + if (frameExtentEnd > currentOffset + viewportHeight) + { + //End of item is below viewport, it should snap to end of viewport + targetOffset = frameExtentEnd - viewportHeight; + } + // (If neither of the conditions apply, item is already fully in view, return current offset unaltered) + + return targetOffset; + } + } + } + /// /// Get all LayoutAttributes for every display element. /// From dca8854a04a8bc36d1b467db41109193ce758f19 Mon Sep 17 00:00:00 2001 From: David Oliver Date: Mon, 24 Sep 2018 14:41:24 -0400 Subject: [PATCH 3/5] ListView - adjust ScrollIntoView for sticky group headers Offset target item position to ensure that item isn't obscured by currently-sticking group header, if any. --- .../ListViewBase/VirtualizingPanelLayout.Android.cs | 9 ++++++++- .../ListViewBase/VirtualizingPanelLayout.iOS.cs | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs index da5cd7043dd7..c52c92502176 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.Android.cs @@ -259,7 +259,9 @@ private void ApplyScrollToPosition(int targetPosition, ScrollIntoViewAlignment a //2. If view for position lies partially outside visible bounds, bring it into view var target = FindViewByAdapterPosition(targetPosition); - var gapToStart = 0 - GetChildStartWithMargin(target); + var gapToStart = 0 - GetChildStartWithMargin(target) + // Ensure sticky group header doesn't cover item + + GetStickyGroupHeaderExtent(); if (!shouldSnapToStart) { gapToStart = Math.Max(0, gapToStart); @@ -288,6 +290,11 @@ private void ApplyScrollToPosition(int targetPosition, ScrollIntoViewAlignment a FillLayout(FillDirection.Back, 0, Extent, ContentBreadth, recycler, state); } + /// + /// Get extent of currently sticking group header (if any) + /// + private int GetStickyGroupHeaderExtent() => GetFirstGroup().ItemsExtentOffset; + private class ScrollToPositionRequest { public int Position { get; } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs index 8d33315ac031..6e3bd9ddfbba 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs @@ -1239,7 +1239,8 @@ internal CGPoint GetTargetScrollOffset(NSIndexPath item, ScrollIntoViewAlignment nfloat GetBaseOffset() { var frame = LayoutAttributesForItem(item).Frame; - var frameExtentStart = GetExtentStart(frame); + var headerCorrection = GetStickyHeaderExtent(item.Section); + var frameExtentStart = GetExtentStart(frame) - headerCorrection; if (alignment == ScrollIntoViewAlignment.Leading) { // Alignment=Leading snaps item to same position no matter where it currently is @@ -1269,6 +1270,13 @@ nfloat GetBaseOffset() } } + /// + /// Get extent added by sticky group header for given section, if any. + /// + private nfloat GetStickyHeaderExtent(int section) => AreStickyGroupHeadersEnabled && RelativeGroupHeaderPlacement == RelativeHeaderPlacement.Inline ? + GetExtent(_inlineHeaderFrames[section].Size) : + 0; + /// /// Get all LayoutAttributes for every display element. /// From 2b5501412b3ab232bc05ff14c55de5cde38c9747 Mon Sep 17 00:00:00 2001 From: David Oliver Date: Wed, 26 Sep 2018 14:10:19 -0400 Subject: [PATCH 4/5] [Xamarin] ListView - implement GroupStyle.HeaderTemplateSelector --- .../Windows.UI.Xaml.Controls/GroupStyle.cs | 2 +- src/Uno.UI/UI/Xaml/Controls/GroupStyle.cs | 11 +++- .../ListViewBase/ListViewBaseSource.iOS.cs | 26 ++++++++- .../NativeListViewBase.Android.cs | 2 +- .../NativeListViewBaseAdapter.Android.cs | 58 ++++++++++++++----- .../VirtualizingPanelLayout.iOS.cs | 10 ++-- 6 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/GroupStyle.cs b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/GroupStyle.cs index 236d46eaffde..d0df247df227 100644 --- a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/GroupStyle.cs +++ b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Controls/GroupStyle.cs @@ -35,7 +35,7 @@ public bool HidesIfEmpty } } #endif - #if __ANDROID__ || __IOS__ || NET46 || __WASM__ + #if false || false || false || false [global::Uno.NotImplemented] public global::Windows.UI.Xaml.Controls.DataTemplateSelector HeaderTemplateSelector { diff --git a/src/Uno.UI/UI/Xaml/Controls/GroupStyle.cs b/src/Uno.UI/UI/Xaml/Controls/GroupStyle.cs index 12ddf9b75404..d3647fbc7bf4 100644 --- a/src/Uno.UI/UI/Xaml/Controls/GroupStyle.cs +++ b/src/Uno.UI/UI/Xaml/Controls/GroupStyle.cs @@ -7,7 +7,14 @@ namespace Windows.UI.Xaml.Controls public partial class GroupStyle { public DataTemplate HeaderTemplate { get; set; } - public Style HeaderContainerStyle { get; set; } - public bool HidesIfEmpty { get; set; } + +#if false || false || NET46 || __WASM__ + [Uno.NotImplemented] +#endif + public DataTemplateSelector HeaderTemplateSelector { get; set; } + + public Style HeaderContainerStyle { get; set; } + + public bool HidesIfEmpty { get; set; } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs index d44dcde623d9..258371e6d897 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBaseSource.iOS.cs @@ -264,6 +264,9 @@ internal bool UpdateLastMaterializedItem(NSIndexPath newItem) /// private bool IsMaterialized(NSIndexPath itemPath) => itemPath.Compare(_lastMaterializedItem) >= 0; + // Consider group header to be materialized if first item in group is materialized + private bool IsMaterialized(int section) => section <= _lastMaterializedItem.Section; + public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath) { var index = Owner?.XamlParent?.GetIndexFromIndexPath(IndexPath.FromNSIndexPath(indexPath)) ?? -1; @@ -318,6 +321,9 @@ public override UICollectionReusableView GetViewForSupplementaryElement( else if (elementKind == NativeListViewBase.ListViewSectionHeaderElementKind) { + // Ensure correct template can be retrieved + UpdateLastMaterializedItem(indexPath); + return GetBindableSupplementaryView( collectionView: collectionView, elementKind: NativeListViewBase.ListViewSectionHeaderElementKindNS, @@ -325,7 +331,7 @@ public override UICollectionReusableView GetViewForSupplementaryElement( reuseIdentifier: NativeListViewBase.ListViewSectionHeaderReuseIdentifierNS, //ICollectionViewGroup.Group is used as context for sectionHeader context: listView.XamlParent.GetGroupAtDisplaySection(indexPath.Section).Group, - template: listView.GroupStyle?.HeaderTemplate, + template: GetTemplateForGroupHeader(indexPath.Section), style: listView.GroupStyle?.HeaderContainerStyle ); } @@ -477,9 +483,10 @@ internal CGSize GetFooterSize() return Owner.FooterTemplate != null ? GetTemplateSize(Owner.FooterTemplate, NativeListViewBase.ListViewFooterElementKindNS) : CGSize.Empty; } - internal CGSize GetSectionHeaderSize() + internal CGSize GetSectionHeaderSize(int section) { - return (Owner.GroupStyle?.HeaderTemplate).SelectOrDefault(ht => GetTemplateSize(ht, NativeListViewBase.ListViewSectionHeaderElementKindNS), CGSize.Empty); + var template = GetTemplateForGroupHeader(section); + return template.SelectOrDefault(ht => GetTemplateSize(ht, NativeListViewBase.ListViewSectionHeaderElementKindNS), CGSize.Empty); } @@ -522,6 +529,19 @@ private DataTemplate GetTemplateForItem(NSIndexPath indexPath) } } + private DataTemplate GetTemplateForGroupHeader(int section) + { + var groupStyle = Owner.GroupStyle; + if (IsMaterialized(section)) + { + return DataTemplateHelper.ResolveTemplate(groupStyle?.HeaderTemplate, groupStyle?.HeaderTemplateSelector, Owner.XamlParent.GetGroupAtDisplaySection(section).Group); + } + else + { + return groupStyle?.HeaderTemplate; + } + } + /// /// Gets the actual item template size, using a non-databound materialized /// view of the template. diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs index 6afa7d04b8a3..094beb345839 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs @@ -242,7 +242,7 @@ partial void OnUnloadedPartial() public void Refresh() { - CurrentAdapter?.NotifyDataSetChanged(); + CurrentAdapter?.Refresh(); var isScrollResetting = NativeLayout != null && NativeLayout.ContentOffset != 0; NativeLayout?.Refresh(); diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBaseAdapter.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBaseAdapter.Android.cs index 51e038b22cd4..cb39b37e60c6 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBaseAdapter.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBaseAdapter.Android.cs @@ -23,12 +23,13 @@ namespace Windows.UI.Xaml.Controls public class NativeListViewBaseAdapter : RecyclerView.Adapter { private const int NoTemplateItemType = 0; - private const int GroupHeaderItemType = -1; + private const int NoTemplateGroupHeaderType = -1; private const int HeaderItemType = -2; private const int FooterItemType = -3; private const int MaxRecycledViewsPerViewType = 10; + private readonly HashSet _groupHeaderItemTypes = new HashSet(); private ManagedWeakReference _ownerWeakReference; @@ -66,7 +67,8 @@ public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int positi this.Log().Debug($"Binding view with view type {viewType} at position {position}."); } - if (parent.GetIsGroupHeader(position) || parent.GetIsHeader(position) || parent.GetIsFooter(position)) + var isGroupHeader = parent.GetIsGroupHeader(position); + if (isGroupHeader || parent.GetIsHeader(position) || parent.GetIsFooter(position)) { var item = parent.GetElementFromDisplayPosition(position); @@ -79,7 +81,7 @@ public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int positi container.Style = style; } - var dataTemplate = GetDataTemplateFromItem(parent, item, viewType); + var dataTemplate = GetDataTemplateFromItem(parent, item, viewType, isGroupHeader); container.DataContext = item; container.ContentTemplate = dataTemplate; @@ -123,24 +125,20 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int private ContentControl GenerateContainer(int viewType) { - if (viewType == GroupHeaderItemType) - { - return XamlParent?.GetGroupHeaderContainer(null); - } if (viewType == HeaderItemType || viewType == FooterItemType) { return ContentControl.CreateItemContainer(); } + if (_groupHeaderItemTypes.Contains(viewType)) + { + return XamlParent?.GetGroupHeaderContainer(null); + } return XamlParent?.GetContainerForIndex(-1) as ContentControl; } public override int GetItemViewType(int position) { - if (XamlParent?.GetIsGroupHeader(position) ?? false) - { - return GroupHeaderItemType; - } if (XamlParent?.GetIsHeader(position) ?? false) { return HeaderItemType; @@ -151,16 +149,38 @@ public override int GetItemViewType(int position) } var item = XamlParent?.GetElementFromDisplayPosition(position); - var template = GetDataTemplateFromItem(XamlParent, item, null); + var isGroupHeader = XamlParent?.GetIsGroupHeader(position) ?? false; + var template = GetDataTemplateFromItem(XamlParent, item, null, isGroupHeader); - return template?.GetHashCode() ?? NoTemplateItemType; + int viewType; + if (template != null) + { + viewType = template.GetHashCode(); + } + else + { + viewType = isGroupHeader ? NoTemplateGroupHeaderType : NoTemplateGroupHeaderType; + } + + if (isGroupHeader) + { + _groupHeaderItemTypes.Add(viewType); + } + + return viewType; } - private static DataTemplate GetDataTemplateFromItem(ListViewBase parent, object item, int? itemViewType) + private static DataTemplate GetDataTemplateFromItem(ListViewBase parent, object item, int? itemViewType, bool isGroupHeader) { - if (itemViewType == GroupHeaderItemType) + if (isGroupHeader) { - return parent.GetGroupStyle()?.HeaderTemplate; + var groupStyle = parent.GetGroupStyle(); + + return DataTemplateHelper.ResolveTemplate( + groupStyle?.HeaderTemplate, + groupStyle?.HeaderTemplateSelector, + item + ); } if (itemViewType == HeaderItemType) { @@ -176,5 +196,11 @@ private static DataTemplate GetDataTemplateFromItem(ListViewBase parent, object item ); } + + internal void Refresh() + { + _groupHeaderItemTypes.Clear(); + NotifyDataSetChanged(); + } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs index 6e3bd9ddfbba..246c70d6ab3f 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.iOS.cs @@ -516,7 +516,7 @@ private CGSize PrepareLayoutInternal(bool createLayoutInfo, bool isCollectionCha availableGroupBreadth -= (nfloat)GroupPaddingBreadthEnd; //a. Layout group header, if present - frame.Size = oldGroupHeaderSizes?.UnoGetValueOrDefault(section) ?? GetSectionHeaderSize(); + frame.Size = oldGroupHeaderSizes?.UnoGetValueOrDefault(section) ?? GetSectionHeaderSize(section); if (RelativeGroupHeaderPlacement != RelativeHeaderPlacement.Adjacent) { //Give the maximum breadth available, since for now we don't adjust the measured width of the list based on the databound item @@ -819,19 +819,19 @@ protected CGSize GetItemSizeForIndexPath(NSIndexPath indexPath) return Source.GetTarget()?.GetItemSize(CollectionView, indexPath) ?? CGSize.Empty; } - protected CGSize GetHeaderSize() + private CGSize GetHeaderSize() { return Source.GetTarget()?.GetHeaderSize() ?? CGSize.Empty; } - protected CGSize GetFooterSize() + private CGSize GetFooterSize() { return Source.GetTarget()?.GetFooterSize() ?? CGSize.Empty; } - protected CGSize GetSectionHeaderSize() + private CGSize GetSectionHeaderSize(int section) { - return Source.GetTarget()?.GetSectionHeaderSize() ?? CGSize.Empty; + return Source.GetTarget()?.GetSectionHeaderSize(section) ?? CGSize.Empty; } /// From 054269a6fb8963a37a89c0e793420f1088bdd603 Mon Sep 17 00:00:00 2001 From: David Oliver Date: Wed, 26 Sep 2018 21:44:17 -0400 Subject: [PATCH 5/5] Control - fix ApplyTemplate() Fix bug where Control.ApplyTemplate() would only work when view was attached to the visual tree. UWP only requires that Template be non-null. Restore correct behaviour of Control.CanCreateTemplateWithoutParent. --- .../ControlTests/Given_Control.cs | 68 ++++++++++++------- .../Given_DependencyProperty.Propagation.cs | 2 - .../ItemsControlTests/Given_ItemsControl.cs | 3 - .../UI/Xaml/Controls/Control/Control.cs | 8 +-- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/Uno.UI.Tests/ControlTests/Given_Control.cs b/src/Uno.UI.Tests/ControlTests/Given_Control.cs index 10373c50ce47..143202800244 100644 --- a/src/Uno.UI.Tests/ControlTests/Given_Control.cs +++ b/src/Uno.UI.Tests/ControlTests/Given_Control.cs @@ -13,46 +13,64 @@ namespace Uno.UI.Tests.ControlTests public partial class Given_Control { [TestMethod] - public void When_ManualyApplyTemplate() + public void When_ManuallyApplyTemplate() { - var templatedRoot = default(UIElement); - var sut = new MyControl + var current = FeatureConfiguration.Control.UseLegacyLazyApplyTemplate; + try { - Template = new ControlTemplate(() => templatedRoot = new Grid()) - }; + FeatureConfiguration.Control.UseLegacyLazyApplyTemplate = true; + var templatedRoot = default(UIElement); + var sut = new MyControl + { + Template = new ControlTemplate(() => templatedRoot = new Grid()) + }; - Assert.IsNull(sut.TemplatedRoot); - Assert.IsNull(templatedRoot); + Assert.IsNull(sut.TemplatedRoot); + Assert.IsNull(templatedRoot); - new Grid().Children.Add(sut); // This kind-of simulate that the control is in the visual tree. + new Grid().Children.Add(sut); // This kind-of simulate that the control is in the visual tree. - Assert.IsNull(sut.TemplatedRoot); - Assert.IsNull(templatedRoot); + Assert.IsNull(sut.TemplatedRoot); + Assert.IsNull(templatedRoot); - var applied = sut.ApplyTemplate(); + var applied = sut.ApplyTemplate(); - Assert.IsTrue(applied); - Assert.IsNotNull(sut.TemplatedRoot); - Assert.AreSame(templatedRoot, sut.TemplatedRoot); + Assert.IsTrue(applied); + Assert.IsNotNull(sut.TemplatedRoot); + Assert.AreSame(templatedRoot, sut.TemplatedRoot); + } + finally + { + FeatureConfiguration.Control.UseLegacyLazyApplyTemplate = current; + } } [TestMethod] - public void When_ManualyApplyTemplate_OutOfVisualTree() + public void When_ManuallyApplyTemplate_OutOfVisualTree() { - var templatedRoot = default(UIElement); - var sut = new MyControl + var current = FeatureConfiguration.Control.UseLegacyLazyApplyTemplate; + try { - Template = new ControlTemplate(() => templatedRoot = new Grid()) - }; + FeatureConfiguration.Control.UseLegacyLazyApplyTemplate = true; + var templatedRoot = default(UIElement); + var sut = new MyControl + { + Template = new ControlTemplate(() => templatedRoot = new Grid()) + }; - Assert.IsNull(sut.TemplatedRoot); - Assert.IsNull(templatedRoot); + Assert.IsNull(sut.TemplatedRoot); + Assert.IsNull(templatedRoot); - var applied = sut.ApplyTemplate(); + var applied = sut.ApplyTemplate(); - Assert.IsFalse(applied); - Assert.IsNull(sut.TemplatedRoot); - Assert.IsNull(templatedRoot); + Assert.IsTrue(applied); + Assert.IsNotNull(sut.TemplatedRoot); + Assert.AreSame(templatedRoot, sut.TemplatedRoot); + } + finally + { + FeatureConfiguration.Control.UseLegacyLazyApplyTemplate = current; + } } public partial class MyControl : Control diff --git a/src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.Propagation.cs b/src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.Propagation.cs index eefe61649542..8ffab7683a4f 100644 --- a/src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.Propagation.cs +++ b/src/Uno.UI.Tests/DependencyProperty/Given_DependencyProperty.Propagation.cs @@ -476,8 +476,6 @@ public void When_ControlTemplate_And_Animation() }); SUT.Template = template; - - new Grid().Children.Add(SUT); // This is enough for now, but the `SUT` should be in the visual tree for its template to get applied SUT.ApplyTemplate(); Assert.IsNotNull(anim); diff --git a/src/Uno.UI.Tests/ItemsControlTests/Given_ItemsControl.cs b/src/Uno.UI.Tests/ItemsControlTests/Given_ItemsControl.cs index ad33260aa240..7cae135a1f0e 100644 --- a/src/Uno.UI.Tests/ItemsControlTests/Given_ItemsControl.cs +++ b/src/Uno.UI.Tests/ItemsControlTests/Given_ItemsControl.cs @@ -40,9 +40,6 @@ public void When_EarlyItems() } }; - new Grid().Children.Add(SUT); // This is enough for now, but the `SUT` should be in the visual tree for its template to get applied - SUT.ApplyTemplate(); - // Search on the panel for now, as the name lookup is not properly // aligned on net46. Assert.IsNotNull(panel.FindName("b1")); diff --git a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs index e227ec0d93c2..ad106c75bd89 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs @@ -91,10 +91,10 @@ private void OnTemplateChanged(DependencyPropertyChangedEventArgs e) internal void SetUpdateControlTemplate(bool forceUpdate = false) { if ( - (!FeatureConfiguration.Control.UseLegacyLazyApplyTemplate - || forceUpdate - || CanCreateTemplateWithoutParent) - && this.HasParent() // Instead we should check if we are in a the visual tree + !FeatureConfiguration.Control.UseLegacyLazyApplyTemplate + || forceUpdate + || this.HasParent() + || CanCreateTemplateWithoutParent ) { UpdateTemplate();