Xamarin.Forms无限滚动图像背景效果

时间:2019-06-22 08:09:32

标签: c# performance user-interface animation xamarin.forms

我正在尝试使我认为对应用程序会有很好的效果-一系列图像(例如墙纸)将在视图期间不断在背景中滚动。我开始在Xamarin.Forms中对此进行原型制作,创建一个自定义控件。计划采用对角线平移,但从最基本的方法开始,但仍然很快遇到一些问题,即它并不完全平滑,因为它在这里和那里变得有点断断续续(即使使用缓存并且仅使用10kb的图像)和2 ),如果用户执行的动作更复杂,则可能会导致时滞,并且图像之间的渲染比实际情况更紧密。是否有一种方法可以解决此问题,以使其尽可能平滑且不会干扰(或干扰)其他UI元素,或者是否存在类似这种更好的方法-任何人都可以解决吗?请让我知道,谢谢。

FlyingImageBackground.cs

public class FlyingImageBackground : ContentView
{
    public static readonly BindableProperty FlyingImageProperty =
      BindableProperty.Create(nameof(FlyingImage), typeof(ImageSource), typeof(FlyingImageBackground), default(ImageSource), BindingMode.TwoWay, propertyChanged: OnFlyingImageChanged);

    public ImageSource FlyingImage
    {
        get => (ImageSource)GetValue(FlyingImageProperty);
        set => SetValue(FlyingImageProperty, value);
    }

    private AbsoluteLayout canvas;

    public FlyingImageBackground()
    {
        this.canvas = new AbsoluteLayout()
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                VerticalOptions = LayoutOptions.FillAndExpand
            };

        this.canvas.SizeChanged += Canvas_SizeChanged;

        Content = this.canvas;
    }

    ~FlyingImageBackground() => this.canvas.SizeChanged -= Canvas_SizeChanged;

    private static void OnFlyingImageChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = (FlyingImageBackground)bindable;
        control.BringToLife();
    }

    private void BringToLife()
    {
        if (this.canvas.Width <= 0 || this.canvas.Height <= 0)
            return;

        Device.StartTimer(TimeSpan.FromSeconds(1), () =>
        {
            Device.BeginInvokeOnMainThread(async () =>
            {
                await SendImageWave();
            });

            return this.canvas.IsVisible;
        });
    }

    private async Task SendImageWave()
    {
        var startingX = -100;
        var endingX = this.canvas.Width;

        if (endingX <= 0)
            return;

        endingX += 100;

        var yPositions = Enumerable.Range(0, (int)this.canvas.Height).Where(x => x % 90 == 0).ToList();

        var imgList = new List<CachedImage>();

        foreach (var yPos in yPositions)
        {
            var img = new CachedImage
            {
                Source = FlyingImage,
                HeightRequest = 50
            };
            imgList.Add(img);

            this.canvas.Children.Add(img, new Point(startingX, yPos));
        }

        await Task.WhenAll(
            imgList.Select(x => x.TranslateTo(endingX, 0, 10000)));
        //.Concat(imgList.Select(x => x.TranslateTo(startingX, 0, uint.MinValue))));

        imgList.ForEach(x =>
        {
            this.canvas.Children.Remove(x);
            x = null;
        });

        imgList = null;
    }

    private void Canvas_SizeChanged(object sender, EventArgs e)
    {
        BringToLife();
    }
}

用法示例:

只需将其与主要内容一起放入ContentPage中的Grid中: 例如:

<ContentPage.Content>
    <Grid>
        <controls:FlyingImageBackground FlyingImage="fireTruck.png" />

        <StackLayout HorizontalOptions="Center">
            <Button
                Text="I'm a button!" />
            <Label
                FontAttributes="Bold,Italic"
                Text="You're a good man, old sport!!!"
                TextDecorations="Underline" />
        </StackLayout>
    </Grid>

</ContentPage.Content>

1 个答案:

答案 0 :(得分:0)

切换到SkiaSharp,效果更好。动画看起来很流畅,如果流程中断,图像将保持适当的距离。在初稿中也使用内置的Xamarin动画实现了这一功能,我将检查的运行时间搞砸了。即使页面不再显示在屏幕上,IsVisible道具也将保持为true,因此在此新版本中,需要绑定到一个告诉我页面是否实际处于活动状态的属性(基于何时导航到和导航离开),如果没有,则停止动画。目前,这仍只是处理水平滚动效果。希望其他人觉得它有用,并且欢迎其他任何改进,请发表评论/发表答案!

[DesignTimeVisible(true)]
public class FlyingImageBackgroundSkia : ContentView
{
    public static readonly BindableProperty IsActiveProperty =
        BindableProperty.Create(
            nameof(IsActive),
            typeof(bool),
            typeof(FlyingImageBackground),
            default(bool),
            BindingMode.TwoWay,
            propertyChanged: OnPageActivenessChanged);

    private SKCanvasView canvasView;
    private SKBitmap resourceBitmap;
    private Stopwatch stopwatch = new Stopwatch();

    // consider making these bindable props
    private float percentComplete;
    private float imageSize = 40;
    private float columnSpacing = 100;
    private float rowSpacing = 100;
    private float framesPerSecond = 60;
    private float cycleTime = 1; // in seconds, for a single column

    public FlyingImageBackgroundSkia()
    {
        this.canvasView = new SKCanvasView();
        this.canvasView.PaintSurface += OnCanvasViewPaintSurface;
        this.Content = this.canvasView;

        string resourceID = "XamarinTestProject.Resources.Images.fireTruck.png";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            this.resourceBitmap = SKBitmap.Decode(stream);
        }
    }

    ~FlyingImageBackgroundSkia() => this.resourceBitmap.Dispose();

    public bool IsActive
    {
        get => (bool)GetValue(IsActiveProperty);
        set => SetValue(IsActiveProperty, value);
    }

    private static async void OnPageActivenessChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = (FlyingImageBackgroundSkia)bindable;
        await control.AnimationLoop();
    }

    private async Task AnimationLoop()
    {
        this.stopwatch.Start();

        while (IsActive)
        {
            this.percentComplete = (float)(this.stopwatch.Elapsed.TotalSeconds % this.cycleTime) / this.cycleTime; // always between 0 and 1
            this.canvasView.InvalidateSurface(); // trigger redraw
            await Task.Delay(TimeSpan.FromSeconds(1.0 / this.framesPerSecond)); // non-blocking
        }

        this.stopwatch.Stop();
    }

    private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        var xPositions = Enumerable.Range(0, info.Width + (int)this.columnSpacing).Where(x => x % (int)this.columnSpacing == 0).ToList();
        xPositions.Insert(0, -(int)this.columnSpacing);

        var yPositions = Enumerable.Range(0, info.Height + (int)this.rowSpacing).Where(x => x % (int)this.rowSpacing == 0).ToList();
        yPositions.Insert(0, -(int)this.rowSpacing);

        if (this.resourceBitmap != null)
        {
            foreach (var xPos in xPositions)
            {
                var xPosNow = xPos + (this.rowSpacing * this.percentComplete);
                foreach (var yPos in yPositions)
                {
                    canvas.DrawBitmap(
                        this.resourceBitmap,
                        new SKRect(xPosNow, yPos, xPosNow + this.imageSize, yPos + this.imageSize));
                }
            }
        }
    }
}