Flutter で Nas 音楽プレーヤーを作成する (2)
2023-01-11 02:36 ≈ 2k字 ≈ 9分

叫んでください! ルーティングを通じて部分的なリフレッシュを行いたい場合は、クラッシュを感じる可能性があるため、Flutter の組み込みルーティングを使用しないでください。今日、右側の部分のローカル更新をグローバルルーティングに切り替えたところ、左側と下部のウィジェットを保持したまま動的に更新することができないことがわかり、腹が立って書き直しました。しかし、それは長い時間がかかりました。仕方がないので、モバイル版の調整時に左側を横スライド形式として独立させるために、ValueNotifierを使用してルートをハードライトし、現在のハイライトを削除することにしました。主な理由は、詳細ページで、上司に追跡することはできますが、それを望むかどうかは関係ないように感じます。 40ポイントのフォントで巨大なタイトルを書いてそこに置いたので、絶対に見逃すことはできません。

これで、歌手、アルバム、再生に関するすべてのロジックが完成しました。これは、再生されている内容などの便利なクエリ ポイントにすぎません。さらに重要な点が 2 つあります。それは、検索と歌詞です。直接トランスコーディングで従来の検索と簡素化された検索の両方を実現できるかどうかはわかりません。そうでない場合は、API を使用する必要があり、クエリ時間の無駄です。

1. アルバムの滝の流れ

これは私がずっと望んでいたものです。Jellyfin を使用して以来、すべての映画を無駄に見てきたような気がします。この種のテレビのカーテンウォールはさらにハンサムで、滝の流れ用の既製のホイールもあります [flutter_staggered_grid_view](https: //pub.dev /packages/flutter_staggered_grid_view) の実装は非常に簡単で、pubspec.yamlに追加してからflutter pub getを実行するだけです。

複数の表示方法をサポートしています。詳しくはホームページを参照してください。ただし、私は、ListView を構築するのと同じように、MasonryGridView を選択しました。ここで定義されています。

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 ...
}

写真と 2 行のテキストなど、ウィジェット の組み合わせを内部に封印すると、効果が現れます。

もちろん画像キャッシュもできるのですが、もともとやりたかったのですが、フォルダに保存するか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
//現在のリソース ID を監視します。これは音楽の再生だけでなく、歌手の写真などの取得にも使用されます。
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、曲リスト、リスト内の現在の曲のインデックスを渡して、マップを閉じます。

主な目的は、曲の画像 URL を取得することです。これは、元のマップにはありません。サーバー アドレスと ID 情報を含む URL を入力する必要があります。それ以外の場合は、直接使用する方が便利です。ソングインスタンス。

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 {
//1. 声明
final playlist = ConcatenatingAudioSource(
useLazyPreparation: true,
shuffleOrder: DefaultShuffleOrder(),
children: await _getPlayList(),
);
//3. プレイリストを追加し、現在再生中の曲を設定します
await _player.setAudioSource(playlist,
initialIndex: activeIndex.value, initialPosition: Duration.zero);
//4. プレイを開始する
_player.play();
}

_getPlayList() async {
List<AudioSource> children = [];
List _songs = activeList.value;
把触发的时候传的值拿出来
for (var element in _songs) {
Songs _song = element;
//2. データベースにアクセスして音楽ファイルのアドレスを入力します。
String _url = await getSongStreamUrl(_song.id);
children.add(AudioSource.uri(Uri.parse(_url), tag: _song.id)); //ここでの tag の設定に注意してください。これは、後の監視に使用されます。
}
return children;
}

2.4. 再生の変化を監視する

次の曲を再生するときは、曲名やカバーなどの情報を置き換える必要があります。このとき、操作のためにモニタリングを挿入する必要があります。

Just Audio には、聴くことができるさまざまなイベント ストリームがあります。

  • sequenceStream: これは追加されたシーケンスのプレイリストです。プレイリストが変更されるたびに、新しいリストがここに生成されます。
  • shuffleModeEnabledStream: シャッフル再生のモニタリング、このモードのオンとオフの切り替えはブール値です。
  • shuffleIndicesStream: これは、プレイリスト sequenceStream 内の項目を指す整数のシャッフルされたリストです (それ自体はシャッフルされません)。 shuffleオーディオ プレーヤーでこのメソッドが呼び出されると、ここで新しいリストが生成されます。
  • currentIndexStream: このストリームは、曲が最初に開始されたときに現在の曲のインデックスを通知します。この整数は、プレイリスト内のオーディオ ソース「sequenceStream」を指します。
  • loopModeStream: デフォルトでは、リスト内のすべての曲を一度に再生します。ただし、単一の曲やプレイリストを繰り返すこともできます。これがループ モードです。このストリームは、ループ モードが変更されるたびに新しい値を生成します。

さて、上記を見てください。監視するには、sequenceStateStream を使用します。ここでは、上記の 5 つのストリームのいずれかが新しい値を生成する限り、sequenceStateStream が生成します。結合値型SequenceState、1つは5の価値があるので、とても嬉しいです

まずこのメソッド呼び出しを 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;
//拿到生成播放列表的时候放到tag里面的id
final _title = currentItem?.tag as String?;
//去数据库查资料拿到歌曲信息
final _tem = await getSong(_title.toString());
//更新当前歌曲id
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;

// 这里监控乱序的,我没用,自己写了一个(主要是先写了乱序的逻辑,后做的播放列表,懒得改了)
// isShuffleModeEnabledNotifier.value = sequenceState.shuffleModeEnabled;

//拿到播放列表
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. 早送りと巻き戻し

実際、プログレスバーがあるので今では多くの人がこの 2 つのボタンをキャンセルしていますが、ボタンは以前に書かれたものなので、そのまま削除するのはもったいないです。

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. 上と下

ここでは、以前に設定した値 isFirstSongNotifierisLastSongNotifier を監視する必要があります。フィルタリングがない場合は、最初の曲が再生されたときに次の曲をクリックし、最後の曲が再生されたときにクリックします。次の曲をクリックしてください。問題が発生します。

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 に変化すると、後続の進行状況バーが移動するため、ここでの数字は固定幅を使用する必要があります。フォント。

アプリ全体で 2 つのフォントを使用しました。1 つは中国語、日本語、韓国語、英語のフォントをサポートする NotoSansSC で、当時設定した Jellyfin のバックアップ フォントからドラッグして使用しました。当時は中国語、日本語、韓国語、英語の字幕を表示するためにも使用されていましたが、ここでも同様に使用できます。

ただし、このフォントには2つのバージョンがあり、通常版では数字と英語が問題なく表示されるのですが、Mono版では英語と数字が半角で表示されてしまいます。そのため、ヘッドが非常に大きく見えるため、特にプログレスバーの前の数字に使用する Google Fonts の小さな固定幅フォントを見つけました。

  1. 資産の宣言

または、pubspec.yaml で宣言します。

1
2
3
4
5
6
7
8
9
10
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
fonts:
- family: NotoSansSC
fonts:
- asset: assets/fonts/NotoSansSC-Regular.otf
- family: ChivoMono
fonts:
- asset: assets/fonts/ChivoMono-Regular.ttf
  1. 局所使用
1
2
3
4
5
Text(
formatSongDuration(widget.position),
style: TextStyle(
color: borderColor, fontFamily: 'ChivoMono', fontSize: 12),
)
  1. 世界的な使用
1
2
3
4
5
6
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'XiuMusic',
theme: ThemeData(fontFamily: 'NotoSansSC'),
home: MainScreen(),
);

4. やることリスト

ストレスを感じたときは、コードを書くことで自分自身の緊張を解きほぐすので、急いで終わらせることはありません。ゆっくり時間をかけてください。

Todo リスト は Github で直接更新されます

実際、すでに入手可能になっていますので、興味のある方はぜひプレイしてみてください。XiuMusic