用波浪动画填充椭圆形

27

我已在Windows Phone 8.1 Silverlight应用程序和UWP中创建了一个椭圆,并且我想要用动画波形来填充它,为此,我正在按照这个解决方案进行操作。

但是这个方案是针对WPF的,所以我无法使用一些控件,比如"Visual Brush"。

我想要用类似于这个图像的波形来填充椭圆(忽略图像中的50%) -

进入图像描述

这是我的椭圆:

<Ellipse Name="WaveEllipse" Grid.Column="1" Grid.Row="0" VerticalAlignment="Top"
         Stroke="{StaticResource PhoneAccentBrush}"
         StrokeThickness="4"
         Width="225"
         Height="225">
</Ellipse>

有没有关于视觉笔刷的其它选择?主要是我想在Windows Phone 8.1 Silverlight中实现它,但如果在WP平台上不可用,我会转换到UWP。

3个回答

93

在提供代码之前,请观看下面的动画 gif,尝试理解如何创建此动画。

图片描述

有意义吧?我们需要做的就是创建这样一个形状,无限地对其进行X轴(水平方向)和Y轴(水位高度)的偏移动画,并最后将其剪切为椭圆形状。

因此,首先需要使用Adobe Illustrator或类似工具创建此形状。在AI中,有一个名为Zig Zag效果(请参见下面的截图),非常适合此用途。只需确保起点与结束点相同,以便在重复动画时,它会感觉像是永无止境的。

图片描述

目前在UWP中缺少的是使用非矩形形状裁剪UIElement的能力,因此在此我们需要将其导出为png(否则我们将其导出为svg并使用Path显示它)。

同样出于这个原因,裁剪部分需要大量的工作。就像Jet Chopper所回答的那样,为了得到一个surfaceBrush,需要写很多代码!更不用说,您还需要手动处理设备丢失和应用程序生命周期。

值得庆幸的是,在创作者更新(即15063)中,有一个名为LoadedImageSurface的新API,可以通过图像URI创建一个CompositionSurfaceBrush,只需几行代码。在我下面的代码示例中,您将看到我使用了此功能,这意味着,如果要支持Windows 10的旧版本,则需要将其替换为Jet答案中的内容。

代码

想法是创建一个名为WaveProgressControl的UserControl,该UserControl封装了所有动画逻辑,并公开了一个称为Percent的依赖属性,该属性控制水位高度

WaveProgressControl控件 - XAML

<UserControl x:Class="WaveProgressControlRepo.WaveProgressControl"
             Height="160"
             Width="160">

    <Grid x:Name="Root">
        <Ellipse x:Name="ClippedImageContainer"
                 Fill="White"
                 Margin="6" />
        <Ellipse x:Name="CircleBorder"
                 Stroke="#FF0289CD"
                 StrokeThickness="3" />
        <TextBlock Foreground="#FF0289CD"
                   FontSize="36"
                   FontWeight="SemiBold"
                   TextAlignment="Right"
                   VerticalAlignment="Center"
                   Width="83"
                   Margin="0,0,12,0">
            <Run Text="{x:Bind Percent, Mode=OneWay}" />
            <Run Text="%"
                 FontSize="22" />
        </TextBlock>
    </Grid>
</UserControl>

WaveProgressControl控件 - 代码后台

private readonly Compositor _compositor;
private readonly CompositionPropertySet _percentPropertySet;

public WaveProgressControl()
{
    InitializeComponent();

    _compositor = Window.Current.Compositor;

    _percentPropertySet = _compositor.CreatePropertySet();
    _percentPropertySet.InsertScalar("Value", 0.0f);

    Loaded += OnLoaded;
}

public double Percent
{
    get => (double)GetValue(PercentProperty);
    set => SetValue(PercentProperty, value);
}
public static readonly DependencyProperty PercentProperty =
    DependencyProperty.Register("Percent", typeof(double), typeof(WaveProgressControl),
        new PropertyMetadata(0.0d, (s, e) =>
        {
            var self = (WaveProgressControl)s;
            var propertySet = self._percentPropertySet;
            propertySet.InsertScalar("Value", Convert.ToSingle(e.NewValue) / 100);
        }));

private void OnLoaded(object sender, RoutedEventArgs e)
{
    CompositionSurfaceBrush imageSurfaceBrush;

    SetupClippedWaveImage();
    SetupEndlessWaveAnimationOnXAxis();
    SetupExpressionAnimationOnYAxisBasedOnPercentValue();

    void SetupClippedWaveImage()
    {
        // Note LoadedImageSurface is only available in 15063 onward.
        var imageSurface = LoadedImageSurface.StartLoadFromUri(new Uri(BaseUri, "/Assets/wave.png"));
        imageSurfaceBrush = _compositor.CreateSurfaceBrush(imageSurface);
        imageSurfaceBrush.Stretch = CompositionStretch.None;
        imageSurfaceBrush.Offset = new Vector2(120, 248);

        var maskBrush = _compositor.CreateMaskBrush();
        var maskSurfaceBrush = ClippedImageContainer.GetAlphaMask(); // CompositionSurfaceBrush
        maskBrush.Mask = maskSurfaceBrush;
        maskBrush.Source = imageSurfaceBrush;

        var imageVisual = _compositor.CreateSpriteVisual();
        imageVisual.RelativeSizeAdjustment = Vector2.One;
        ElementCompositionPreview.SetElementChildVisual(ClippedImageContainer, imageVisual);

        imageVisual.Brush = maskBrush;
    }

    void SetupEndlessWaveAnimationOnXAxis()
    {
        var waveOffsetXAnimation = _compositor.CreateScalarKeyFrameAnimation();
        waveOffsetXAnimation.InsertKeyFrame(1.0f, -80.0f, _compositor.CreateLinearEasingFunction());
        waveOffsetXAnimation.Duration = TimeSpan.FromSeconds(1);
        waveOffsetXAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
        imageSurfaceBrush.StartAnimation("Offset.X", waveOffsetXAnimation);
    }

    void SetupExpressionAnimationOnYAxisBasedOnPercentValue()
    {
        var waveOffsetYExpressionAnimation = _compositor.CreateExpressionAnimation("Lerp(248.0f, 120.0f, Percent.Value)");
        waveOffsetYExpressionAnimation.SetReferenceParameter("Percent", _percentPropertySet);
        imageSurfaceBrush.StartAnimation("Offset.Y", waveOffsetYExpressionAnimation);
    }
}

主页面 MainPage

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <local:WaveProgressControl x:Name="WaveProgressControl" />

    <Slider Grid.Row="1"
            Margin="24"
            Value="{x:Bind WaveProgressControl.Percent, Mode=TwoWay}" />
</Grid>

我已经将所有东西都放入了这个示例项目中,下面是一个实时演示。享受吧! :)

输入图片描述


1
感谢 LoadedImageSurface。加载图像一直很困难。 - Jet Chopper
1
谢谢你们两个的回答,结合起来帮了我很多。 - Shubham Sahu
1
@lindexi 当然可以! - Justin XL
1
@JustinXL 感谢你的代码,但我不知道为什么要使用 imageSurfaceBrush.Offset = new Vector2(120, 248);,你能告诉我为什么要使用 120,248 吗? - lindexi
2
@lindexi,图像位于屏幕中央,然后向下推 248 个单位并向右推 120 个单位。 - Justin XL
显示剩余3条评论

12

这是UWP示例,你可以根据自己的需求进行调整:

<Canvas>
    <Ellipse x:Name="Ellipse" Width="256" Height="256" Fill="DarkViolet" Stroke="DeepSkyBlue" StrokeThickness="8"/>
    <Border x:Name="VisualBorder" Opacity="0.5"/>
</Canvas>

代码后台:

    private async void CreateVisuals()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;

        var bitmap = await CanvasBitmap.LoadAsync(CanvasDevice.GetSharedDevice(),
            new Uri("ms-appx:///Assets/Wave-PNG-Transparent-Picture.png"));

        var drawingSurface =
            CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice())
                .CreateDrawingSurface(bitmap.Size,
                    DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
        using (var ds = CanvasComposition.CreateDrawingSession(drawingSurface))
        {
            ds.Clear(Colors.Transparent);
            ds.DrawImage(bitmap);
        }

        var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
        surfaceBrush.Stretch = CompositionStretch.None;

        var maskedBrush = compositor.CreateMaskBrush();
        maskedBrush.Mask = Ellipse.GetAlphaMask();
        maskedBrush.Source = surfaceBrush;

        var sprite = compositor.CreateSpriteVisual();
        sprite.Size = new Vector2((float)Ellipse.Width, (float)Ellipse.Height);
        sprite.Brush = maskedBrush;
        sprite.CenterPoint = new Vector3(sprite.Size / 2, 0);
        sprite.Scale = new Vector3(0.9f);

        ElementCompositionPreview.SetElementChildVisual(VisualBorder, sprite);

        var offsetAnimation = compositor.CreateScalarKeyFrameAnimation();
        offsetAnimation.InsertKeyFrame(0, 0);
        offsetAnimation.InsertKeyFrame(1, 256, compositor.CreateLinearEasingFunction());
        offsetAnimation.Duration = TimeSpan.FromMilliseconds(1000);
        offsetAnimation.IterationBehavior = AnimationIterationBehavior.Forever;

        surfaceBrush.StartAnimation("Offset.X", offsetAnimation);
    }
}

这就是它的样子:

在此输入图片描述


谢谢,它能在Windows Phone 8.1 Silverlight上运行吗?我正在检查中...... - Shubham Sahu
代码中有什么遗漏吗?谢谢你的努力,但是它并不完全符合我的要求,我想要填充到那个波浪线,并根据我在给定解决方案链接中提到的进度进行填充。而且它是针对Anniversary 14393 SDK的,一些API在10240 SDK中不可用,仍然有一些用户的手机没有升级到周年更新,所以我不想直接针对14393升级。但还是感谢你的努力。............................ :) - Shubham Sahu
最低 SDK 版本为 10586。您可以移动 Offset.Y 以使其填满整个椭圆。并更改波形以隐藏波浪下方的内容。 - Jet Chopper
非常感谢你们两位的回答,你们的合作帮助了我很多。 - Shubham Sahu

1
我使用了一个简单的解决方案来实现这个:
将Wave2.png复制并粘贴到第一张图片的末尾,从而得到了一个更长的图像。
该解决方案适用于WP8/商店应用程序/UWP/Silverlight。

enter image description here

<Border
        Background="White"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        CornerRadius="10000"
        BorderBrush="Black"
        BorderThickness="5">
        <Grid>
            <Ellipse
                x:Name="ellipse"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Height="200"
                Width="200">
                <Ellipse.Fill>
                    <ImageBrush
                        x:Name="WaveImage"
                        Stretch="None"
                        ImageSource="wave2.png">
                        <ImageBrush.Transform>
                            <CompositeTransform
                                TranslateY="200"
                                TranslateX="299" />
                        </ImageBrush.Transform>
                    </ImageBrush>
                </Ellipse.Fill>
            </Ellipse>
            <TextBlock
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Text="HUJ" />
        </Grid>
    </Border>

这里是动画代码:

<Storyboard
        x:Name="AnimateWave">
        <DoubleAnimationUsingKeyFrames
            RepeatBehavior="Forever"
            EnableDependentAnimation="True"
            Storyboard.TargetProperty="(Shape.Fill).(Brush.Transform).(CompositeTransform.TranslateX)"
            Storyboard.TargetName="ellipse">
            <EasingDoubleKeyFrame
                KeyTime="0:0:5"
                Value="-299" />
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>

很棒的想法 - Shubham Sahu
1
@ShubhamSahu 刚刚检查了一下,发现可以在没有图像扩展名的情况下完成。是的,只需将 X 翻译从 299 和 -299 更改为 99 和 -99。然后相应地调整 Y 翻译。 - Developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接