WPF SkiaSharp弹幕
驚鏵 人气:0SkiaSharp 自绘弹幕效果
框架使用.NET60
;
Visual Studio 2022
;
项目使用 MIT 开源许可协议;
上期有网友建议使用Skia实现弹幕。
- 弹幕消息使用
SKElement
做弹幕展现,然后在SKCanvas
进行绘制弹幕。 - 由于需要绘制矩形与文本所以需要使用到
SKBitmap
进行绘制弹幕类。 - 创建
SKBitmap
设置宽(根据文本的长度定义宽度)与高度40
。 - 创建对象
SKCanvas
并实例化的时候将SKBitmap
传入,然后对SKCanvas
进行绘制背景DrawRoundRect
与文本DrawText
,使用属性记录X
与Y
的值方便在动画的时候让弹幕动起来。 Barrage
在Render
的时候进行绘制弹幕图片DrawImage(SKBitmap,x,y)
。- 弹幕每次移动多少值 等于
SKCanvas
的宽除以弹幕的宽
。 - 当弹幕移动
Move()
时如超过-Width
则通过out
返回GUID
就移除弹幕对象。
实现代码
1) 准备 MsgInfo 弹幕消息类如下:
using System; using SkiaSharp; namespace SkiaSharpBarrage { /// <summary> /// msg info /// </summary> public class MsgInfo { private string _msg; public string GUID; public MsgInfo(string msg, SKTypeface _font, float windowsWidth) { _msg = msg; var _random = new Random(); var skColor = new SKColor((byte) _random.Next(1, 255), (byte) _random.Next(1, 255), (byte) _random.Next(1, 233)); using var paint = new SKPaint { Color = skColor, Style = SKPaintStyle.Fill, IsAntialias = true, StrokeWidth = 2 }; paint.Shader = SKShader.CreateLinearGradient( new SKPoint(0, 0), new SKPoint(1, 1), new[] {SKColors.Transparent, skColor}, new float[] {0, 1}, SKShaderTileMode.Repeat); using var paintText = new SKPaint { Color = SKColors.White, IsAntialias = true, Typeface = _font, TextSize = 24 }; var textBounds = new SKRect(); paintText.MeasureText(msg, ref textBounds); var width = textBounds.Width + 100; SKImage skImage; using (var bitmap = new SKBitmap((int) width, 40, true)) using (var canvas = new SKCanvas(bitmap)) { canvas.DrawRoundRect(0, 0, width, 40, 20, 20, paint); canvas.DrawText(msg, width / 2 - textBounds.Width / 2, bitmap.Height / 2 + textBounds.Height / 2, paintText); var image = SKImage.FromBitmap(bitmap); skImage = image; } SKImage = skImage; Width = width; X = windowsWidth + Width; CanvasWidth = windowsWidth; CostTime = TimeSpan.FromMilliseconds(Width); GUID = Guid.NewGuid().ToString("N"); } public float X { get; set; } public float Y { get; set; } public float Width { get; set; } public float CanvasWidth { get; set; } public SKImage SKImage { get; set; } public float MoveNum => CanvasWidth / (float) CostTime.TotalMilliseconds; public TimeSpan CostTime { get; set; } /// <summary> /// 定时调用,移动指定距离 /// </summary> public void Move(out string guid) { guid = string.Empty; X = X - MoveNum; if (X <= -Width) guid = GUID; } } }
2) 新建 Barrage.cs 类如下:
using System.Collections.Generic; using System.Linq; using SkiaSharp; namespace SkiaSharpBarrage { public class Barrage { private readonly SKTypeface _font; private readonly List<MsgInfo> _MsgInfo; private int _num, _index; private double _right, _top; private float _width; private readonly float _height; public Barrage(SKTypeface font, float width, float height, List<string> strList) { _width = width; _height = height; _font = font; _num = (int) height / 40; _MsgInfo = new List<MsgInfo>(); foreach (var item in strList) BuildMsgInfo(item); } private void BuildMsgInfo(string text) { _index++; if (_right != 0) _width = (float) _right; var model = new MsgInfo(text, _font, _width); _right = _right == 0 ? _height + model.Width : _right; var y = _height - 40; _top = _top + 40 >= y ? 40 : _top; model.Y = (float) _top; _MsgInfo.Add(model); _top += 60; } public void AddBarrage(string text) { BuildMsgInfo(text); } public void Render(SKCanvas canvas, SKTypeface font, int width, int height, List<string> strList) { for (var i = 0; i < _MsgInfo.Count; i++) { var info = _MsgInfo[i]; var guid = string.Empty; info.Move(out guid); if (!string.IsNullOrEmpty(guid)) { var model = _MsgInfo.FirstOrDefault(x => x.GUID == guid); _MsgInfo.Remove(model); } canvas.DrawImage(info.SKImage, info.X, info.Y); } } } }
3) MainWindow.xaml.cs 如下:
<wpfdev:Window x:Class="SkiaSharpBarrage.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers" xmlns:skia="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF" mc:Ignorable="d" WindowStartupLocation="CenterScreen" ResizeMode="CanMinimize" Title="SkiaSharpBarrage - 弹幕篇" Height="450" Width="800"> <Grid Margin="4"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <MediaElement Stretch="Uniform" Grid.RowSpan="2" Name="myMediaElement" /> <skia:SKElement x:Name="skElement" /> <Grid Grid.Row="1" Name="MyGrid"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBox wpfdev:ElementHelper.IsWatermark="True" x:Name="tbBarrage" wpfdev:ElementHelper.Watermark="请弹幕内容" /> <Button Grid.Column="1" Style="{StaticResource PrimaryButton}" Content="发射弹幕" Margin="4,0,0,0" Click="ButtonBase_OnClick" /> </Grid> </Grid> </wpfdev:Window>
3) 逻辑 MainWindow.xaml.cs 如下:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using SkiaSharp; using SkiaSharp.Views.Desktop; namespace SkiaSharpBarrage { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow { private readonly Barrage _barrage; private readonly SKTypeface _font; private readonly List<string> list = new List<string>(); public MainWindow() { list.Add("2333"); list.Add("测试弹幕公众号:WPF开发者"); list.Add("很难开心"); list.Add("LOL~"); list.Add("青春记忆"); list.Add("bing"); list.Add("Microsoft"); InitializeComponent(); var index = SKFontManager.Default.FontFamilies.ToList().IndexOf("微软雅黑"); _font = SKFontManager.Default.GetFontStyles(index).CreateTypeface(0); _barrage = new Barrage(_font, (float) Width, (float) Height - (float) MyGrid.ActualHeight, list); skElement.PaintSurface += SkElement_PaintSurface; Loaded += delegate { myMediaElement.Source = new Uri(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Leagueoflegends.mp4")); }; _ = Task.Run(() => { try { while (true) { Dispatcher.Invoke(() => { skElement.InvalidateVisual(); }); _ = SpinWait.SpinUntil(() => false, 1000 / 60); //每秒60帧 } } catch (Exception e) { } }); } private void SkElement_PaintSurface(object? sender, SKPaintSurfaceEventArgs e) { var canvas = e.Surface.Canvas; canvas.Clear(); _barrage.Render(canvas, _font, e.Info.Width, e.Info.Height, list); } private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { _barrage.AddBarrage(tbBarrage.Text); } } }
实现效果
加载全部内容