Flutter で Nas 音楽プレーヤーを作成する (4)
2023-01-14 16:50 2023-01-15 15:54 ≈ 2.1k字 ≈ 9分

今日、長い間後回しにしていたホームページを書き終えました。そのうちの 3 つは、ランダム アルバム、よく再生された曲のリスト、最近追加したアルバムです。

実際、このアプリと NetEase Cloud の最大の違いは、これらの曲を自分で収集して聴くため、推奨アルゴリズムが必要ないことです。

CustomScrollView

ホームページで最も厄介な点は、水平リストと垂直リストが混在することですが、それほど複雑ではありません。主なことは、高さと幅、または最大幅と最大高さを固定することを覚えておくことです。それ以外の場合は、非常に簡単です。エラーを報告します。

1 つ目は、人によって好まれたり嫌われたりする スライバー です。ただし、これらの自動化された機能により、彼の 子供 になることができるコントロールはほとんどありません。しかし幸いなことに、公式は SliverToBoxAdapter コンポーネントを提供しているので、 ではないコンポーネントをスキンのレイヤーで覆うことができます。これは非常に優れています。

レイアウトは、ランダムな 10 枚のアルバムの水平スクロール リストです。以下は最も再生された 10 曲の垂直方向のリストで、その下には最近追加された 10 個の水平方向のアルバムのリストが表示されます。の方がいいです?

WechatIMG650

実装も比較的単純です。たとえば、SliverList コントロールに加えて、タイトル部分を SliverToBoxAdapter でカバーする必要があります。

デスクトップと互換性を持たせたいため、デスクトップ上で左右にスライドできる Apple の Magic Mouse を除いて、通常のマウスにはこの機能がないため、デスクトップ用に左右にスライドするボタンを追加する必要があります。クリック。

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!,
))
]
);

これはホームページ ビルドのコンテンツであり、MySliverControlBar の内部は非常に単純で、行の中に 1 行が含まれているだけです。

MySliverControlList は実際には水平の ListView です

ここで、デスクトップ要件がない場合、つまりスクロールを手動で制御する必要がない場合は、コントローラーを作成する必要はありませんが、スクロールを制御する必要がある場合は、制御メソッドを挿入する必要があることに注意してください。比較的単純で、ボタンをクリックするだけでトリガーされます。

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

offsetは非常に雰囲気のあるもので、1 回のクリックで画面の半分が移動します。数回のクリックを節約できます。半日は動きません。

このようにして、後でコンテンツを追加したい場合は、今のところはこれで完了です。

さらに、今日は、下部のコントロール部分を bottomNavigationBar に完全に移動しました。これにより、レイアウトがよりシンプルに見えます。これは、後でもう一度調整する必要があるかもしれません。 LeftScreen を各ページに直接埋め込んで、フラッター全体のルーティングを使用し、ルーティングを制御するグローバル変数を削除できるようにします。

最も重要なことは、デスクトップの場合は LeftScreen を表示し、bottomNavigationBar に直接ナビゲーションを追加して、横にスライドするボタンをオンにすることです。モバイル端末の左上は取り外し可能です。左側は Windows クライアント用の場所を自由に残すことができます。クライアントが Windows の場合、Windows のズームとクロスが右側にあるため、検索と設定のボタンを左側に配置できるため、削除しました。タイトル バーが表示されるため、十字をブロックすることはできません。

MediaQuery を使用するとソフト キーボードがポップアップしなくなる

モバイル ソリューション

Scaffold はさまざまなデバイスに応じてさまざまなウィンドウ サイズに適応し、動的にウィンドウ サイズを取得するメソッドは MediaQuery.of(context).size であるため、これはマルチターミナル開発中に発生する問題です * , これはデスクトップ側で開発する場合には非常に便利ですが、モバイル側でキーボードがポップアップする場合に問題が発生します。

パネルの高さが size を使用して強制的に定義されているが、ソフト キーボードがポップアップする場合、上にスライドしたときのフォームの高さもこれを使用して計算されるため、上にスライドした後、パネルはキーボードの位置を押し上げます。ソフトキーボードで頻繁に点滅するとポップアップ状態が維持されず、ページが更新され続けます。

強制的な配置を行いたい場合、window.physicalSize を使用するのが最善の方法です。モバイル端末上のフォームのサイズは変更されず、複数の端末に適応させるために、使用される最終的な幅は次のとおりです。 window .physicalSize.width / window.devicePixelRatio とし、resizeToAvoidBottomInset: falseScaffold に追加して、小さな変位によって引き起こされるエラーを回避します

デスクトップ ソリューション

モバイル側の設定が完了すると、デスクトップ側で window.physicalSize が一度呼び出されます。ただし、デスクトップ上で window.physicalSize を使用し続ける場合、デスクトップをドラッグして拡大および縮小することができます。この時点では、パネルは元の高さを維持します。を動的に調整するには 2 つの方法があります。1 つはフォームにリスナーを追加し、フォームのサイズが変更されたときに 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 を取得できる場所で値の変更が行われます。その後、他の層で値を変更する必要はありません。 Flutter はフォームが変更されると自動的に saffold を再構築するため、このとき、これら 2 つの値の変更が最初にトリガーされ、その後、レンダリング時に後続の値がトリガーされます。新しい値は自然に取得されるので、必要はありません。可能であれば、もう一度監視を実行しましょう。監視タスクには別のネストが必要です。さまざまなマトリョーシカ人形を見るのはあまりにも不快です。

1
2
3
4
5
6
//当不是移动端的时候使用这个可以动态监听窗体变化
//如果是移动端的话,窗体不会变化
if (!isMobile.value) {
windowsWidth.value = MediaQuery.of(context).size.width;
windowsHeight.value = MediaQuery.of(context).size.height;
}

このようにして、モバイルとデスクトップの間の適応を最小限のコードで完了できます。

現在再生中のマットエフェクト

当初は、もっと「規律正しく」、上部と下部を AppBarbottomSheet に移動したかったのですが、再生ページを作成しているときに考えが変わりました。下部のポップアップを作成するときに、ウィンドウ内で、AppBarbottomSheet の間に挟まれるのではなく、全画面ページが直接ポップアップ表示されることを願っています。この方法では、たとえすりガラス効果があるとしても…実際はとてもいいのですが、うーん、私は強迫性障害を持っています…

WechatIMG21777

この種の再生中のポップアップ ウィンドウは見苦しいと感じたので、暴力的なアイデアを続けて、再生コントロールの上部と下部を自分のコントロールに直接書き込み、同時に全画面効果を実現します。ポップアップページのボタンで並べ替えることができるので、さらにボタンを配置して移動します。

ここで設定しているのは**BoxDecoration(color: Colors.black.withOpacity(0.7))**で、実際には0.8程度に設定できると思います。

実装方法はStackを使って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
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

### 歌詞

歌詞の実装には、pub 上の唯一の非常に優れたサードパーティ コンポーネント flutter_lyric: ^2.0.4+6 を使用します。実装は難しくありません。デモを作成してオンラインに公開します。ソース コードのサンプルをダウンロードして、それを読み込むことができます。歌詞自体は「描画」されているため、非常に安定しています。再生コントロールとプログレス バーは StreamBuilder を使用して取得するため、定義する必要があります。著者の例よりもはるかに少ないものがあります。

1
2
3
4
5
var lyricModel =
LyricsModelBuilder.create().bindLyricToMain(normalLyric).getModel(); //歌词文件normalLyric就是个字符串,照着抄就行
var lyricUI = UINetease(); //拿到UI文件,这个后面也可以自己改,字体颜色等等
var lyricPadding = 40.0; //直接复制了,可以改
var playing = true; //这个是控制歌词播放的,作者用来联动按钮控制歌词高亮,我不需要联动直接传了true,后面需要加到自己的暂停按钮里面去(其实不加好像也行...就一直true呗)

次に、initState に 2 行を追加します。

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;
//我的定位信息是用StreamBuilder控制的,所以要套个头
return LyricsReader(
padding: EdgeInsets.symmetric(horizontal: lyricPadding),
model: lyricModel, //这里需要注意的是,因为这个StreamBuilder是一直在跑的,这里面可不敢放变量赋值,不然会一直run,如果跟我一样做了歌词的全局变量来读的话,检测值一定要放到StreamBuilder的外层!!!否则字幕会一直闪烁
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();
//这里是更新seek的位置,不过我那个进度条做好了监听,所以不需要setState,直接改就可以了
//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(
//这是自己写的方法,用于把Duration改成小时、分钟、秒这样的字符串
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 を渡すだけです。進行状況バーの値モニターに遭遇しないように注意する必要があります。そうしないと、そのままだと歌詞が点滅し続けます。ランニング。

現在のコントロールに問題があります。歌詞をドラッグして特定の文を見つけると、確かに [OK] をクリックして目的の位置を見つけることができますが、歌詞のスクロールが停止します。作者のデモでも同じことを見たので、後で解決する方法を確認するために、問題 を作者に提起しました。

1673766650968

となると、肝心なのは…歌詞を一括で見つける方法…NetEase CloudのAPIを使わないといけないかも…