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

WinUI 3 MVVM项目图片切换淡入淡出动画实现求助

WinUI 3 MVVM 图片切换淡入淡出实现方案

核心思路

用两个叠加的媒体控件(图片/视频容器)分别承载当前显示内容和下一个待切换内容,通过控制两者的Opacity属性执行同步动画,实现旧内容淡出、新内容淡入的平滑过渡。结合MVVM模式,通过ViewModel的状态变量驱动动画,避免直接操作UI控件。

具体实现步骤

1. XAML布局调整

替换原单个Image结构,用Grid作为容器叠加两个内容控件,默认仅显示当前内容,下一个内容初始透明度为0:

<Grid>
    <!-- 当前显示的媒体(图片/视频) -->
    <ContentControl 
        x:Name="CurrentMedia"
        Content="{x:Bind ViewModel.CurrentMediaContent, Mode=OneWay}"
        Opacity="{x:Bind ViewModel.CurrentMediaOpacity, Mode=OneWay}"/>
    
    <!-- 待切换的下一个媒体(图片/视频) -->
    <ContentControl 
        x:Name="NextMedia"
        Content="{x:Bind ViewModel.NextMediaContent, Mode=OneWay}"
        Opacity="{x:Bind ViewModel.NextMediaOpacity, Mode=OneWay}"/>
</Grid>

2. ViewModel 核心逻辑实现

在ViewModel中定义媒体源、透明度属性,以及自动播放、动画切换的方法:

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Threading.Tasks;

public partial class WallpaperViewModel : ObservableObject
{
    private List<MediaItem> _mediaList;
    private int _currentIndex = 0;

    // 当前媒体内容控件
    [ObservableProperty]
    private FrameworkElement _currentMediaContent;

    // 下一个媒体内容控件
    [ObservableProperty]
    private FrameworkElement _nextMediaContent;

    // 当前媒体透明度
    [ObservableProperty]
    private double _currentMediaOpacity = 1.0;

    // 下一个媒体透明度
    [ObservableProperty]
    private double _nextMediaOpacity = 0.0;

    public WallpaperViewModel()
    {
        // 从JSON加载媒体列表(实际项目替换为你的JSON解析逻辑)
        _mediaList = LoadMediaList();
        // 初始化当前媒体
        CurrentMediaContent = CreateMediaControl(_mediaList[_currentIndex]);
        // 启动自动播放任务
        _ = StartAutoPlayLoop();
    }

    // 自动播放循环
    private async Task StartAutoPlayLoop()
    {
        while (true)
        {
            // 等待当前媒体播放时长
            await Task.Delay(_mediaList[_currentIndex].Duration * 1000);

            // 计算下一个媒体索引
            int nextIndex = (_currentIndex + 1) % _mediaList.Count;
            // 准备下一个媒体控件
            NextMediaContent = CreateMediaControl(_mediaList[nextIndex]);

            // 执行淡入淡出动画
            await RunFadeTransition();

            // 更新当前媒体状态
            _currentIndex = nextIndex;
            CurrentMediaContent = NextMediaContent;
            // 重置透明度,为下一次切换做准备
            CurrentMediaOpacity = 1.0;
            NextMediaOpacity = 0.0;
        }
    }

    // 执行淡入淡出过渡动画
    private async Task RunFadeTransition()
    {
        // 旧媒体淡出动画(1→0)
        var fadeOutAnim = new DoubleAnimation
        {
            From = 1.0,
            To = 0.0,
            Duration = new Duration(TimeSpan.FromSeconds(1))
        };

        // 新媒体淡入动画(0→1)
        var fadeInAnim = new DoubleAnimation
        {
            From = 0.0,
            To = 1.0,
            Duration = new Duration(TimeSpan.FromSeconds(1))
        };

        // 绑定动画到ViewModel属性
        var storyboard = new Storyboard();
        Storyboard.SetTarget(fadeOutAnim, this);
        Storyboard.SetTargetProperty(fadeOutAnim, nameof(CurrentMediaOpacity));

        Storyboard.SetTarget(fadeInAnim, this);
        Storyboard.SetTargetProperty(fadeInAnim, nameof(NextMediaOpacity));

        storyboard.Children.Add(fadeOutAnim);
        storyboard.Children.Add(fadeInAnim);

        // 等待动画完成
        var tcs = new TaskCompletionSource<bool>();
        storyboard.Completed += (s, e) => tcs.SetResult(true);
        storyboard.Begin();
        await tcs.Task;
    }

    // 根据媒体类型创建对应的控件(图片/视频)
    private FrameworkElement CreateMediaControl(MediaItem item)
    {
        if (item.Source.AbsolutePath.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase))
        {
            // 视频控件
            var mediaPlayer = new MediaPlayerElement
            {
                Source = MediaSource.CreateFromUri(item.Source),
                Stretch = Stretch.UniformToFill,
                AutoPlay = true
            };
            return mediaPlayer;
        }
        else
        {
            // 图片控件
            var image = new Image
            {
                Source = new BitmapImage(item.Source),
                Stretch = Stretch.UniformToFill
            };
            return image;
        }
    }

    // 模拟从JSON加载媒体列表
    private List<MediaItem> LoadMediaList()
    {
        return new List<MediaItem>
        {
            new MediaItem { Source = new Uri("ms-appx:///Assets/Img1.jpg"), Duration = 4 },
            new MediaItem { Source = new Uri("ms-appx:///Assets/Img2.jpg"), Duration = 4 },
            new MediaItem { Source = new Uri("ms-appx:///Assets/Video1.mp4"), Duration = 10 }
        };
    }
}

// 媒体项模型
public class MediaItem
{
    public Uri Source { get; set; }
    public int Duration { get; set; } // 播放时长(秒)
}

3. 原代码问题分析

  • 单个Image控件切换Source时,无法实现新旧媒体的过渡衔接,Loaded事件仅在首次加载触发,后续切换不会重新执行动画。
  • AutoReverse=True会让动画回到初始状态,不符合“切换到下一张图”的需求,应该是旧图淡出后保持隐藏,新图淡入后保持显示。
  • 动画目标为容器的Opacity,会导致整个容器透明度变化,而非媒体内容的平滑切换。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 17:27:08