呐喊! 想要通过路由做局部刷新的,千万要自己写路由,不要用flutter自带的,会让你感受到崩溃。今天为了右侧部分的局部刷新切到全局路由,改了大部分代码之后发现,根本不能在保留左侧和底部Widget 的前提下动态刷新,一气之下重新写了一个终于好用了,但是前后浪费了好久。没办法,为了到时候调移动端的时候,左边可以做为侧滑窗体独立出来,决定用ValueNotifier 硬写路由,然后把左侧的当前页高亮显示去掉了,主要有Detail 页面,虽然可以跟踪到上级,不过感觉要不要无所谓。我写了一个硕大的40号字体标题放在那,怎么也不会看不到
目前已经算是可以使用了,歌手和专辑以及播放的全部逻辑都做完了,后面还有就是首页,无非就是查询点正在播放之类的方便使用。还有两个重头,搜索和歌词,不知道直接转码能不能实现繁简通查,不行的话还要用API,就浪费查询时间
1. 专辑瀑布流 这个是我一直想要的,自从用了Jellyfin之后,感觉以前电影都白看了,这种电视幕墙是比较帅的,瀑布流有现成的轮子可以用flutter_staggered_grid_view 实现起来非常简单,在pubspec.yaml
里面添加然后flutter pub get
即可
它本身支持多种呈现方式详细可以去主页看,不过我选了最合适大量图片的MasonryGridView
,和构建ListView一样的,这里有三个属性需要定义
crossAxisCount: 多少列
mainAxisSpacing: 行间距
crossAxisSpacing: 列间距
我是建议做一个计算,因为你固定Count 的话会导致一个问题,窗体拖大会让图片特别大,就很怪
1 2 3 4 5 6 Size _size = MediaQuery.of(context).size; double _rightWidth = (_size.width - 160 ) / 180 ;int _count = _rightWidth.truncate();
这样一来你怎么拖动窗体这个图片宽度只会在小范围内变化,不会特别夸张,构建也很简单
1 2 3 4 5 6 7 8 MasonryGridView.count( crossAxisCount: _count, mainAxisSpacing: 10 , crossAxisSpacing: 10 , itemCount: _albums!.length, itemBuilder: (context, index) { return ... }
里面可以封一个Widget 的组合,比如图片加两行文字这样,效果就出来了
当然啦,可以做图片缓存,我本来也是想要做的,只是没想好到底是保存在文件夹里还是转base64存到数据库里面
其实不做缓存也可以,图片懒加载的效果还是可以的,用frameBuilder 套一下可以实现渐显效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ClipRRect( borderRadius: BorderRadius.circular(15 ), child: Image.network( _temURL, fit: BoxFit.cover, frameBuilder: (context, child, frame,wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) { return child; } return AnimatedSwitcher( child: frame != null ? child : Image.asset("assets/images/logo.jpg" ), duration: const Duration (milliseconds: 2000 ), ); }, ), )
2. 音乐播放 播放一首歌很简单,直接setAudioSource
就可以了,麻烦的是播放列表,以及为了控制播放列表的按钮操作。比如前一首没有了,但是按钮没有置灰点了就会报错之类的,所有的控制都使用ValueNotifier
来进行,我算是把它用明白了
2.1. 设置监控变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ValueNotifier<String > activeSongValue = ValueNotifier<String >("1" ); ValueNotifier<List > activeList = ValueNotifier<List >([]); ValueNotifier<int > activeIndex = ValueNotifier<int >(0 ); ValueNotifier<Map > activeSong = ValueNotifier<Map >(Map ()); ValueNotifier<bool > isShuffleModeEnabledNotifier = ValueNotifier<bool >(false ); ValueNotifier<bool > isFirstSongNotifier = ValueNotifier<bool >(true ); ValueNotifier<bool > isLastSongNotifier = ValueNotifier<bool >(true );
2.2. 触发 在歌曲列表里面点击歌曲之后,把id、歌曲List和当前歌曲在列表中的index传过去,然后封一个Map
主要是为了拿歌曲的图片URL,这个在原本的Map里面没有,需要到后台拼一个带服务器地址和身份信息的URL出来,不然直接用Songs的实例更加方便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 onTap: () async { activeSongValue.value = _tem.id; activeList.value = _songs!; activeIndex.value = index; Map _activeSong = new Map (); String _url = await getCoverArt(_tem.id); _activeSong["artist" ] = _tem.artist; _activeSong["url" ] = _url; _activeSong["title" ] = _tem.title; _activeSong["album" ] = _tem.album; activeSong.value = _activeSong; }
2.3. 接收和生成播放列表 这里接收的是activeSongValue
,为什么不直接用activeList
呢,因为如果你接收activeList
的话,在当前列表内点其他的歌,它默认值没有发生变化
1 2 3 4 5 6 7 ValueListenableBuilder<String >( valueListenable: activeSongValue, builder: ((context, value, child) { if (value != "1" ) { setAudioSource(); } return ...
如果是默认值1 就说明你还没选择音乐
值发生变化就开始生成播放列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Future<void > setAudioSource() async { final playlist = ConcatenatingAudioSource( useLazyPreparation: true , shuffleOrder: DefaultShuffleOrder(), children: await _getPlayList(), ); await _player.setAudioSource(playlist, initialIndex: activeIndex.value, initialPosition: Duration .zero); _player.play(); } _getPlayList() async { List <AudioSource> children = []; List _songs = activeList.value; 把触发的时候传的值拿出来 for (var element in _songs) { Songs _song = element; String _url = await getSongStreamUrl(_song.id); children.add(AudioSource.uri(Uri .parse(_url), tag: _song.id)); } return children; }
2.4. 监听播放变化 当播放到下一首的时候,需要把歌曲名字封面这些信息换掉,这时候就需要注入监听来进行操作
Just Audio 有许多不同的事件流,都可以监听:
sequenceStream :这是添加的顺序的播放列表,每当播放列表更改时,这里将产生一个新列表
shuffleModeEnabledStream :乱序播放的监听,打开和关闭此模式,是个布尔值
shuffleIndicesStream :这是一个打乱的整数列表,指向播放列表中的项目sequenceStream
(它本身不会打乱)。shuffle
当音频播放器上调用该方法时,这里将产生一个新列表。
currentIndexStream :此流会在歌曲首次开始时通知当前歌曲的索引。这个整数指向播放列表中的一个音频源sequenceStream
loopModeStream :默认播放列表中的所有歌曲一次。但是,也可以选择重复单首歌曲甚至重复播放列表,就是循环模式,LoopMode
每次循环模式更改时,此流都会产生一个新的值
好了,上面的看看就行了,监听嘛,用一个就能搞定,就是sequenceStateStream ,这里只要上面的五个流之一产生一个新值,都会sequenceStateStream
产生一个组合值 type SequenceState
,一个顶五个,美滋滋
首先在initState 里面加入这个方法调用,然后..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void _listenForChangesInSequenceState() { _player.sequenceStateStream.listen((sequenceState) async { if (sequenceState == null ) return ; final currentItem = sequenceState.currentSource; final _title = currentItem?.tag as String? ; final _tem = await getSong(_title.toString()); activeSongValue.value = _title.toString(); Map _activeSong = new Map (); String _url = await getCoverArt(_tem["id" ]); _activeSong["artist" ] = _tem["artist" ]; _activeSong["url" ] = _url; _activeSong["title" ] = _tem["title" ]; _activeSong["album" ] = _tem["album" ]; activeSong.value = _activeSong; final playlist = sequenceState.effectiveSequence; if (playlist.isEmpty || currentItem == null ) { isFirstSongNotifier.value = true ; isLastSongNotifier.value = true ; } else { isFirstSongNotifier.value = playlist.first == currentItem; isLastSongNotifier.value = playlist.last == currentItem; } }); }
2.5. 快进快退 其实这俩按钮现在很多人都已经取消掉了,因为有进度条啊,那个多方便,不过之前把按钮都写好了,那就写呗,删了多可惜
1 2 3 4 5 6 7 8 9 10 11 12 IconButton( icon: const Icon( Icons.fast_rewind, color: kTextColor, ), onPressed: () { if (widget.player.position.inSeconds - 15 > 0 ) { widget.player.seek( Duration (seconds: widget.player.position.inSeconds - 15 )); } }, ),
这里需要做一个判断,如果你当前的位置减你想要退会的位置小于0,就不要操作,避免报错,前进倒无所谓,加多了直接转下一首了
2.6. 上下首 这里就需要监听前面设置的值了isFirstSongNotifier 和isLastSongNotifier ,不做过滤的话,当在播放第一首的时候点上一首,和在最后一首的时候点下一首都会有问题
1 2 3 4 5 6 7 8 9 10 11 12 13 ValueListenableBuilder<bool >( valueListenable: isLastSongNotifier, builder: (_, isLast, __) { return IconButton( icon: Icon( Icons.skip_next, color: isLast ? badgeDark : kTextColor, ), onPressed: () { (isLast) ? null : widget.player.seekToNext(); }, ); }),
做一个判断,如果有问题的话直接把按钮置灰,然后onPressed传一个null就可以了
2.7. 单曲循环、全部循环、乱序播放、停止、暂停 难点在上面,全部都写好之后,按钮部分就比较简单
单曲循环: player.setLoopMode(LoopMode.one);
全部循环: player.setLoopMode(LoopMode.all);
乱序播放: player.setShuffleModeEnabled(true);
3. 等宽字体 由于在歌曲进度条前后加了数字,所以一个老生常谈的问题出现了,就是数字从1到0的时候会导致后面的进度条有位移,所以这里的数字需要用等宽字体。
整个APP我用了两个字体,一个是NotoSansSC ,这是支持中日韩英的字体,用起来贼happy,是从我当时设置的Jellyfin的备用字体里面拖过来的。当时也是为了显示中日韩英的字幕用的,这里一样可以用。
不过这个字体有个问题,它有两个版本,正常的版本数字和英文都显示没问题的,但是Mono的版本,就是等宽字体会把英文和数字用半角显示,就会变细,看起来头很大,所以我又从Google Fonts里面找了一个小的等宽字体来用,专门用于进度条前面的数字,避免位移
声明Asset
还是在pubspec.yaml里面声明
1 2 3 4 5 6 7 8 9 10 assets: - assets/images/ fonts: - family: NotoSansSC fonts: - asset: assets/fonts/NotoSansSC-Regular.otf - family: ChivoMono fonts: - asset: assets/fonts/ChivoMono-Regular.ttf
局部使用
1 2 3 4 5 Text( formatSongDuration(widget.position), style: TextStyle( color: borderColor, fontFamily: 'ChivoMono' , fontSize: 12 ), )
全局使用
1 2 3 4 5 6 return MaterialApp( debugShowCheckedModeBanner: false , title: 'XiuMusic' , theme: ThemeData(fontFamily: 'NotoSansSC' ), home: MainScreen(), );
4. Todo List 这东西已经玩了一周了,压力大的时候写写代码真心解压,所以我也不着急完成它,慢慢来吧
Todo List 直接在Github上更新了
其实目前已经可以用了,有兴趣的人可以下来玩一玩,XiuMusic