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

如何围绕StackLayout创建方向变色的环绕加载旋转动画?

实现围绕StackLayout的彩色旋转加载指示器方案

我来给你分享一个可行的实现思路,基于.NET MAUI(如果是Xamarin.Forms也可以类似调整),核心是通过自定义ContentView包裹StackLayout,在其上层绘制动态的环绕动画:

核心思路

  1. 用自定义ContentView封装StackLayout,保留原布局的所有功能,同时叠加动画绘制层
  2. 循环动画控制指示器的位置进度,触发重绘
  3. 沿StackLayout的边框路径绘制移动的指示器,颜色随位置动态变化,同时通过多层半透明线段实现轨迹渐隐效果

完整代码实现

自定义LoadingStackLayout控件

using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace YourAppNamespace;

public class LoadingStackLayout : ContentView
{
    private double _animationProgress;
    private readonly Animation _loadingAnimation;
    private readonly Paint _basePaint = new() { Style = PaintStyle.Stroke, StrokeCap = StrokeCap.Round };

    // 暴露自定义属性,让控件更灵活
    public static readonly BindableProperty StrokeWidthProperty =
        BindableProperty.Create(nameof(StrokeWidth), typeof(double), typeof(LoadingStackLayout), 4.0);
    
    public static readonly BindableProperty IndicatorLengthProperty =
        BindableProperty.Create(nameof(IndicatorLength), typeof(double), typeof(LoadingStackLayout), 20.0);
    
    public static readonly BindableProperty CornerRadiusProperty =
        BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(LoadingStackLayout), 8.0);

    public double StrokeWidth
    {
        get => (double)GetValue(StrokeWidthProperty);
        set => SetValue(StrokeWidthProperty, value);
    }

    public double IndicatorLength
    {
        get => (double)GetValue(IndicatorLengthProperty);
        set => SetValue(IndicatorLengthProperty, value);
    }

    public double CornerRadius
    {
        get => (double)GetValue(CornerRadiusProperty);
        set => SetValue(CornerRadiusProperty, value);
    }

    public LoadingStackLayout()
    {
        // 内部嵌套StackLayout,保留原布局功能
        var stackLayout = new StackLayout();
        stackLayout.SetBinding(StackLayout.ChildrenProperty, new Binding(nameof(Content), source: this));
        base.Content = stackLayout;

        // 创建循环动画,每2秒完成一圈
        _loadingAnimation = new Animation(v =>
        {
            _animationProgress = v;
            InvalidateSurface();
        }, 0, 1, Easing.Linear);
        
        _loadingAnimation.Commit(this, "LoadingLoop", length: 2000, repeat: () => true);
    }

    protected override void OnPaintSurface(SurfacePaintEventArgs args)
    {
        base.OnPaintSurface(args);
        if (Content == null) return;

        var canvas = args.Canvas;
        var layoutBounds = Content.Bounds;
        var cornerRadius = (float)CornerRadius;
        var strokeWidth = (float)StrokeWidth;
        var indicatorLength = (float)IndicatorLength;

        // 计算边框总周长(包含圆角弧长)
        var straightEdgeLength = (layoutBounds.Width + layoutBounds.Height - 4 * cornerRadius) * 2;
        var arcLength = 2 * Math.PI * cornerRadius;
        var totalPerimeter = straightEdgeLength + 2 * arcLength;

        // 当前指示器的起始位置(按周长比例计算)
        var startDistance = _animationProgress * totalPerimeter;

        // 生成边框的完整路径
        var fullPath = new PathF();
        fullPath.AddRoundedRectangle(layoutBounds, cornerRadius);

        // 绘制渐隐轨迹:通过多层透明度递减的线段模拟拖尾效果
        for (int i = 0; i < 10; i++)
        {
            var offset = (startDistance - i * 8 + totalPerimeter) % totalPerimeter;
            var segmentPath = GetPathSegment(fullPath, offset, indicatorLength - i * 1.5f);
            if (segmentPath == null) continue;

            // 根据进度计算当前色相,实现颜色随方向变化
            var hue = (_animationProgress * 360 - i * 10) % 360;
            var trailColor = Color.FromHsla(hue / 360, 0.8, 0.6, 1 - i * 0.1f);
            
            _basePaint.Color = trailColor;
            _basePaint.StrokeWidth = strokeWidth - i * 0.3f;
            canvas.DrawPath(segmentPath, _basePaint);
        }

        // 绘制当前高亮的指示器
        var currentSegment = GetPathSegment(fullPath, startDistance, indicatorLength);
        if (currentSegment != null)
        {
            var currentHue = (_animationProgress * 360) % 360;
            var highlightColor = Color.FromHsla(currentHue / 360, 0.8, 0.6);
            
            _basePaint.Color = highlightColor;
            _basePaint.StrokeWidth = strokeWidth;
            canvas.DrawPath(currentSegment, _basePaint);
        }
    }

    // 从完整边框路径中截取指定距离的线段
    private PathF? GetPathSegment(PathF fullPath, double startDistance, float segmentLength)
    {
        var bounds = fullPath.Bounds;
        var cornerRadius = (float)CornerRadius;
        var sideLengths = new[]
        {
            bounds.Width - 2 * cornerRadius, // 上边直线段
            bounds.Height - 2 * cornerRadius, // 右边直线段
            bounds.Width - 2 * cornerRadius, // 下边直线段
            bounds.Height - 2 * cornerRadius // 左边直线段
        };

        double cumulativeLength = 0;
        int currentSide = 0;

        // 判断当前起始距离落在哪个边/圆角上
        while (currentSide < 4)
        {
            var sideTotalLength = sideLengths[currentSide] + Math.PI * cornerRadius;
            if (startDistance <= cumulativeLength + sideTotalLength) break;
            
            cumulativeLength += sideTotalLength;
            currentSide++;
        }

        if (currentSide >= 4) return null;
        var remainingDistance = startDistance - cumulativeLength;
        var segmentPath = new PathF();

        switch (currentSide)
        {
            case 0: // 上边
                var topStartX = bounds.Left + cornerRadius + (float)remainingDistance;
                var topEndX = topStartX + segmentLength;
                
                if (topEndX > bounds.Right - cornerRadius)
                {
                    // 进入右上角圆角
                    var arcLength = topEndX - (bounds.Right - cornerRadius);
                    segmentPath.MoveTo(topStartX, bounds.Top);
                    segmentPath.LineTo(bounds.Right - cornerRadius, bounds.Top);
                    segmentPath.AddArc(
                        new RectF(bounds.Right - 2 * cornerRadius, bounds.Top, 2 * cornerRadius, 2 * cornerRadius),
                        180,
                        (float)(arcLength / (Math.PI * cornerRadius) * 90));
                }
                else
                {
                    segmentPath.MoveTo(topStartX, bounds.Top);
                    segmentPath.LineTo(topEndX, bounds.Top);
                }
                break;
            
            case 1: // 右边
                var rightStartY = bounds.Top + cornerRadius + (float)remainingDistance;
                var rightEndY = rightStartY + segmentLength;
                
                if (rightEndY > bounds.Bottom - cornerRadius)
                {
                    var arcLength = rightEndY - (bounds.Bottom - cornerRadius);
                    segmentPath.MoveTo(bounds.Right, rightStartY);
                    segmentPath.LineTo(bounds.Right, bounds.Bottom - cornerRadius);
                    segmentPath.AddArc(
                        new RectF(bounds.Right - 2 * cornerRadius, bounds.Bottom - 2 * cornerRadius, 2 * cornerRadius, 2 * cornerRadius),
                        270,
                        (float)(arcLength / (Math.PI * cornerRadius) * 90));
                }
                else
                {
                    segmentPath.MoveTo(bounds.Right, rightStartY);
                    segmentPath.LineTo(bounds.Right, rightEndY);
                }
                break;
            
            case 2: // 下边
                var bottomStartX = bounds.Right - cornerRadius - (float)remainingDistance;
                var bottomEndX = bottomStartX - segmentLength;
                
                if (bottomEndX < bounds.Left + cornerRadius)
                {
                    var arcLength = (bounds.Left + cornerRadius) - bottomEndX;
                    segmentPath.MoveTo(bottomStartX, bounds.Bottom);
                    segmentPath.LineTo(bounds.Left + cornerRadius, bounds.Bottom);
                    segmentPath.AddArc(
                        new RectF(bounds.Left, bounds.Bottom - 2 * cornerRadius, 2 * cornerRadius, 2 * cornerRadius),
                        0,
                        (float)(arcLength / (Math.PI * cornerRadius) * 90));
                }
                else
                {
                    segmentPath.MoveTo(bottomStartX, bounds.Bottom);
                    segmentPath.LineTo(bottomEndX, bounds.Bottom);
                }
                break;
            
            case 3: // 左边
                var leftStartY = bounds.Bottom - cornerRadius - (float)remainingDistance;
                var leftEndY = leftStartY - segmentLength;
                
                if (leftEndY < bounds.Top + cornerRadius)
                {
                    var arcLength = (bounds.Top + cornerRadius) - leftEndY;
                    segmentPath.MoveTo(bounds.Left, leftStartY);
                    segmentPath.LineTo(bounds.Left, bounds.Top + cornerRadius);
                    segmentPath.AddArc(
                        new RectF(bounds.Left, bounds.Top, 2 * cornerRadius, 2 * cornerRadius),
                        90,
                        (float)(arcLength / (Math.PI * cornerRadius) * 90));
                }
                else
                {
                    segmentPath.MoveTo(bounds.Left, leftStartY);
                    segmentPath.LineTo(bounds.Left, leftEndY);
                }
                break;
        }

        return segmentPath;
    }
}

在XAML中使用控件

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:YourAppNamespace"
             x:Class="YourAppNamespace.MainPage">

    <local:LoadingStackLayout StrokeWidth="5" IndicatorLength="25" CornerRadius="10">
        <!-- 这里放你原来的StackLayout内容 -->
        <Label Text="加载中..." HorizontalOptions="Center" VerticalOptions="Center" FontSize="18"/>
        <Button Text="测试按钮" Margin="0,20,0,0" HorizontalOptions="Center"/>
    </local:LoadingStackLayout>

</ContentPage>

关键功能说明

  1. 颜色动态变化:通过HSL颜色模型,根据动画进度计算色相,实现指示器沿路径移动时的颜色渐变
  2. 轨迹渐隐效果:循环绘制10层偏移线段,每层透明度和线条宽度递减,模拟拖尾淡出的视觉效果
  3. 圆角适配:支持自定义圆角半径,自动处理圆角处的路径过渡
  4. 可配置性:通过BindableProperty暴露线条宽度、指示器长度、圆角半径等参数,方便自定义样式

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 18:17:46