You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xamarin Form PCL实现圆形菜单点击后移至底部并展开页面

嘿,这个圆形菜单的交互需求挺有意思的,我之前做过类似的效果,给你梳理下具体的实现思路和关键代码要点,应该能帮到你:

核心实现思路拆解

1. 布局基础:用绝对布局承载圆形菜单

首先,你需要一个能精确定位子视图的布局,AbsoluteLayout是最合适的选择——它能让你把每个菜单项(按钮/自定义View)放在圆形轨迹的指定坐标上。把所有菜单项都放在一个容器里,方便后续统一做旋转动画。

2. 初始定位:计算每个菜单项的圆形坐标

8个菜单项均匀分布在圆上,每个项之间的角度差是360°/8=45°。你需要在页面尺寸确定后(比如OnSizeChanged事件里)计算圆心位置和合适的半径,然后用三角函数算出每个菜单项的坐标:

  • 圆心设为页面的中心位置:(Width/2, Height/2)
  • 半径建议取屏幕较短边的1/3左右,保证菜单不会超出屏幕
  • 每个菜单项的坐标公式(注意角度转弧度):
    弧度 = 角度 × π / 180
    X = 圆心X + 半径 × Cos(弧度)
    Y = 圆心Y + 半径 × Sin(弧度)
    
    这里要注意Xamarin Forms的Y轴是向下的,所以Sin计算出来的正方向正好对应屏幕下方,符合我们的视觉习惯。

3. 点击动画:让菜单沿圆形轨迹旋转到底部

这是核心部分,不用单独移动每个菜单项,而是给整个菜单容器做旋转动画——设置容器的AnchorX和AnchorY为圆心相对于容器的比例,这样旋转时所有菜单项会自动沿圆形轨迹移动:

  • 点击某个菜单项时,先算出它的初始角度(比如第0个项是0°,第1个是45°,以此类推)
  • 我们需要让点击的项最终停在底部,对应的绝对角度是270°(数学坐标系里270°对应正下方)
  • 计算容器需要旋转到的目标角度:目标旋转角度 = 270° - 菜单项初始角度
  • 用Xamarin Forms的Animation类创建平滑的旋转动画,设置合适的时长(比如300ms)和缓动函数(比如Easing.CubicInOut),让旋转更自然

4. 页面跳转:动画完成后展开对应页面

等旋转动画结束后,再触发页面跳转逻辑——可以用Task.Delay等待动画时长,或者监听动画完成事件,然后根据点击的菜单项索引跳转到对应的页面(比如Navigation.PushAsync,或者切换Tab页)


关键代码示例

XAML布局

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="YourApp.MainPage">
    <AbsoluteLayout x:Name="MenuContainer" BackgroundColor="#f5f5f5">
        <!-- 8个菜单项,这里用Button示例,你可以换成自定义View -->
        <Button x:Name="MenuItem0" Text="首页" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem1" Text="消息" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem2" Text="发现" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem3" Text="我的" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem4" Text="设置" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem5" Text="收藏" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem6" Text="历史" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
        <Button x:Name="MenuItem7" Text="帮助" Clicked="OnMenuItemClicked" WidthRequest="60" HeightRequest="60" CornerRadius="30"/>
    </AbsoluteLayout>
</ContentPage>

后台逻辑代码

using System;
using Xamarin.Forms;

namespace YourApp
{
    public partial class MainPage : ContentPage
    {
        private const int MenuItemCount = 8;
        private double _menuRadius;
        private Point _menuCenter;
        private Button[] _menuItems;

        public MainPage()
        {
            InitializeComponent();
            _menuItems = new[] { MenuItem0, MenuItem1, MenuItem2, MenuItem3, MenuItem4, MenuItem5, MenuItem6, MenuItem7 };
        }

        protected override void OnSizeChanged(double width, double height)
        {
            base.OnSizeChanged(width, height);
            // 计算圆心和半径
            _menuCenter = new Point(width / 2, height / 2);
            _menuRadius = Math.Min(width, height) / 3;
            // 设置菜单容器的旋转锚点为圆心
            MenuContainer.AnchorX = _menuCenter.X / width;
            MenuContainer.AnchorY = _menuCenter.Y / height;
            // 初始化菜单项位置
            PositionMenuItems();
        }

        private void PositionMenuItems()
        {
            for (int i = 0; i < MenuItemCount; i++)
            {
                // 计算当前项的角度(从0°开始,每次加45°)
                var angle = i * 360.0 / MenuItemCount;
                var radian = angle * Math.PI / 180;
                // 计算项的中心坐标
                var itemCenterX = _menuCenter.X + _menuRadius * Math.Cos(radian);
                var itemCenterY = _menuCenter.Y + _menuRadius * Math.Sin(radian);
                // 设置项的位置(AbsoluteLayout需要左上角坐标,所以减去一半尺寸)
                AbsoluteLayout.SetLayoutBounds(_menuItems[i], new Rectangle(
                    itemCenterX - _menuItems[i].WidthRequest / 2,
                    itemCenterY - _menuItems[i].HeightRequest / 2,
                    AbsoluteLayout.AutoSize,
                    AbsoluteLayout.AutoSize));
            }
        }

        private async void OnMenuItemClicked(object sender, EventArgs e)
        {
            var clickedItem = sender as Button;
            var itemIndex = Array.IndexOf(_menuItems, clickedItem);
            if (itemIndex == -1) return;

            // 计算目标旋转角度,让点击的项停在270°(底部)
            var initialItemAngle = itemIndex * 360.0 / MenuItemCount;
            var targetRotation = 270 - initialItemAngle;

            // 优化旋转路径:取最短的旋转方向(比如需要转300°的话,反向转60°更快)
            var rotationDiff = targetRotation - MenuContainer.Rotation;
            if (Math.Abs(rotationDiff) > 180)
            {
                targetRotation = rotationDiff > 0 ? targetRotation - 360 : targetRotation + 360;
            }

            // 创建并执行旋转动画
            var rotationAnimation = new Animation(v => MenuContainer.Rotation = v, MenuContainer.Rotation, targetRotation, Easing.CubicInOut);
            rotationAnimation.Commit(this, "MenuRotateAnimation", length: 300);

            // 等待动画完成后跳转页面
            await Task.Delay(300);
            NavigateToPage(itemIndex);
        }

        private async void NavigateToPage(int itemIndex)
        {
            switch (itemIndex)
            {
                case 0:
                    await Navigation.PushAsync(new HomePage());
                    break;
                case 1:
                    await Navigation.PushAsync(new MessagesPage());
                    break;
                case 2:
                    await Navigation.PushAsync(new DiscoverPage());
                    break;
                case 3:
                    await Navigation.PushAsync(new ProfilePage());
                    break;
                case 4:
                    await Navigation.PushAsync(new SettingsPage());
                    break;
                case 5:
                    await Navigation.PushAsync(new FavoritesPage());
                    break;
                case 6:
                    await Navigation.PushAsync(new HistoryPage());
                    break;
                case 7:
                    await Navigation.PushAsync(new HelpPage());
                    break;
            }
        }
    }
}

额外优化建议

  • 最短路径旋转:上面的代码已经加了这个逻辑,避免菜单做超长距离的旋转,提升交互体验
  • 自定义菜单项:把Button换成自定义View,比如圆形图片+文字,让菜单更美观
  • 屏幕适配:在OnSizeChanged里重新计算位置,确保旋转屏幕后菜单依然正常显示
  • 动画状态锁定:可以加一个布尔变量,防止动画执行中重复点击菜单项导致混乱

内容的提问来源于stack exchange,提问作者Sanal P

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:47:09