Flutter writes a Nas music player (4)
2023-01-14 16:50 2023-01-15 15:54 ≈ 2.3kWords ≈ 13Minutes

Note

This article was automatically translated using Google

Today I finished the long-awaited homepage, including three: random albums, most played songs list, and recently added albums

In fact, the biggest difference between this APP and Netease Cloud is that you collect and listen to these songs yourself, so those recommended algorithms are not needed

CustomScrollView

The most troublesome thing about the homepage is the mixed arrangement of horizontal and vertical lists, but it is not very complicated. The main thing is that you must remember to set a fixed height and width, or the maximum width and maximum height, otherwise it is very easy to report an error

The first is Sliver, which makes people love and hate. The built-in animation saves a lot of things, but because of these automatic functions, only a small number of controls support it as its child. But fortunately, the official gave a SliverToBoxAdapter component, so that components that are not his child can be layered, which is very nice

The layout is a horizontal scrolling list of 10 random albums, below is a vertical list of the 10 most played songs, and below that is the most recently added horizontal 10 albums, maybe changing the most played songs to 5 looks better?

WechatIMG650

The implementation is also relatively simple. In addition to the pro-son SliverList control, for example, the title part needs to be set with SliverToBoxAdapter, just look at the code

Because I want to be compatible with the desktop, the desktop can slide left and right except Apple’s Magic Mouse, the general mouse does not have this function, so I need to add a button that slides left and right to click on the desktop

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>[
//This is the home page text
SliverToBoxAdapter(
child: Container(
padding: leftrightPadding,
child: Text(indexLocal, style: titleText1)),
),
if (_albums != null && _albums!.length > 0)
SliverToBoxAdapter(
//This set of horizontal title bars, including text and left and right buttons, I wrote a public file for easy application
child: MySliverControlBar(
title: "Random Album",
controller: _randomAlbumcontroller,
)),
if (_albums != null && _albums!.length > 0)
SliverToBoxAdapter(
//This set of horizontal scrolling pictures, I wrote a public file for easy application
child: MySliverControlList(
controller: _randomAlbumcontroller,
albums: _albums!,
url: _imageURL!,
))
]
);

This is the content in the homepage build, and then MySliverControlBar is very simple, it is a Row set a Row layout

MySliverControlList is actually a horizontal ListView

Note here that if there is no need for the desktop side, that is, if you don’t need to manually control the scrolling, you don’t need to write a controller, but if I need to control the scrolling, I need to inject a controller. The control method is relatively simple, just click the button to trigger

1
2
3
controller.animateTo(controller.offset - _size.width / 2,
duration: Duration(milliseconds: 200),
curve: Curves. ease);

Offset is the displacement, atmospheric point, click once to move half the screen directly, the saved point will not be driven for half a day

In this way, the homepage is finished, and if you want to add any content to it later, it will be done in minutes, so let’s leave it at that.

In addition, the layout was adjusted again today, and the following control part was completely taken into bottomNavigationBar, so that the layout looks much simpler, and there may be another adjustment later, which is the middle part. Directly embed LeftScreen into each page, so that I can use flutter’s overall routing, and I can remove a global variable that controls routing

The most important thing is to make another judgment. If it is on the desktop, it will display LeftScreen. If it is on the mobile terminal, add navigation directly in bottomNavigationBar, so that the side-sliding button on the top left of the mobile terminal It can be removed. The left side is free to leave a place for Windows. When the client is Windows, the search and setting buttons can be pasted to the left, because the zoom in and out of Windows and the fork are on the right. I removed it for the sake of looking good. The title bar, so it can’t block the fork

Using MediaQuery causes the soft keyboard to not pop up

Mobile solution

This is a problem encountered during multi-terminal development, because Scaffold will adapt to different window sizes according to different devices, and the method to dynamically get the window size is *MediaQuery.of(context).size *, this is very convenient when developing on the desktop, but there will be problems when the keyboard pops up on the mobile phone

If the height of your panel is defined by size, but the soft keyboard pops up, the height of sliding up the form is also calculated by this, so after sliding up, your panel pushes down the position of the keyboard, which will cause the soft keyboard Frequent flashing will not keep the pop-up state, and the page keeps refreshing

If you want to do forced positioning, the best way is to use window.physicalSize, because the size of the form on the mobile terminal will not change, and then in order to do multi-terminal adaptation, the final width used should be: window .physicalSize.width / window.devicePixelRatio, and add resizeToAvoidBottomInset: false in Scaffold to avoid errors caused by small displacements

Desktop solution

After the mobile terminal is set up, there will be such a problem on the desktop terminal, window.physicalSize is called once. However, the desktop side can be dragged to zoom in and out. If we continue to use window.physicalSize on the desktop side, the panel will still be at the original height when zooming in and out. At this time, we want it to make dynamic adjustments , there are two ways to do it, one is to add a window listener to reacquire window.physicalSize when the size of the window changes, and the easiest way is to continue to use window.physicalSize, But put it in a global variable

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);

Then when the outermost layer is built, it is outside Saffold, just right where MediaQuery.of(context).size can be obtained to make a value change, and then there is no need to do Value in other places Listening, because flutter will automatically rebuild saffold when the window changes. At this time, the change of these two values ​​will be triggered first, and then the latter will be rendered. When rendering, the new value will naturally be taken, no need Do the monitoring again, if you can be a little lazy, just be a little lazy, the value monitoring needs to be set up again, all kinds of nesting dolls are too uncomfortable to watch

1
2
3
4
5
6
//Use this to dynamically monitor form changes when it is not a mobile terminal
//If it is a mobile terminal, the form will not change
if (!isMobile. value) {
windowsWidth.value = MediaQuery.of(context).size.width;
windowsHeight.value = MediaQuery.of(context).size.height;
}

In this way, the adaptation of the mobile terminal and the desktop terminal can be completed with the least code

The matte effect being played

Originally, I wanted to be “rule” and split the top and bottom into AppBar and bottomSheet, but I changed my mind when writing the playing page, because when making the bottom pop-up window, I hope that a full-screen page will pop up directly, instead of being stuck between AppBar and bottomSheet, so that even if the effect of frosted glass is done, it looks…actually pretty good, but um , I have OCD…

WechatIMG21777

This kind of pop-up window that is playing feels…not good-looking, so I still continue my violent thinking, directly write the top and bottom into my own control to achieve a full-screen effect, and at the same time, I can rearrange the playback control on the pop-up page button, so put more buttons on it for movement.

The setting here is BoxDecoration(color: Colors.black.withOpacity(0.7)), I think it can actually be set to about 0.8

The implementation method is to use Stack to cover a layer, see the code

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(
//这里设置背景图片,其实可以直接Image的,不需要做个圆角矩形,我是复制过来的,无所谓啦
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:... //放上层控件

1673685301772

Lyrics

The implementation of the lyrics uses the only and very good third-party component flutter_lyric: ^2.0.4+6 on the pub. It is not difficult to implement. The author wrote a demo and put it online. You can download the example in the source code and look at it accordingly. The lyrics themselves are “drawn”, so they are quite stable. Since my playback control and progress bar are obtained with StreamBuilder, I need to define There are a lot less things in the author’s example

1
2
3
4
5
var lyricModel =
LyricsModelBuilder.create().bindLyricToMain(normalLyric).getModel(); //The lyrics file normalLyric is just a string, just copy it
var lyricUI = UINetease(); //Get the UI file, you can also change it later, font color, etc.
var lyricPadding = 40.0; //Copied directly, can be changed
var playing = true; //This is to control the playback of the lyrics. The author uses the linkage button to control the highlighting of the lyrics. I don’t need linkage to pass true directly, and I need to add it to my own pause button later (actually, it seems to be OK if you don’t add it ...it will always be true)

Then add two lines in initState

1
lyricUI.highlight = true; //Set highlight

Then build the Widget of the lyrics reading UI and put it in the desired position

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;
//My positioning information is controlled by StreamBuilder, so I have to wrap my head around it
return LyricsReader(
padding: EdgeInsets.symmetric(horizontal: lyricPadding),
model: lyricModel, //What needs to be noted here is that because this StreamBuilder is running all the time, don’t dare to assign variable values here, otherwise it will run all the time. If you make a global variable of lyrics to read like me, the detection value Be sure to put it in the outer layer of StreamBuilder! ! ! Otherwise the subtitles will keep flashing
position: position,
lyricUi: lyricUI,
playing: playing,
// set the size
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("click event");
confirm. call();
//Here is the position to update seek, but my progress bar has been monitored, so I don’t need setState, just change it directly
//setState(() {
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(
//This is a method written by myself, which is used to change Duration to a string such as hours, minutes, and seconds
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";
}
}

At this point, the lyrics problem is solved, and it is enough to pass in different normalLyric later. You need to be careful not to run into the value monitoring of the progress bar, otherwise the lyrics will keep flashing if you keep running

It’s just that there is a problem with the current control. When you drag the lyrics to a certain sentence, you can indeed locate the desired position by clicking OK, but the scrolling of the lyrics will be stuck. I think the same is true in the author’s demo, so I raised an issue to the author, let’s see how to solve it later

1673766650968

Then the key thing is… how to find lyrics in batches… maybe I have to use Netease Cloud’s api…