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

在Xamarin/Maui中获取页面内View位置,优化下拉列表显示

实现自适应位置的下拉选择器

我正在自定义一个下拉选择器(基于CollectionView实现),需求是:

  • 当选择器位于页面底部时,下拉列表显示在选中项上方
  • 当选择器不在底部时,下拉列表正常显示在选中项下方
    当前问题:选择器在页面底部时,用户点击选中项后,下拉列表会超出页面范围,无法访问选项。

点击选中项时的列表

原实现代码

XAML布局

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Name="ComboBoxView"
             BindingContext="{x:Reference ComboBoxView}"
             x:Class="MauiApp1.ComboBoxNew">

    <Grid RowDefinitions="Auto,*" >
        <Frame HasShadow="False" Padding="5" CornerRadius="0" BorderColor="Gray" Grid.Row="0">
            <Grid Padding="5" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ColumnDefinitions="75*,25*" >
                <Label Text="{Binding Source={Reference ComboBoxView}, Path=SelectedItem }" Grid.Column="0" HorizontalOptions="FillAndExpand" VerticalOptions="Fill" />
                <ImageButton Source="arrowdown" Grid.Column="1" HorizontalOptions="EndAndExpand" VerticalOptions="CenterAndExpand" HeightRequest="25" WidthRequest="25" ></ImageButton>
            </Grid>
            <Frame.GestureRecognizers>
                <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="OpenListItemsClickedEvent" />
            </Frame.GestureRecognizers>
        </Frame>
        <Grid Grid.Row="1" x:Name="ComboElementsList" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" RowDefinitions="Auto,*" >
            <Image Grid.Row="0" HeightRequest="15" WidthRequest="15" Source="arrowup" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ></Image>
            <CollectionView Grid.Row="1" ItemsSource="{Binding Source={Reference ComboBoxView},Path=ItemSource}" Unfocused="ListUnfocusedEvent" x:Name="ComboListElements" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" HeightRequest="40" >
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Frame CornerRadius="0" Padding="15" HasShadow="True" BorderColor="Gray">
                            <Label Text="{Binding .}" />
                            <Frame.GestureRecognizers>
                                <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="ItemSelectedFromListViewEvent" CommandParameter="{Binding .}" />
                            </Frame.GestureRecognizers>
                        </Frame>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </Grid>
    </Grid>
</ContentView>

C#逻辑

using System.Collections.ObjectModel;

namespace MauiApp1;

public partial class ComboBoxNew : ContentView
{
    public ComboBoxNew()
    {
        InitializeComponent();
        ComboListElements.Opacity = 0;
        ComboElementsList.Opacity = 0;
    }

    protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
    {
        return base.OnMeasure(widthConstraint, heightConstraint);
    }

    public static readonly BindableProperty ItemSourceProperty = BindableProperty.Create("ItemSource", typeof(ObservableCollection<string>), typeof(ObservableCollection<string>), null, BindingMode.TwoWay, propertyChanged: ItemSourceChangedEvent);

    public ObservableCollection<string> ItemSource
    {
        get => (ObservableCollection<string>)GetValue(ItemSourceProperty);
        set => SetValue(ItemSourceProperty, value);
    }

    public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create("SelectedItem", typeof(string), typeof(string), string.Empty, BindingMode.TwoWay);
    public string SelectedItem 
    {
        get => (string)GetValue(SelectedItemProperty);
        set => SetValue(SelectedItemProperty, value);
    }

    private void OpenListItemsClickedEvent(object sender, EventArgs e)
    {
        ComboListElements.FadeTo(1, 500, Easing.Linear);
        ComboElementsList.FadeTo(1, 500, Easing.Linear);
        var Xitem = ComboListElements.X;
        var Yitem = ComboListElements.Y;

    }

    private void ListUnfocusedEvent(object sender, FocusEventArgs e)
    {
        ComboListElements.FadeTo(0, 500, Easing.Linear);
        ComboElementsList.FadeTo(0, 500, Easing.Linear);
    }

    private void ItemSelectedFromListViewEvent(object sender, EventArgs e)
    {
        var clickedValue = ((e as TappedEventArgs).Parameter) as string;
        SelectedItem = clickedValue;
        ComboListElements.FadeTo(0, 500, Easing.Linear);
        ComboElementsList.FadeTo(0, 500, Easing.Linear);
    }

    private static void ItemSourceChangedEvent(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ComboBoxNew comboBoxNew && newValue != null && newValue is ObservableCollection<string> newItemSource) 
        {
            comboBoxNew.SelectedItem = newItemSource.FirstOrDefault();
        }
    }
}

解决方案

要实现自适应位置的下拉列表,核心是计算选择器在页面中的绝对位置,判断下方剩余空间是否足够展开列表,再动态调整列表的布局位置。

1. 修改XAML布局

改用AbsoluteLayout控制下拉列表的位置,使其能灵活切换显示在选中项的上方或下方:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Name="ComboBoxView"
             BindingContext="{x:Reference ComboBoxView}"
             x:Class="MauiApp1.ComboBoxNew">

    <AbsoluteLayout>
        <!-- 选中项显示区域 -->
        <Frame x:Name="SelectedItemFrame" HasShadow="False" Padding="5" CornerRadius="0" BorderColor="Gray"
               AbsoluteLayout.LayoutBounds="0,0,1,Auto" AbsoluteLayout.LayoutFlags="WidthProportional">
            <Grid Padding="5" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ColumnDefinitions="75*,25*" >
                <Label Text="{Binding SelectedItem}" Grid.Column="0" HorizontalOptions="FillAndExpand" VerticalOptions="Fill" />
                <ImageButton Source="arrowdown" Grid.Column="1" HorizontalOptions="EndAndExpand" VerticalOptions="CenterAndExpand" HeightRequest="25" WidthRequest="25" ></ImageButton>
            </Grid>
            <Frame.GestureRecognizers>
                <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="OpenListItemsClickedEvent" />
            </Frame.GestureRecognizers>
        </Frame>

        <!-- 下拉列表区域 -->
        <Grid x:Name="ComboElementsList" HorizontalOptions="FillAndExpand" 
              AbsoluteLayout.LayoutBounds="0,0,1,Auto" AbsoluteLayout.LayoutFlags="WidthProportional"
              Opacity="0">
            <Image x:Name="ArrowImage" Grid.Row="0" HeightRequest="15" WidthRequest="15" Source="arrowdown" HorizontalOptions="Center" VerticalOptions="FillAndExpand" ></Image>
            <CollectionView Grid.Row="1" ItemsSource="{Binding ItemSource}" Unfocused="ListUnfocusedEvent" x:Name="ComboListElements" 
                            HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" HeightRequest="160" >
                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <Frame CornerRadius="0" Padding="15" HasShadow="True" BorderColor="Gray">
                            <Label Text="{Binding .}" />
                            <Frame.GestureRecognizers>
                                <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="ItemSelectedFromListViewEvent" CommandParameter="{Binding .}" />
                            </Frame.GestureRecognizers>
                        </Frame>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </Grid>
    </AbsoluteLayout>
</ContentView>

2. 更新C#逻辑,添加位置计算

在打开列表的事件中,计算页面剩余空间,动态调整列表位置:

using System.Collections.ObjectModel;
using Microsoft.Maui.Controls;

namespace MauiApp1;

public partial class ComboBoxNew : ContentView
{
    public ComboBoxNew()
    {
        InitializeComponent();
    }

    public static readonly BindableProperty ItemSourceProperty = BindableProperty.Create(
        nameof(ItemSource), 
        typeof(ObservableCollection<string>), 
        typeof(ComboBoxNew), 
        null, 
        BindingMode.TwoWay, 
        propertyChanged: ItemSourceChangedEvent);

    public ObservableCollection<string> ItemSource
    {
        get => (ObservableCollection<string>)GetValue(ItemSourceProperty);
        set => SetValue(ItemSourceProperty, value);
    }

    public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(
        nameof(SelectedItem), 
        typeof(string), 
        typeof(ComboBoxNew), 
        string.Empty, 
        BindingMode.TwoWay);

    public string SelectedItem 
    {
        get => (string)GetValue(SelectedItemProperty);
        set => SetValue(SelectedItemProperty, value);
    }

    private void OpenListItemsClickedEvent(object sender, EventArgs e)
    {
        // 获取选择器在页面中的绝对位置和尺寸
        var frameBounds = SelectedItemFrame.Bounds;
        var absolutePos = SelectedItemFrame.TranslateToParent(new Point(frameBounds.X, frameBounds.Y));
        var screenHeight = Application.Current.MainPage.Window.Height;
        
        // 计算下拉列表所需总高度
        var requiredHeight = ComboListElements.HeightRequest + ArrowImage.HeightRequest;
        // 计算选择器下方剩余空间
        var availableSpaceBelow = screenHeight - (absolutePos.Y + frameBounds.Height);

        // 判断是否需要向上展开列表
        bool shouldShowAbove = availableSpaceBelow < requiredHeight;

        // 设置列表位置
        if (shouldShowAbove)
        {
            AbsoluteLayout.SetLayoutBounds(ComboElementsList, new Rect(0, absolutePos.Y - requiredHeight, 1, requiredHeight));
            ArrowImage.Source = "arrowup";
        }
        else
        {
            AbsoluteLayout.SetLayoutBounds(ComboElementsList, new Rect(0, absolutePos.Y + frameBounds.Height, 1, requiredHeight));
            ArrowImage.Source = "arrowdown";
        }

        ComboElementsList.FadeTo(1, 200, Easing.Linear);
    }

    private void ListUnfocusedEvent(object sender, FocusEventArgs e)
    {
        ComboElementsList.FadeTo(0, 200, Easing.Linear);
    }

    private void ItemSelectedFromListViewEvent(object sender, EventArgs e)
    {
        if (e is TappedEventArgs tappedArgs && tappedArgs.Parameter is string clickedValue)
        {
            SelectedItem = clickedValue;
            ComboElementsList.FadeTo(0, 200, Easing.Linear);
        }
    }

    private static void ItemSourceChangedEvent(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ComboBoxNew comboBoxNew && newValue is ObservableCollection<string> newItemSource && newItemSource.Any())
        {
            comboBoxNew.SelectedItem = newItemSource.FirstOrDefault();
        }
    }
}

关键改动说明

  • 用AbsoluteLayout替代原Grid,让下拉列表可以自由定位
  • 计算选择器的绝对位置和页面剩余空间,自动判断列表展开方向
  • 根据展开方向切换箭头图标,优化视觉提示
  • 缩短动画时长,提升交互流畅度

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 22:15:26