.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
相关产品推荐
相关产品推荐

