A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

298 lines
10 KiB

using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
namespace ControlCatalog.Pages;
public partial class NavigationPageCurvedHeaderPage : UserControl
{
static readonly Color Primary = Color.Parse("#137fec");
static readonly Color BgLight = Color.Parse("#f6f7f8");
static readonly Color TextDark = Color.Parse("#111827");
static readonly Color TextMuted = Color.Parse("#64748b");
const double DomeH = 32.0;
const double HomeHeaderFlatH = 130.0;
const double ProfileHeaderFlatH = 110.0;
const double AvatarHomeSize = 72.0;
const double AvatarProfileSize = 88.0;
NavigationPage? _navPage;
ScrollViewer? _infoPanel;
public NavigationPageCurvedHeaderPage()
{
InitializeComponent();
_navPage = this.FindControl<NavigationPage>("NavPage");
if (_navPage != null)
_ = _navPage.PushAsync(BuildHomePage());
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
_infoPanel = this.FindControl<ScrollViewer>("InfoPanel");
UpdateInfoPanelVisibility();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BoundsProperty)
UpdateInfoPanelVisibility();
}
void UpdateInfoPanelVisibility()
{
if (_infoPanel != null)
_infoPanel.IsVisible = Bounds.Width >= 650;
}
const string AssetBase = "avares://ControlCatalog/Assets/CurvedHeader/";
static Bitmap? LoadImg(string name)
{
try { return new Bitmap(AssetLoader.Open(new Uri(AssetBase + name))); }
catch { return null; }
}
static IBrush ImgBrush(string name, IBrush fallback)
{
var bmp = LoadImg(name);
return bmp != null ? new ImageBrush(bmp) { Stretch = Stretch.UniformToFill } : fallback;
}
ContentPage BuildHomePage()
{
var bgPath = new Path { Fill = new SolidColorBrush(Colors.White) };
var headerContent = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Spacing = 4,
Margin = new Thickness(24, 20, 24, 0),
Children =
{
new TextBlock
{
Text = "Welcome back, Alex",
FontSize = 20,
FontWeight = FontWeight.Bold,
Foreground = new SolidColorBrush(TextDark),
TextAlignment = TextAlignment.Center,
},
new TextBlock
{
Text = "Ready to explore?",
FontSize = 14,
Foreground = new SolidColorBrush(TextMuted),
TextAlignment = TextAlignment.Center,
},
new TextBox
{
Margin = new Thickness(0, 10, 0, 0),
PlaceholderText = "Search for products...",
CornerRadius = new CornerRadius(999),
Height = 40,
Padding = new Thickness(16, 10),
FontSize = 14,
TextAlignment = TextAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Background = new SolidColorBrush(Color.Parse("#f1f5f9")),
BorderThickness = new Thickness(0),
Styles =
{
new Style(x => x.OfType<TextBox>().Template().OfType<TextBlock>().Name("PART_PlaceholderText"))
{
Setters =
{
new Setter(TextBlock.TextAlignmentProperty, TextAlignment.Center),
new Setter(TextBlock.HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
}
}
}
},
}
};
var avatar = new Ellipse
{
Width = AvatarHomeSize,
Height = AvatarHomeSize,
Fill = ImgBrush("avatar.jpg", new SolidColorBrush(Color.Parse("#93c5fd"))),
};
var avatarRing = new Ellipse
{
Width = AvatarHomeSize + 6,
Height = AvatarHomeSize + 6,
Stroke = Brushes.White,
StrokeThickness = 3,
Fill = Brushes.Transparent,
};
var headerPanel = new Panel { VerticalAlignment = VerticalAlignment.Top };
var homeScroll = new CurvedHeaderHomeScrollView
{
NavigateRequested = async () => { if (_navPage != null) await _navPage.PushAsync(BuildProfilePage()); },
};
headerPanel.SizeChanged += (_, args) =>
{
double w = args.NewSize.Width;
if (w <= 1) return;
bgPath.Data = BuildDomeGeometry(w, HomeHeaderFlatH, DomeH);
double domeTipY = HomeHeaderFlatH + DomeH;
Canvas.SetLeft(avatar, (w - AvatarHomeSize) / 2);
Canvas.SetTop(avatar, domeTipY - AvatarHomeSize / 2);
Canvas.SetLeft(avatarRing, (w - (AvatarHomeSize + 6)) / 2);
Canvas.SetTop(avatarRing, domeTipY - (AvatarHomeSize + 6) / 2);
};
var avatarCanvas = new Canvas { IsHitTestVisible = true, Cursor = new Cursor(StandardCursorType.Hand) };
avatarCanvas.Children.Add(avatarRing);
avatarCanvas.Children.Add(avatar);
avatarCanvas.PointerReleased += async (_, _) => { if (_navPage != null) await _navPage.PushAsync(BuildProfilePage()); };
headerPanel.Children.Add(bgPath);
headerPanel.Children.Add(headerContent);
headerPanel.Children.Add(avatarCanvas);
var root = new Panel();
root.Children.Add(homeScroll);
root.Children.Add(headerPanel);
var page = new ContentPage
{
Background = new SolidColorBrush(BgLight),
Content = root,
};
NavigationPage.SetHasNavigationBar(page, false);
return page;
}
ContentPage BuildProfilePage()
{
var bgPath = new Path
{
Fill = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop(Color.Parse("#137fec"), 0),
new GradientStop(Color.Parse("#0a4fa8"), 1),
}
}
};
var profileContent = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
Spacing = 3,
Margin = new Thickness(24, 52, 24, 0),
Children =
{
new TextBlock
{
Text = "Alex Johnson",
FontSize = 20,
FontWeight = FontWeight.Bold,
Foreground = Brushes.White,
TextAlignment = TextAlignment.Center,
},
new TextBlock
{
Text = "UI/UX Designer · San Francisco",
FontSize = 13,
Foreground = new SolidColorBrush(Color.FromArgb(210, 255, 255, 255)),
TextAlignment = TextAlignment.Center,
},
}
};
var avatar = new Ellipse
{
Width = AvatarProfileSize,
Height = AvatarProfileSize,
Fill = ImgBrush("avatar.jpg", new SolidColorBrush(Color.Parse("#60a5fa"))),
};
var avatarRing = new Ellipse
{
Width = AvatarProfileSize + 6,
Height = AvatarProfileSize + 6,
Stroke = Brushes.White,
StrokeThickness = 3,
Fill = Brushes.Transparent,
};
var headerPanel = new Panel { VerticalAlignment = VerticalAlignment.Top };
var profileScroll = new CurvedHeaderProfileScrollView();
headerPanel.SizeChanged += (_, args) =>
{
double w = args.NewSize.Width;
if (w <= 1) return;
bgPath.Data = BuildDomeGeometry(w, ProfileHeaderFlatH, DomeH);
double tipY = ProfileHeaderFlatH + DomeH;
Canvas.SetLeft(avatar, (w - AvatarProfileSize) / 2);
Canvas.SetTop(avatar, tipY - AvatarProfileSize / 2);
Canvas.SetLeft(avatarRing, (w - (AvatarProfileSize + 6)) / 2);
Canvas.SetTop(avatarRing, tipY - (AvatarProfileSize + 6) / 2);
};
var avatarCanvas = new Canvas { IsHitTestVisible = false };
avatarCanvas.Children.Add(avatarRing);
avatarCanvas.Children.Add(avatar);
headerPanel.Children.Add(bgPath);
headerPanel.Children.Add(profileContent);
headerPanel.Children.Add(avatarCanvas);
var root = new Panel();
root.Children.Add(profileScroll);
root.Children.Add(headerPanel);
var page = new ContentPage
{
Background = new SolidColorBrush(BgLight),
Content = root,
};
NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay);
return page;
}
static StreamGeometry BuildDomeGeometry(double w, double flatH, double domeH)
{
var sg = new StreamGeometry();
using var ctx = sg.Open();
ctx.BeginFigure(new Point(0, 0), isFilled: true);
ctx.LineTo(new Point(w, 0));
ctx.LineTo(new Point(w, flatH));
ctx.ArcTo(
new Point(0, flatH),
new Size(w / 2, domeH),
rotationAngle: 0,
isLargeArc: false,
sweepDirection: SweepDirection.Clockwise);
ctx.EndFigure(true);
return sg;
}
}