歌词控件在好久之前就想写了,但是懒得动手,今天有时间试着写了一下,就拿网易云音乐(PC)的来仿吧~
打开vs之前我想得先了解下歌词的结构,鉴于我们的目的是模仿网易云音乐,所以歌词结构也借用它的好了。
打开网易云音乐的下载目录并没有找到歌词文件,可能是在线获取的,本来想放弃换QQ音乐之类的,但是意外地通过搜索引擎找到了网易云音乐获取歌词的api接口,真是喜出望外。
歌词接口地址
http://music.163.com/api/song/media?id=歌曲ID
填写歌曲的id即可获取到json格式的数据,如下:
{"songStatus":0,"lyricVersion":10,"lyric":"[by:Esida]\n[ti:起风了]\n[ar:买辣椒也用劵]\n[al:起风了]\n[by:九九Lrc歌词网~www.99Lrc.net]\n\n\n[00:04.00]原曲: ヤキモチ\n\n\n\n[00:20.00]后期: 圣雨轻纱\n\n[00:24.00]海报:不 咸\n\n\n\n[00:28.64]这一路上走走停停 顺着少年漂流的痕迹\n\n[00:35.11]迈出车站的前一刻 竟有些犹豫\n\n[00:41.08]不禁笑这近乡情怯 仍无可避免\n\n[00:46.49]而长野的天 依旧这么暖 风吹起了从前\n\n[00:52.02]从前初识这世间 万般流连 看着天边似在眼前\n\n[00:59.50]也甘愿赴汤蹈火去走它一遍\n\n[01:04.52]如今走过这世间 万般流连 翻过岁月不同侧脸\n\n[01:11.75]措不及防闯入你的笑颜\n\n[01:17.37]我曾难自拔于世界之大 也沉溺于其中梦话 不得真假 不做挣扎 不惧笑话\n\n[01:30.39]我曾将青春翻涌成她 也曾指尖弹出盛夏 心之所动 且就随缘去吧\n\n[01:42.40]逆着光行走 任风吹雨打\n\n[01:49.89]-M-\n\n[01:59.14]短短的路走走停停 也有了几分的距离\n\n[02:05.20]不知抚摸的是故事 还是段心情\n\n[02:11.22]也许期待的不过是 与时间为敌\n\n[02:16.94]再次看到你 微凉晨光里 笑的很甜蜜\n\n[02:22.29]从前初识这世间 万般流连 看着天边似在眼前\n\n[02:29.58]也甘愿赴汤蹈火去走它一遍\n\n[02:34.42]如今走过这世间 万般流连 翻过岁月不同侧脸\n\n[02:41.87]措不及防闯入你的笑颜\n\n[02:47.23]我曾难自拔于世界之大 也沉溺于其中梦话 不得真假 不做挣扎 不惧笑话\n\n[03:00.13]我曾将青春翻涌成她 也曾指尖弹出盛夏 心之所动 且就随缘去吧\n\n[03:15.51]-=-\n\n[03:38.30]晚风吹起你鬓间的白发 抚平回忆留下的疤 你的眼中 明暗交杂 一笑生花\n\n[03:50.53]暮色遮住你蹒跚的步伐 走进床头藏起的画 画中的你 低着头说话\n\n[04:03.05]我仍感叹于世界之大 也沉醉于儿时情话 不剩真假 不做挣扎 无谓笑话\n\n[04:15.34]我终将青春还给了她 连同指尖弹出的盛夏 心之所动 就随风去了\n\n[04:27.63]以爱之名 你还愿意吗\n\n[04:35.36]-E-","code":200}
我们需要用到的数据只有lyric部分。
可以看到歌词的结构很简单,[歌曲时间]歌词部分,然后用\n作为换行符。
看完歌词结构思路已经有了,首先将时间和歌词提取出来,然后播放歌曲,判断歌曲播放的进度,然后跟歌词对应的时间作交互即可。这里我们要做的控件效果如网易云音乐(以下简称网音)的效果差不多即可,先看下网音的效果动图:
播放到指定歌词时字体颜色变成白色,并且随着歌词的位置进行滚动视图使焦点相对于定位在中间的位置。
ok要做的效果和思路我们都有了,开始写代码,新建项目,新建一个用户控件命名为:LrcView,前台代码修改为:
<UserControl x:Class="网易云音乐歌词显示控件.Controls.LrcView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid> <ScrollViewer Name="c_scrollviewer"> <StackPanel Name="c_lrc_items"> </StackPanel> </ScrollViewer> </Grid> </UserControl>
应该很好理解,用一个scrollviewer包住stackpanel,把歌词(textblock)添加到stackpanel里。(*只要不设置固定高度stackpanel会随着包含的内容高度的增大而增大,这时候套个scrollviewer就可以滚动stackpanel了。
转到控件后台代码,为了方便修改歌词颜色等操作,我们建立一个歌词模型类:
#region 歌词模型 public class LrcModel { /// <summary> /// 歌词所在控件 /// </summary> public TextBlock c_LrcTb { get; set; } /// <summary> /// 歌词字符串 /// </summary> public string LrcText { get; set; } /// <summary> /// 时间(读取格式参照网易云音乐歌词格式:xx:xx.xx,即分:秒.毫秒,秒小数点保留2位。如:00:28.64) /// </summary> public string Time { get; set; } } #endregion
我暂时想到应该有的属性就这些了。下面是需要用到的变量
#region 变量 //歌词集合 public Dictionary<string, LrcModel> Lrcs = new Dictionary<string, LrcModel>(); //当前焦点所在歌词集合位置 public int FoucsLrcLocation { get; set; } = -1; //非焦点歌词颜色 public SolidColorBrush NoramlLrcColor = new SolidColorBrush(Colors.Black); //焦点歌词颜色 public SolidColorBrush FoucsLrcColor = new SolidColorBrush(Colors.OrangeRed); #endregion
接着是加载歌词了。将网音歌词接口获取到的歌词复制出来,用\n切割,再用正则表达式取出 [时间] 时间部分,剩下的就是时间对应的歌词了。
#region 加载歌词 public void LoadLrc(string lrcstr) { //循环以换行\n切割出歌词 foreach (string str in lrcstr.Split(‘\n‘)) { //过滤空行 if (str.Length > 0) { //歌词时间 string time = GetTime(str); //歌词 string lrc = str.Replace("[" + time + "]", ""); //过滤空行 if (time.Length > 0) { //歌词显示textblock控件 TextBlock c_lrcbk = new TextBlock(); //赋值 c_lrcbk.Text = lrc; if (c_lrc_items.Children.Count > 0) { //增加一些行间距,see起来不那么拥挤~ c_lrcbk.Margin = new Thickness(0, 10, 0, 0); } //添加到集合,方便日后操作 Lrcs.Add(time, new LrcModel() { c_LrcTb = c_lrcbk, LrcText = lrc, Time = time }); //将歌词显示textblock控件添加到界面中显示 c_lrc_items.Children.Add(c_lrcbk); } } } } //正则表达式提取时间 public string GetTime(string str) { Regex reg = new Regex(@"\[(?<time>.*)\]", RegexOptions.IgnoreCase); return reg.Match(str).Groups["time"].Value; } #endregion
接下来是歌词滚动,也就是输入当前播放的音乐所在时间后将歌词标亮以及滚动到相对中间的位置。
计算时间部分代码来源于:http://www.cnblogs.com/hongfei/archive/2013/03/11/2953366.html
#region 歌词滚动 public void LrcRoll(string nowtime) { nowtime = "00:" + nowtime; if (FoucsLrcLocation < 0) { //音乐开始时歌词焦点到第一句 FoucsLrcLocation = 0; Lrcs.Values.ToList()[FoucsLrcLocation].c_LrcTb.Foreground = FoucsLrcColor; } else { //循环获取歌词 for (int i = FoucsLrcLocation + 1; i < Lrcs.Values.Count; i++) { LrcModel lrc = Lrcs.Values.ToList()[i]; //调整格式方便计算 string lrctime = "00:" + lrc.Time; //计算当前音乐播放时间与歌词时间的差值,大于等于0时代表到达歌词位置 double s = DateDiff(Convert.ToDateTime(nowtime), Convert.ToDateTime(lrctime)); if (s >= 0) { //取消当前焦点歌词 Lrcs.Values.ToList()[FoucsLrcLocation].c_LrcTb.Foreground = NoramlLrcColor; //给歌词控件设置颜色突出显示 lrc.c_LrcTb.Foreground = FoucsLrcColor; //重新设置当前歌词位置 FoucsLrcLocation = i; ResetLrcviewScroll(); //Debug.WriteLine("nowtime:" + nowtime + ",lrctime:" + lrctime + ",s:" + s); break; } } } } //计算时间差 public double DateDiff(DateTime DateTime1, DateTime DateTime2) { double dateDiff = 0; try { TimeSpan ts1 = new TimeSpan(DateTime1.Ticks); TimeSpan ts2 = new TimeSpan(DateTime2.Ticks); TimeSpan t = ts1 - ts2; TimeSpan ts = ts1.Subtract(ts2).Duration(); dateDiff = t.Seconds + t.Milliseconds; } catch { } return dateDiff; } #endregion #region 调整歌词控件滚动条位置 public void ResetLrcviewScroll() { //获得焦点歌词位置 GeneralTransform gf = Lrcs.Values.ToList()[FoucsLrcLocation].c_LrcTb.TransformToVisual(c_lrc_items); Point p = gf.Transform(new Point(0, 0)); //滚动条当前位置 Debug.WriteLine(c_scrollviewer.VerticalOffset + "/" + p.Y); //计算滚动位置(p.Y是焦点歌词控件(c_LrcTb)相对于父级控件c_lrc_items(StackPanel)的位置) //拿焦点歌词位置减去滚动区域控件高度除以2的值得到的【大概】就是歌词焦点在滚动区域控件的位置 double os = p.Y - (c_scrollviewer.ActualHeight / 2) + 10; //滚动 c_scrollviewer.ScrollToVerticalOffset(os); } #endregion
控件部分写完了,现在重新生成项目,我们调用试试效果。
打开MainWindow.xaml,修改为:
<Window 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:local="clr-namespace:网易云音乐歌词显示控件" xmlns:Controls="clr-namespace:网易云音乐歌词显示控件.Controls" x:Class="网易云音乐歌词显示控件.MainWindow" mc:Ignorable="d" Title="网易云音乐歌词显示控件" Height="350" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="80"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid Grid.Row="0"> <StackPanel Height="25" Orientation="Horizontal"> <Button Name="pbtn" Width="100" Content="播放" Click="Button_Click"></Button> <Button Name="sbtn" Width="100" Content="暂停" Click="Button_Click_1"></Button> <TextBlock Name="metime" VerticalAlignment="Center" Text="00:00"></TextBlock> <MediaElement Name="me" LoadedBehavior="Manual"/> </StackPanel> </Grid> <Controls:LrcView x:Name="LrcView" Grid.Row="1" Margin="10"/> </Grid> </Window>
后台代码很简单,播放音乐,然后给我们写好的控件赋值(加载歌词、将音乐当前播放时间填入)而已。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Media; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace 网易云音乐歌词显示控件 { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { DispatcherTimer dt; public MainWindow() { InitializeComponent(); string lrc = "[00:04.00]原曲: ヤキモチ\n\n\n\n[00:20.00]后期: 圣雨轻纱\n\n[00:24.00]海报:不 咸\n\n\n\n[00:28.64]这一路上走走停停 顺着少年漂流的痕迹\n\n[00:35.11]迈出车站的前一刻 竟有些犹豫\n\n[00:41.08]不禁笑这近乡情怯 仍无可避免\n\n[00:46.49]而长野的天 依旧这么暖 风吹起了从前\n\n[00:52.02]从前初识这世间 万般流连 看着天边似在眼前\n\n[00:59.50]也甘愿赴汤蹈火去走它一遍\n\n[01:04.52]如今走过这世间 万般流连 翻过岁月不同侧脸\n\n[01:11.75]措不及防闯入你的笑颜\n\n[01:17.37]我曾难自拔于世界之大 也沉溺于其中梦话 不得真假 不做挣扎 不惧笑话\n\n[01:30.39]我曾将青春翻涌成她 也曾指尖弹出盛夏 心之所动 且就随缘去吧\n\n[01:42.40]逆着光行走 任风吹雨打\n\n[01:49.89]-M-\n\n[01:59.14]短短的路走走停停 也有了几分的距离\n\n[02:05.20]不知抚摸的是故事 还是段心情\n\n[02:11.22]也许期待的不过是 与时间为敌\n\n[02:16.94]再次看到你 微凉晨光里 笑的很甜蜜\n\n[02:22.29]从前初识这世间 万般流连 看着天边似在眼前\n\n[02:29.58]也甘愿赴汤蹈火去走它一遍\n\n[02:34.42]如今走过这世间 万般流连 翻过岁月不同侧脸\n\n[02:41.87]措不及防闯入你的笑颜\n\n[02:47.23]我曾难自拔于世界之大 也沉溺于其中梦话 不得真假 不做挣扎 不惧笑话\n\n[03:00.13]我曾将青春翻涌成她 也曾指尖弹出盛夏 心之所动 且就随缘去吧\n\n[03:15.51]-=-\n\n[03:38.30]晚风吹起你鬓间的白发 抚平回忆留下的疤 你的眼中 明暗交杂 一笑生花\n\n[03:50.53]暮色遮住你蹒跚的步伐 走进床头藏起的画 画中的你 低着头说话\n\n[04:03.05]我仍感叹于世界之大 也沉醉于儿时情话 不剩真假 不做挣扎 无谓笑话\n\n[04:15.34]我终将青春还给了她 连同指尖弹出的盛夏 心之所动 就随风去了\n\n[04:27.63]以爱之名 你还愿意吗\n\n[04:35.36]-E-"; LrcView.LoadLrc(lrc); dt = new DispatcherTimer(); dt.Interval = TimeSpan.FromSeconds(1); dt.Tick += (e, c) => { string time = me.Position.ToString("mm") + ":" + me.Position.ToString("ss") + "." + me.Position.ToString("ff"); metime.Text = me.Position.ToString("mm") + ":" + me.Position.ToString("ss"); LrcView.LrcRoll(time); }; Play(false); me.Source = new Uri(@"C:\CloudMusic\qfl.mp3", UriKind.RelativeOrAbsolute); } void Play(bool s) { if (s) { sbtn.IsEnabled = true; pbtn.IsEnabled = false; } else { sbtn.IsEnabled = false; pbtn.IsEnabled = true; } } private void Button_Click(object sender, RoutedEventArgs e) { Play(true); dt.Start(); me.Play(); } private void Button_Click_1(object sender, RoutedEventArgs e) { Play(false); dt.Stop(); me.Pause(); } } }
这里用了一个计时器,在播放音乐开始时启动,每隔1秒执行一次控件的LrcRoll方法去设置对应歌词颜色和滚动位置。
启动程序看看效果吧~
写到这发现了几个问题:
1,歌词模型中的歌词属性暂时似乎是用不到的。。。;
2,Dictionary作为数据集合似乎也是“大材小用”了;
3,当用户调整歌曲进度后退时会导致歌词定位错误(这也是我写完了才发现的,好马虎),逻辑上有些问题。
继续优化一下吧。。。。
2018年1月11日16:39:10
想试试关键词c#歌词滚动能不能搜到我这篇博客时发现我写的代码跟有几篇文章的代码几近相同,我想发自内心说明一下,我绝对没有复制网上的代码来改,有复制的地方都会在项目文件或博客中特别标注的,要信我啊信我啊。。。。。。。。。。。
2018年1月11日16:48:58