// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform; /// /// A page that displays tabs for navigation between child pages. /// public class SkiaTabbedPage : SkiaLayoutView { private readonly List _tabs = new(); private int _selectedIndex = 0; private float _tabBarHeight = 48f; private bool _tabBarOnBottom = false; // SKColor fields for rendering private SKColor _tabBarBackgroundColorSK = SkiaTheme.PrimarySK; private SKColor _selectedTabColorSK = SKColors.White; private SKColor _unselectedTabColorSK = SkiaTheme.WhiteSemiTransparentSK; private SKColor _indicatorColorSK = SKColors.White; // MAUI Color backing fields private Color _tabBarBackgroundColor = Color.FromRgb(33, 150, 243); private Color _selectedTabColor = Colors.White; private Color _unselectedTabColor = Color.FromRgba(255, 255, 255, 180); private Color _indicatorColor = Colors.White; /// /// Gets or sets the height of the tab bar. /// public float TabBarHeight { get => _tabBarHeight; set { if (_tabBarHeight != value) { _tabBarHeight = value; InvalidateMeasure(); Invalidate(); } } } /// /// Gets or sets whether the tab bar is positioned at the bottom. /// public bool TabBarOnBottom { get => _tabBarOnBottom; set { if (_tabBarOnBottom != value) { _tabBarOnBottom = value; Invalidate(); } } } /// /// Gets or sets the selected tab index. /// public int SelectedIndex { get => _selectedIndex; set { if (value >= 0 && value < _tabs.Count && _selectedIndex != value) { _selectedIndex = value; SelectedIndexChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } } } /// /// Gets the currently selected tab. /// public TabItem? SelectedTab => _selectedIndex >= 0 && _selectedIndex < _tabs.Count ? _tabs[_selectedIndex] : null; /// /// Gets the tabs in this page. /// public IReadOnlyList Tabs => _tabs; /// /// Background color for the tab bar. /// public Color TabBarBackgroundColor { get => _tabBarBackgroundColor; set { _tabBarBackgroundColor = value; _tabBarBackgroundColorSK = value.ToSKColor(); Invalidate(); } } /// /// Color for selected tab text/icon. /// public Color SelectedTabColor { get => _selectedTabColor; set { _selectedTabColor = value; _selectedTabColorSK = value.ToSKColor(); Invalidate(); } } /// /// Color for unselected tab text/icon. /// public Color UnselectedTabColor { get => _unselectedTabColor; set { _unselectedTabColor = value; _unselectedTabColorSK = value.ToSKColor(); Invalidate(); } } /// /// Color of the selection indicator. /// public Color IndicatorColor { get => _indicatorColor; set { _indicatorColor = value; _indicatorColorSK = value.ToSKColor(); Invalidate(); } } /// /// Height of the selection indicator. /// public float IndicatorHeight { get; set; } = 3f; /// /// Event raised when the selected index changes. /// public event EventHandler? SelectedIndexChanged; /// /// Adds a tab with the specified title and content. /// public void AddTab(string title, SkiaView content, string? iconPath = null) { var tab = new TabItem { Title = title, Content = content, IconPath = iconPath }; _tabs.Add(tab); AddChild(content); if (_tabs.Count == 1) { _selectedIndex = 0; } InvalidateMeasure(); Invalidate(); } /// /// Removes a tab at the specified index. /// public void RemoveTab(int index) { if (index >= 0 && index < _tabs.Count) { var tab = _tabs[index]; _tabs.RemoveAt(index); RemoveChild(tab.Content); if (_selectedIndex >= _tabs.Count) { _selectedIndex = Math.Max(0, _tabs.Count - 1); } InvalidateMeasure(); Invalidate(); } } /// /// Clears all tabs. /// public void ClearTabs() { foreach (var tab in _tabs) { RemoveChild(tab.Content); } _tabs.Clear(); _selectedIndex = 0; InvalidateMeasure(); Invalidate(); } protected override Size MeasureOverride(Size availableSize) { // Measure the content area (excluding tab bar) var contentHeight = availableSize.Height - TabBarHeight; var contentSize = new Size(availableSize.Width, contentHeight); foreach (var tab in _tabs) { tab.Content.Measure(contentSize); } return availableSize; } protected override Rect ArrangeOverride(Rect bounds) { // Calculate content bounds based on tab bar position Rect contentBounds; if (TabBarOnBottom) { contentBounds = new Rect( bounds.Left, bounds.Top, bounds.Width, bounds.Height - TabBarHeight); } else { contentBounds = new Rect( bounds.Left, bounds.Top + TabBarHeight, bounds.Width, bounds.Height - TabBarHeight); } // Arrange each tab's content to fill the content area foreach (var tab in _tabs) { tab.Content.Arrange(contentBounds); } return bounds; } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { canvas.Save(); canvas.ClipRect(bounds); // Draw tab bar background DrawTabBar(canvas); // Draw selected content if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) { _tabs[_selectedIndex].Content.Draw(canvas); } canvas.Restore(); } private void DrawTabBar(SKCanvas canvas) { // Calculate tab bar bounds SKRect tabBarBounds; if (TabBarOnBottom) { tabBarBounds = new SKRect( (float)Bounds.Left, (float)Bounds.Bottom - TabBarHeight, (float)Bounds.Right, (float)Bounds.Bottom); } else { tabBarBounds = new SKRect( (float)Bounds.Left, (float)Bounds.Top, (float)Bounds.Right, (float)Bounds.Top + TabBarHeight); } // Draw background using var bgPaint = new SKPaint { Color = _tabBarBackgroundColorSK, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawRect(tabBarBounds, bgPaint); if (_tabs.Count == 0) return; // Calculate tab width float tabWidth = tabBarBounds.Width / _tabs.Count; // Draw tabs using var textPaint = new SKPaint { IsAntialias = true, TextSize = 14f, Typeface = SKTypeface.Default }; for (int i = 0; i < _tabs.Count; i++) { var tab = _tabs[i]; var tabBounds = new SKRect( tabBarBounds.Left + i * tabWidth, tabBarBounds.Top, tabBarBounds.Left + (i + 1) * tabWidth, tabBarBounds.Bottom); bool isSelected = i == _selectedIndex; textPaint.Color = isSelected ? _selectedTabColorSK : _unselectedTabColorSK; textPaint.FakeBoldText = isSelected; // Draw tab title centered var textBounds = new SKRect(); textPaint.MeasureText(tab.Title, ref textBounds); float textX = tabBounds.MidX - textBounds.MidX; float textY = tabBounds.MidY - textBounds.MidY; canvas.DrawText(tab.Title, textX, textY, textPaint); } // Draw selection indicator if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) { using var indicatorPaint = new SKPaint { Color = _indicatorColorSK, Style = SKPaintStyle.Fill, IsAntialias = true }; float indicatorLeft = tabBarBounds.Left + _selectedIndex * tabWidth; float indicatorTop = TabBarOnBottom ? tabBarBounds.Top : tabBarBounds.Bottom - IndicatorHeight; var indicatorRect = new SKRect( indicatorLeft, indicatorTop, indicatorLeft + tabWidth, indicatorTop + IndicatorHeight); canvas.DrawRect(indicatorRect, indicatorPaint); } } public override SkiaView? HitTest(float x, float y) { if (!IsVisible || !Bounds.Contains(x, y)) return null; // Check if hit is in tab bar SKRect tabBarBounds; if (TabBarOnBottom) { tabBarBounds = new SKRect( (float)Bounds.Left, (float)(Bounds.Top + Bounds.Height) - TabBarHeight, (float)(Bounds.Left + Bounds.Width), (float)(Bounds.Top + Bounds.Height)); } else { tabBarBounds = new SKRect( (float)Bounds.Left, (float)Bounds.Top, (float)(Bounds.Left + Bounds.Width), (float)Bounds.Top + TabBarHeight); } if (tabBarBounds.Contains(x, y)) { return this; // Tab bar handles its own hits } // Check selected content if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) { var hit = _tabs[_selectedIndex].Content.HitTest(x, y); if (hit != null) return hit; } return this; } public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; // Check if click is in tab bar SKRect tabBarBounds; if (TabBarOnBottom) { tabBarBounds = new SKRect( (float)Bounds.Left, (float)(Bounds.Top + Bounds.Height) - TabBarHeight, (float)(Bounds.Left + Bounds.Width), (float)(Bounds.Top + Bounds.Height)); } else { tabBarBounds = new SKRect( (float)Bounds.Left, (float)Bounds.Top, (float)(Bounds.Left + Bounds.Width), (float)Bounds.Top + TabBarHeight); } if (tabBarBounds.Contains(e.X, e.Y) && _tabs.Count > 0) { // Calculate which tab was clicked float tabWidth = tabBarBounds.Width / _tabs.Count; int clickedIndex = (int)((e.X - tabBarBounds.Left) / tabWidth); clickedIndex = Math.Clamp(clickedIndex, 0, _tabs.Count - 1); SelectedIndex = clickedIndex; e.Handled = true; } base.OnPointerPressed(e); } }