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

.NET MAUI中X轴自定义动画:侧边栏宽度切换实现难题

问题描述

初始状态与目标状态对比:
初始状态 ---> 目标状态

需要实现点击按钮时修改名为trackListSideBar的Frame元素宽度,但尝试缩放动画时会同时缩放内部内容,无法达到预期效果。以下是当前的XAML代码:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:converters="clr-namespace:Solution.MobileApp.Converters"
             xmlns:viewModels="clr-namespace:Solution.MobileApp.ViewModels"
             x:Class="Solution.MobileApp.Pages.Tabs.PlayerPage"
             x:DataType="viewModels:PlayerPageViewModel"
             Title="wPlayer"
             BackgroundColor="Black"
             Shell.NavBarIsVisible="False"
             Unloaded="OnUnloaded">
  <ContentPage.Resources>
    <toolkit:TimeSpanToSecondsConverter x:Key="TimeSpanConverter" />
    <converters:SecondsToStringConverter x:Key="SecondsToStringConverter" />
  </ContentPage.Resources>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="0" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="1*"/>
      <ColumnDefinition Width="5*"/>
    </Grid.ColumnDefinitions>

    <Frame Grid.Row="0" Grid.RowSpan="5" Grid.Column="0"
           ZIndex="100"
           HeightRequest="600"
           x:Name="trackListSideBar">
      <ImageButton Source="song.png" Clicked="OnTrackListImageClicked" WidthRequest="50" HeightRequest="50" />
    </Frame>

    <toolkit:MediaElement
        Grid.Row="0"
        Grid.Column="1"
        x:Name="mediaElement"
        ShouldAutoPlay="True"
        Source="{Binding Source}"
        MediaEnded="OnMediaEnded"
        MediaFailed="OnMediaFailed"
        MediaOpened="OnMediaOpened"
        PositionChanged="OnPositionChanged"
        StateChanged="OnStateChanged"
        SeekCompleted="OnSeekCompleted" />

    <HorizontalStackLayout Grid.Row="1" Grid.Column="1" Padding="0,0,0,15">
      <Label HorizontalOptions="Center">
        <Label.Text>
          <MultiBinding StringFormat="Current State: {0}">
            <Binding Path="CurrentState" Source="{x:Reference mediaElement}" />
          </MultiBinding>
        </Label.Text>
      </Label>
    </HorizontalStackLayout>

    <Grid Grid.Row="2" Grid.Column="1" Padding="0,10,0,10" ColumnDefinitions="*,*,*,*" ColumnSpacing="5">
      <ImageButton Grid.Column="0" Source="back.png" Clicked="OnPreviousClicked" HeightRequest="50" WidthRequest="50" />
      <ImageButton Grid.Column="1" Source="play.png" Clicked="OnPlayOrPauseClicked" HeightRequest="50" WidthRequest="50">
        <ImageButton.Triggers>
          <DataTrigger TargetType="ImageButton"
                       Binding="{Binding CurrentState, Source={x:Reference mediaElement}}"
                       Value="Playing">
            <Setter Property="Source" Value="pause.png" />
          </DataTrigger>
          <DataTrigger TargetType="ImageButton"
                       Binding="{Binding CurrentState, Source={x:Reference mediaElement}}"
                       Value="Paused">
            <Setter Property="Source" Value="play.png" />
          </DataTrigger>
          <DataTrigger TargetType="ImageButton"
                       Binding="{Binding CurrentState, Source={x:Reference mediaElement}}"
                       Value="None">
            <Setter Property="Source" Value="play.png" />
          </DataTrigger>
        </ImageButton.Triggers>
      </ImageButton>
      <ImageButton Grid.Column="2" Source="stop.png" Clicked="OnStopClicked" HeightRequest="50" WidthRequest="50" />
      <ImageButton Grid.Column="3" Source="forward.png" Clicked="OnNextClicked" HeightRequest="50" WidthRequest="50" />
    </Grid>

    <VerticalStackLayout Grid.Row="3" Grid.Column="1" Padding="0,10,0,10">
      <Slider x:Name="positionSlider"
              MinimumTrackColor="Gray"
              DragStarted="OnSliderDragStarted"
              DragCompleted="OnSliderDragCompleted"/>

      <HorizontalStackLayout Padding="0,10,0,10">
        <Label HorizontalOptions="Center">
          <Label.Text>
            <MultiBinding StringFormat="{}Position: {0}/{1}">
              <Binding Path="Position" Source="{x:Reference mediaElement}" Converter="{StaticResource SecondsToStringConverter}" />
              <Binding Path="Duration" Source="{x:Reference mediaElement}" Converter="{StaticResource SecondsToStringConverter}" />
            </MultiBinding>
          </Label.Text>
        </Label>
      </HorizontalStackLayout>
    </VerticalStackLayout>

    <Grid Grid.Row="4" Grid.Column="1" Padding="0,10,0,10" RowDefinitions="*,*" ColumnDefinitions="*,*" ColumnSpacing="5">
      <Label Grid.Column="0" Grid.Row="0">
        <Label.FormattedText>
          <FormattedString>
            <Span Text="Volume:" />
            <Span Text="{Binding Source={x:Reference mediaElement}, Path=Volume, StringFormat='{}{0:P0}'}" />
          </FormattedString>
        </Label.FormattedText>
      </Label>
      <Slider Grid.Column="0" Grid.Row="1" 
              Maximum="1.0"
              Minimum="0.0"
              MinimumTrackColor="Red"
              MaximumTrackColor="Gray"
              Margin="10,0,10,0">
        <Slider.Value>
          <Binding Path="Volume" Source="{x:Reference mediaElement}" />
        </Slider.Value>
      </Slider>
      <ImageButton Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
                   HeightRequest="50" WidthRequest="50"
                   Source="mute.png"
                   Clicked="OnMuteClicked">
        <ImageButton.Triggers>
          <DataTrigger TargetType="ImageButton"
                       Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                       Value="True">
            <Setter Property="Source" Value="sound.png" />
          </DataTrigger>
          <DataTrigger TargetType="ImageButton"
                       Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                       Value="False">
            <Setter Property="Source" Value="mute.png" />
          </DataTrigger>
        </ImageButton.Triggers>
      </ImageButton>
    </Grid>
  </Grid>
</ContentPage>
解决方案

要实现侧边栏宽度变化且不缩放内部内容,核心是直接修改Grid列的宽度而非Frame的缩放,同时用动画平滑过渡。

步骤1:修改XAML结构

将Grid的第一列宽度绑定到ViewModel的属性,同时给Frame设置HorizontalOptions="Fill"确保填充列宽:

<!-- 修改Grid.ColumnDefinitions部分 -->
<Grid.ColumnDefinitions>
  <ColumnDefinition Width="{Binding SideBarWidth}"/>
  <ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>

<!-- 修改trackListSideBar的Frame -->
<Frame Grid.Row="0" Grid.RowSpan="5" Grid.Column="0"
       ZIndex="100"
       HeightRequest="600"
       x:Name="trackListSideBar"
       HorizontalOptions="Fill">
  <ImageButton Source="song.png" Clicked="OnTrackListImageClicked" WidthRequest="50" HeightRequest="50" />
</Frame>

步骤2:在ViewModel中添加属性

在PlayerPageViewModel中添加可绑定的宽度属性,以及控制展开/折叠的状态:

private GridLength _sideBarWidth = new GridLength(1, GridUnitType.Star);
public GridLength SideBarWidth
{
    get => _sideBarWidth;
    set => SetProperty(ref _sideBarWidth, value);
}

private bool _isSideBarExpanded;
public bool IsSideBarExpanded
{
    get => _isSideBarExpanded;
    set
    {
        if (SetProperty(ref _isSideBarExpanded, value))
        {
            // 切换宽度:折叠时用固定值(比如50),展开时用1*
            SideBarWidth = value ? new GridLength(1, GridUnitType.Star) : new GridLength(50);
        }
    }
}

步骤3:实现按钮点击的动画过渡

如果需要平滑动画效果,在代码后台的OnTrackListImageClicked方法中添加动画:

private async void OnTrackListImageClicked(object sender, EventArgs e)
{
    var viewModel = (PlayerPageViewModel)BindingContext;
    var column = ((Grid)trackListSideBar.Parent).ColumnDefinitions[0];
    if (viewModel.IsSideBarExpanded)
    {
        await AnimateColumnWidth(column, new GridLength(50), 250);
        viewModel.IsSideBarExpanded = false;
    }
    else
    {
        await AnimateColumnWidth(column, new GridLength(1, GridUnitType.Star), 250);
        viewModel.IsSideBarExpanded = true;
    }
}

// 自定义Grid列宽度动画方法
private async Task AnimateColumnWidth(ColumnDefinition column, GridLength targetWidth, uint duration)
{
    var startValue = column.Width.Value;
    var startUnit = column.Width.GridUnitType;
    var targetValue = targetWidth.Value;
    var targetUnit = targetWidth.GridUnitType;

    // 处理单位转换:将Star单位转为像素值计算
    if (startUnit != targetUnit)
    {
        var parentGrid = (Grid)column.Parent;
        var totalWidth = parentGrid.Width;
        startValue = startUnit == GridUnitType.Star ? totalWidth * (startValue / parentGrid.ColumnDefinitions.Sum(c => c.Width.Value)) : startValue;
        targetValue = targetUnit == GridUnitType.Star ? totalWidth * (targetValue / parentGrid.ColumnDefinitions.Sum(c => c.Width.Value)) : targetValue;
    }

    var startTime = DateTime.Now;
    while ((DateTime.Now - startTime).TotalMilliseconds < duration)
    {
        var elapsedRatio = (DateTime.Now - startTime).TotalMilliseconds / duration;
        var currentValue = startValue + (targetValue - startValue) * Easing.CubicInOut.Ease((double)elapsedRatio);
        
        MainThread.BeginInvokeOnMainThread(() =>
        {
            column.Width = new GridLength(currentValue, targetUnit);
        });
        await Task.Delay(16); // 保持60fps帧率
    }
    // 确保最终状态准确
    MainThread.BeginInvokeOnMainThread(() => column.Width = targetWidth);
}

关键注意点

  • 禁用Scale动画:缩放会影响内部所有元素,直接修改布局宽度是正确方向
  • 绑定Grid列宽度:让整个布局自适应调整,而非仅修改Frame自身尺寸
  • 动画操作布局属性:通过调整ColumnDefinition.Width实现平滑过渡,内部元素保持原有大小

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 02:27:01