今天把拖了好久的首页写完了,包括了三个:随机专辑、最多播放歌曲列表以及最近添加专辑
其实这个APP和网易云最大的不同就是这些歌都是你自己收集起来听的,所以那些推荐的算法都不需要了
首页最麻烦的是横向列表和纵向列表混排,不过也不是很复杂,主要是一定记得给固定高度和宽度,或者是最大宽度最大高度什么的,不然非常容易报错
首先就是让人又爱又恨的Sliver ,自带的动画省却了好多事情,不过为了这些自动化的功能,导致只有少部分控件支持当他的child 。但是好在官方给了个SliverToBoxAdapter 组件,让不是他child 的组件可以套层皮,这就很nice
布局嘛,就是一个横向滚动的10张随机专辑,下面是一个10首最多播放的纵向歌曲列表,再下面是最近添加的横向的10张专辑,可能把最多播放歌曲改成5个更好看一点?
实现也比较简单,除了亲儿子SliverList 控件之外,比如标题部分要用SliverToBoxAdapter 套一下就可以了,直接看代码吧
因为我要兼容桌面端,桌面除了苹果的魔术鼠标可以左右滑动,一般的鼠标是没有这个功能的,所以需要加一个左右滑动的按钮,给桌面端来点击
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 return CustomScrollView( slivers: <Widget>[ SliverToBoxAdapter( child: Container( padding: leftrightPadding, child: Text(indexLocal, style: titleText1)), ), if (_albums != null && _albums!.length > 0 ) SliverToBoxAdapter( child: MySliverControlBar( title: "随机专辑" , controller: _randomAlbumcontroller, )), if (_albums != null && _albums!.length > 0 ) SliverToBoxAdapter( child: MySliverControlList( controller: _randomAlbumcontroller, albums: _albums!, url: _imageURL!, )) ] );
这是首页build里面的内容,然后MySliverControlBar 里面就很简单,就是一个Row套一个Row这样布局
MySliverControlList 里面其实就是一个横向的ListView
这里注意如果没有桌面端的需求,就是你不用手动控制滚动的话,是不需要写控制器的,但是我需要控制滚动,就要注入一个控制器,控制方法比较简单,点击按钮触发即可
1 2 3 controller.animateTo(controller.offset - _size.width / 2 , duration: Duration (milliseconds: 200 ), curve: Curves.ease);
offset就是位移,大气点,点一次直接移动半屏,省的点半天都不带动的
这样首页就做完了,后面想往上面再加什么内容,那都是分分钟的,目前就这样吧。
另外今天把布局又做了一次调整,把下面的控制部分彻底拿到bottomNavigationBar 里面去了,这样布局看起来简单的多了,后面可能还要再做一次调整就是中间的部分,准备直接把LeftScreen 嵌到每个页面里面去,这样我就可以使用flutter整体路由了,可以去掉一个控制路由的全局变量
最关键的是可以再做个判断,如果是桌面端就显示LeftScreen ,如果是移动端就在bottomNavigationBar 里面直接加导航,这样移动端顶部左侧的那个侧滑的按钮就可以去掉了。左侧空出来可以给Windows端留个位置,当客户端是Windows的时候,那个搜索和设置按钮可以贴到左边去,因为Windows的放大缩小和叉叉是在右边的,我为了好看设置了去掉标题栏,所以不能挡住叉叉
移动端解决 这是多端开发的时候遇到的问题,因为Scaffold 会根据不同设备去适配不同的窗体大小,而动态的取窗口大小的方法就是MediaQuery.of(context).size ,这在桌面端开发的时候十分方便,但是在手机端遇到键盘弹出的时候就会出现问题
如果你的panel高度用了size做强制定义,但是软键盘弹出其实窗体上滑的高度也是用这个计算的,所以上滑后你的panel又把键盘的位置顶下去了,就会导致软键盘频繁闪烁不会保持弹出的状态,而且页面不停的刷新
如果想要做强制定位最好的方法还是使用window.physicalSize ,,因为移动端的窗体大小是不会有变化的,然后为了做多端适应,最终使用的宽度应该是:window.physicalSize.width / window.devicePixelRatio ,同时在Scaffold 里面加上resizeToAvoidBottomInset: false 避免微小位移导致的报错
桌面端解决 移动端设置好之后,桌面端会存在这样一个问题,window.physicalSize 是调用一次的。但是,桌面端是可以拖动放大缩小的,如果我们在桌面端继续使用window.physicalSize 就会导致放大缩小的时候panel还是原来定好的高度,这时候我们又希望它动态做出调整,有两个方法可以做到,一个是添加一个窗体的监听器,当窗体大小变化的时候重新获取window.physicalSize,还有一种最简单的方法是继续使用window.physicalSize ,但是把它放到全局变量里面去
1 2 3 4 5 ValueNotifier<double > windowsWidth = ValueNotifier<double >(window .physicalSize.width / window .devicePixelRatio); ValueNotifier<double > windowsHeight = ValueNotifier<double >(window .physicalSize.height / window .devicePixelRatio);
然后在最外层build的时候,就是Saffold 外面,刚刚好可以获取到MediaQuery.of(context).size 的地方做一次值的变化,然后其他地方是不需要做Value监听的,因为窗体变化的时候flutter会自动rebuild saffold ,这个时候优先触发这两个数值的改变,然后才去渲染后面的,渲染的时候自然取的就是新值了,不需要再做监听了,能偷点懒就偷点懒吧,那个值监听又要套一层,各种套娃看着太难受了
1 2 3 4 5 6 if (!isMobile.value) { windowsWidth.value = MediaQuery.of(context).size.width; windowsHeight.value = MediaQuery.of(context).size.height; }
这样一来,就可以用最少的代码来完成移动端和桌面端的适配了
正在播放的磨砂效果 原本我是想“规矩”一点,把顶部和底部都拆到AppBar 和bottomSheet 里面去,但是在写正在播放页面的时候改变了想法,因为做底部弹窗的时候,我希望直接弹出一个整屏的页面,而不是卡在AppBar 和bottomSheet 中间的,这样即便做了磨砂玻璃的效果,看上去也…其实还挺好看,不过嗯,我有强迫症…
这种正在播放的弹窗感觉就…不好看,所以还是延续我的暴力思路,直接把顶部和底部都写到自己的控件里面实现全屏的效果,同时在弹出的页面可以重新布局播放控制的按钮,这样给移动多放点按钮上去。
这里设置的是**BoxDecoration(color: Colors.black.withOpacity(0.7))**,我觉得其实可以设置到0.8左右
实现方法就是用Stack套一层,看代码
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 Stack( children: <Widget>[ ConstrainedBox( constraints: const BoxConstraints.expand(), child: ValueListenableBuilder<Map >( valueListenable: activeSong, builder: ((context, value, child) { return ClipRRect( borderRadius: BorderRadius.circular(15 ), child: (value.isEmpty) ? Image.asset("assets/images/logo.jpg" ) : CachedNetworkImage( imageUrl: value["url" ], fit: BoxFit.cover, placeholder: (context, url) { return AnimatedSwitcher( child: Image.asset("assets/images/logo.jpg" ), duration: const Duration (milliseconds: imageMilli), ); }, ), ); }))), Center( child: ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10.0 , sigmaY: 10.0 ), child: Container( width: windowsWidth.value, height: windowsHeight.value, decoration: BoxDecoration(color: Colors.black.withOpacity(0.8 )), child:...
歌词 歌词的实现使用的是pub上唯一的也是很不错的第三方组件flutter_lyric: ^2.0.4+6 ,实现起来并不难作者写了个demo而且把它放到网上去了。可以把源码里面的example下载下来对应着看就好了,歌词本身是“画出来的”,所以相当稳定,由于我的播放控制和进度条都是用StreamBuilder 获取的,所以需要定义的东西比作者的例子里面少了不少
1 2 3 4 5 var lyricModel = LyricsModelBuilder.create().bindLyricToMain(normalLyric).getModel(); var lyricUI = UINetease(); var lyricPadding = 40.0 ; var playing = true ;
然后在initState 里面加两条
1 lyricUI.highlight = true ;
然后构建歌词读取UI的Widget,并把它放到想要的位置
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 Widget _buildReaderWidget() { return StreamBuilder<PositionData>( stream: _positionDataStream, builder: (context, snapshot) { final positionData = snapshot.data; final position = positionData?.position.inMilliseconds ?? 0 ; return LyricsReader( padding: EdgeInsets.symmetric(horizontal: lyricPadding), model: lyricModel, position: position, lyricUi: lyricUI, playing: playing, size: Size(windowsWidth.value / 2 , windowsHeight.value / 2 ), emptyBuilder: () => Center( child: Text( "No lyrics" , style: lyricUI.getOtherMainTextStyle(), ), ), selectLineBuilder: (progress, confirm) { return Row( children: [ IconButton( onPressed: () { LyricsLog.logD("点击事件" ); confirm.call(); widget.player.seek(Duration (milliseconds: progress)); }, icon: Icon(Icons.play_arrow, color: kGrayColor)), Expanded( child: Container( decoration: BoxDecoration(color: kGrayColor), height: 1 , width: double .infinity, ), ), Text( formatDurationMilliseconds(progress),, style: TextStyle(color: kGrayColor), ) ], ); }, ); }); } String formatDurationMilliseconds(int _duration) { Duration _dura = Duration (milliseconds: _duration); if (_dura.inHours != 0 ) { String hours = _dura.inHours.toString().padLeft(0 , '2' ); String minutes = _dura.inMinutes.remainder(60 ).toString().padLeft(2 , '0' ); String seconds = _dura.inSeconds.remainder(60 ).toString().padLeft(2 , '0' ); return "$hours :$minutes :$seconds " ; } else { String minutes = _dura.inMinutes.remainder(60 ).toString().padLeft(2 , '0' ); String seconds = _dura.inSeconds.remainder(60 ).toString().padLeft(2 , '0' ); return "$minutes :$seconds " ; } }
至此歌词问题就解决了,后面只用传入不同的normalLyric 就可以了,需要注意不要跑到进度条的值监控里面了,不然一直RUN的话歌词会不停的闪烁
只是目前控件有一个问题,当你拖动歌词定位到某一句的时候,点击确定确实可以定位到想要的位置,但是歌词的滚动会被卡住。我看作者的demo里面也是这个样子,所以给作者提了个issue ,看看后面怎么解决吧
然后关键的就是…怎么批量去找歌词…说不得可能还是要去用网易云的api了…