Skip to content

Commit

Permalink
feat: Add IndicatorPlacement to TabBar
Browse files Browse the repository at this point in the history
  • Loading branch information
kazo0 committed Apr 12, 2023
1 parent 1a7cd87 commit 82cb82f
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 40 deletions.
Binary file added doc/assets/tabbar-indicator-placement-above.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/assets/tabbar-indicator-placement-below.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
97 changes: 74 additions & 23 deletions doc/controls/TabBarAndTabBarItem.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ SelectionIndicatorContent|object|Gets or sets the content to be displayed as the
SelectionIndicatorContentTemplate|DataTemplate|Gets or sets the data template that is used to display the content of the selection indicator
SelectionIndicatorPresenterStyle|Style|Gets or sets the style to be applied for the content of the `TabBarSelectionIndicatorPresenter`
SelectionIndicatorTransitionMode|IndicatorTransitionMode|Gets or sets the behavior of the selection indicator. The indicator can either slide or snap to the newly selected item. Defaults to `Snap`
SelectionIndicatorPlacement|IndicatorPlacement|Gets or sets the placement of the selection indicator. The indicator can either be displayed `Above` or `Below` the TabBarItem Content. Defaults to `Above`

> Note: `TabBar` only supports the single selection mode.

Expand Down Expand Up @@ -141,29 +142,29 @@ xmlns:utu="using:Uno.Toolkit.UI"
...

<utu:TabBar Style="{StaticResource MyCustomTabBarStyle}"
SelectedIndex="0">
<utu:TabBar.SelectionIndicatorContent>
<Border Height="2"
VerticalAlignment="Bottom"
Background="Red" />
</utu:TabBar.SelectionIndicatorContent>
<utu:TabBar.Items>
<utu:TabBarItem Content="HOME">
<utu:TabBarItem.Icon>
<SymbolIcon Symbol="Home" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
<utu:TabBarItem Content="SUPPORT">
<utu:TabBarItem.Icon>
<FontIcon Glyph="&#xE8F2;" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
<utu:TabBarItem Content="ABOUT">
<utu:TabBarItem.Icon>
<FontIcon Glyph="&#xE946;" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
</utu:TabBar.Items>
SelectedIndex="0">
<utu:TabBar.SelectionIndicatorContent>
<Border Height="2"
VerticalAlignment="Bottom"
Background="Red" />
</utu:TabBar.SelectionIndicatorContent>
<utu:TabBar.Items>
<utu:TabBarItem Content="HOME">
<utu:TabBarItem.Icon>
<SymbolIcon Symbol="Home" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
<utu:TabBarItem Content="SUPPORT">
<utu:TabBarItem.Icon>
<FontIcon Glyph="&#xE8F2;" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
<utu:TabBarItem Content="ABOUT">
<utu:TabBarItem.Icon>
<FontIcon Glyph="&#xE946;" />
</utu:TabBarItem.Icon>
</utu:TabBarItem>
</utu:TabBar.Items>
</utu:TabBar>
```

Expand All @@ -182,6 +183,56 @@ The selection indicator has two different transition modes:

![TabBar with slide selection](../assets/tabbar-selection-slide.gif)

### IndicatorPlacement

By default, the selection indicator is rendered above, on "on top" of, the actual content in the TabBar. For example, the following XAML will result in the screenshot below:

```xml
<utu:TabBar Margin="50"
Width="300"
SelectedIndex="1"
Background="Green"
VerticalAlignment="Center">
<utu:TabBar.Items>
<utu:TabBarItem Content="Tab 1" />
<utu:TabBarItem Content="Tab 2" />
<utu:TabBarItem Content="Tab 3" />
</utu:TabBar.Items>
<utu:TabBar.SelectionIndicatorContentTemplate>
<DataTemplate>
<Border Background="Red" />
</DataTemplate>
</utu:TabBar.SelectionIndicatorContentTemplate>
</utu:TabBar>
```

![](../assets/tabbar-indicator-placement-above.png)

The `SelectionIndicatorPlacement` property can be used to render the selection indicator "below" the content so it does not obscure the `TabBarItem`:

```xml
<utu:TabBar Margin="50"
Width="300"
SelectedIndex="1"
Background="Green"
SelectionIndicatorPlacement="Below"
VerticalAlignment="Center">
<utu:TabBar.Items>
<utu:TabBarItem Content="Tab 1" />
<utu:TabBarItem Content="Tab 2" />
<utu:TabBarItem Content="Tab 3" />
</utu:TabBar.Items>
<utu:TabBar.SelectionIndicatorContentTemplate>
<DataTemplate>
<Border Background="Red" />
</DataTemplate>
</utu:TabBar.SelectionIndicatorContentTemplate>
</utu:TabBar>
```

![](../assets/tabbar-indicator-placement-below.png)


### Further Customization

If further customization is required, such as custom animations when sliding the indicator, there is a `SelectionIndicatorPresenterStyle` property on `TabBar` that can be set to customize the style of the internal `ContentPresenter` that is used to display the selection indicator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
<MaterialColorsV1 xmlns="using:Uno.Material"/>
<MaterialResourcesV1 xmlns="using:Uno.Material"/>
<MaterialToolkitTheme xmlns="using:Uno.Toolkit.UI.Material"
FontOverrideSource="ms-appx:///MaterialFontsOverride.xaml"
ColorOverrideSource="ms-appx:///ColorPaletteOverride.xaml" />

<!-- Load Uno.Cupertino resources -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Microsoft.Extensions.Logging;
using VisualTreeHelperEx = Uno.Toolkit.Samples.Helpers.VisualTreeHelperEx;
using Uno.Toolkit.Samples.Content.NestedSamples;
using Uno.Toolkit.Samples.Content;

namespace Uno.Toolkit.Samples
{
Expand Down
190 changes: 189 additions & 1 deletion src/Uno.Toolkit.RuntimeTests/Tests/TabBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
using Uno.UI.RuntimeTests;
using Uno.UI.Extensions;
using Windows.Foundation;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.UI;
using Windows.Foundation.Metadata;

#if IS_WINUI
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
#else
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml;
using Windows.UI;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
#endif

namespace Uno.Toolkit.RuntimeTests.Tests
Expand Down Expand Up @@ -261,5 +265,189 @@ public async Task Verify_Indicator_Transitions(Orientation orientation, Indicato
};
}
}

[TestMethod]
public async Task Verify_Indicator_Display_On_Selection()
{
if (!CanTakeScreenshot())
{
Assert.Inconclusive(); // System.NotImplementedException: RenderTargetBitmap is not supported on this platform.;
}

const int NumItems = 3;
var SUT = new TabBar
{
Background = new SolidColorBrush(Colors.Transparent),
SelectionIndicatorContentTemplate = XamlHelper.LoadXaml<DataTemplate>(@"
<DataTemplate>
<Border Background=""Red"" />
</DataTemplate>
"),
SelectionIndicatorPlacement = IndicatorPlacement.Above,
};

foreach (var item in Enumerable.Range(0, NumItems).Select(_ => CreateItem()))
{
SUT.Items.Add(item);
}

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);



for (int i = 0; i < NumItems; i++)
{
SUT.SelectedIndex = i;
await UnitTestsUIContentHelper.WaitForIdle();

var selectedItem = SUT.ContainerFromItem(SUT.SelectedItem) as TabBarItem;
Assert.IsNotNull(selectedItem);

var renderer = await TakeScreenshot(SUT);
var centerPoint = selectedItem!.TransformToVisual(SUT).TransformPoint(new Point(selectedItem.ActualWidth / 2, selectedItem.ActualHeight / 2));
var pixel = await GetColorAt(renderer, (int)centerPoint.X, (int)centerPoint.Y);

AssertExpectedColor(Colors.Red, pixel);

foreach (var nonSelected in SUT.Items.Cast<TabBarItem>().Where(x => !x.IsSelected))
{
centerPoint = nonSelected.TransformToVisual(SUT).TransformPoint(new Point(nonSelected.ActualWidth / 2, nonSelected.ActualHeight / 2));
pixel = await GetColorAt(renderer, (int)centerPoint.X, (int)centerPoint.Y);
AssertExpectedColor(Colors.Green, pixel);
}
}

TabBarItem CreateItem()
{
return new TabBarItem
{
Content = new Border
{
Padding = new Thickness(20),
Background = new SolidColorBrush(Colors.Green),
}
};
}
}

[TestMethod]
[DataRow(true, DisplayName = "Verify Indicator Above")]
[DataRow(false, DisplayName = "Verify Indicator Below")]
public async Task Verify_Indicator_Placement(bool isAbove)
{
var item = new TabBarItem
{
Content = new Border
{
Padding = new Thickness(20),
Background = new SolidColorBrush(Colors.Green),
}
};

var SUT = new TabBar
{
Background = new SolidColorBrush(Colors.Transparent),
SelectionIndicatorContentTemplate = XamlHelper.LoadXaml<DataTemplate>(@"
<DataTemplate>
<Border Background=""Red"" />
</DataTemplate>
"),
SelectedIndex = 0,
SelectionIndicatorPlacement = isAbove ? IndicatorPlacement.Above : IndicatorPlacement.Below,
};
SUT.Items.Add(item);

await UnitTestUIContentHelperEx.SetContentAndWait(SUT);

var belowPresenter = VisualTreeHelperEx
.GetFirstDescendant<TabBarSelectionIndicatorPresenter>(SUT, x => x.Name == "BelowSelectionIndicatorPresenter");
var abovePresenter = VisualTreeHelperEx
.GetFirstDescendant<TabBarSelectionIndicatorPresenter>(SUT, x => x.Name == "AboveSelectionIndicatorPresenter");


var renderer = await TakeScreenshot(SUT);

var centerPoint = item.TransformToVisual(SUT).TransformPoint(new Point(item.ActualWidth / 2, item.ActualHeight / 2));
var pixel = await GetColorAt(renderer, (int)centerPoint.X, (int)centerPoint.Y);


if (isAbove)
{
Assert.AreEqual(Visibility.Collapsed, belowPresenter!.Visibility);
Assert.AreEqual(Visibility.Visible, abovePresenter!.Visibility);

if (pixel is { })
{
AssertExpectedColor(Colors.Red, pixel);
}
}
else
{
Assert.AreEqual(Visibility.Visible, belowPresenter!.Visibility);
Assert.AreEqual(Visibility.Collapsed, abovePresenter!.Visibility);

if (pixel is { })
{
AssertExpectedColor(Colors.Green, pixel);
}
}
}

private static void AssertExpectedColor(Color expected, Color? actual)
{
Assert.AreEqual(expected.A, actual?.A);
Assert.AreEqual(expected.R, actual?.R);
Assert.AreEqual(expected.G, actual?.G);
Assert.AreEqual(expected.B, actual?.B);
}

private static async Task<Color?> GetColorAt(RenderTargetBitmap? bitmap, int x, int y)
{
if (bitmap is null)
{
return null;
}

var pixelBuffer = await bitmap.GetPixelsAsync();
var pixels = pixelBuffer.ToArray();
foreach (var p in pixels)
{
Console.WriteLine(p);
}

var offset = (y * bitmap.PixelWidth + x) * 4;
var a = pixels[offset + 3];
var r = pixels[offset + 2];
var g = pixels[offset + 1];
var b = pixels[offset + 0];

return Color.FromArgb(a, r, g, b);
}


private static async Task<RenderTargetBitmap?> TakeScreenshot(UIElement element)
{
if (CanTakeScreenshot())
{
var renderer = new RenderTargetBitmap();
await renderer.RenderAsync(element);
return renderer;
}
else
{
return null;
}
}

private static bool CanTakeScreenshot()
{
return ApiInformation.IsTypePresent(
#if IS_WINUI
"Microsoft.UI.Xaml.Media.Imaging.RenderTargetBitmap"
#else
"Windows.UI.Xaml.Media.Imaging.RenderTargetBitmap"
#endif
);
}
}
}
21 changes: 21 additions & 0 deletions src/Uno.Toolkit.UI/Controls/TabBar/IndicatorPlacement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Uno.Toolkit.UI
{
/// <summary>
/// Determines the placement of the selection indicator in a <see cref="TabBar"/>
/// </summary>
public enum IndicatorPlacement
{
/// <summary>
/// The selection indicator will be placed above the content of the <see cref="TabBar"/>
/// </summary>
Above,
/// <summary>
/// The selection indicator will be placed below, or "behind", the content of the <see cref="TabBar"/>
/// </summary>
Below,
}
}
15 changes: 15 additions & 0 deletions src/Uno.Toolkit.UI/Controls/TabBar/TabBar.Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ public IndicatorTransitionMode SelectionIndicatorTransitionMode
set => SetValue(SelectionIndicatorTransitionModeProperty, value);
}

#endregion
#region DependencyProperty: SelectionIndicatorPlacement

public static DependencyProperty SelectionIndicatorPlacementProperty { get; } = DependencyProperty.Register(
nameof(SelectionIndicatorPlacement),
typeof(IndicatorPlacement),
typeof(TabBar),
new PropertyMetadata(IndicatorPlacement.Above, OnPropertyChanged));

public IndicatorPlacement SelectionIndicatorPlacement
{
get => (IndicatorPlacement)GetValue(SelectionIndicatorPlacementProperty);
set => SetValue(SelectionIndicatorPlacementProperty, value);
}

#endregion

private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
Expand Down
Loading

0 comments on commit 82cb82f

Please sign in to comment.