├── audioplayer ├── ios │ ├── Assets │ │ └── .gitkeep │ ├── Classes │ │ ├── AudioplayerPlugin.h │ │ └── AudioplayerPlugin.m │ ├── .gitignore │ └── audioplayer.podspec ├── android │ ├── settings.gradle │ ├── gradle.properties │ ├── .gitignore │ ├── src │ │ └── main │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── build.gradle ├── macos │ ├── Classes │ │ ├── SwiftAudioplayerPlugin.swift │ │ ├── AudioplayerPlugin.h │ │ └── AudioplayerPlugin.m │ ├── Flutter │ │ ├── GeneratedPluginRegistrant.swift │ │ └── ephemeral │ │ │ ├── Flutter-Generated.xcconfig │ │ │ └── flutter_export_environment.sh │ └── audioplayer.podspec ├── .gitignore ├── screenshot.png ├── pubspec.yaml ├── LICENSE ├── CHANGELOG.md └── README.md ├── android ├── speech │ ├── .gitignore │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── src │ │ └── main │ │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ │ ├── jniLibs │ │ │ └── arm64-v8a │ │ │ │ ├── libvad.dnn.so │ │ │ │ └── libBaiduSpeechSDK.so │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── xwh │ │ │ └── lib │ │ │ └── speech │ │ │ └── AsrPlugin.java │ ├── libs │ │ └── bdasr_V3_20190515_c9eed5d.jar │ ├── .classpath │ ├── .project │ ├── proguard-rules.pro │ └── build.gradle ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── keystore.jks │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_bg.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── xml │ │ │ │ │ └── network_security_config.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── xwh │ │ │ │ │ └── flutter │ │ │ │ │ └── music │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── .classpath │ ├── .project │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .project ├── settings.gradle └── build.gradle ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── Flutter.podspec │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile └── Podfile.lock ├── fonts └── iconfont.ttf ├── images ├── music_2.jpg └── placeholder_play_list.jpg ├── screenshot ├── 03_mv.jpg ├── player.jpg ├── 05_artists.jpg ├── 06_player.jpg ├── 07_player.jpg ├── 08_player.jpg ├── 09_setting.jpg ├── 10_search.jpg ├── fullscreen.jpg ├── 01_home_page.jpg ├── 02_play_list.jpg ├── FlutterMusic.png ├── qr_flutter_music.png └── 04_playlist_detail.jpg ├── test ├── async_test.dart └── widget_test.dart ├── lib ├── model │ ├── song.dart │ ├── speech_manager.dart │ ├── video_controller.dart │ ├── play_list.dart │ ├── color_provider.dart │ ├── song_util.dart │ └── Lyric.dart ├── utils │ ├── shared_preference_util.dart │ ├── screen_size.dart │ ├── toast_util.dart │ ├── my_icons.dart │ ├── network_util.dart │ ├── navigator_util.dart │ ├── colors.dart │ ├── file_util.dart │ └── http_util.dart ├── widget │ ├── loading_container.dart │ ├── blur_widget.dart │ ├── text_icon.dart │ ├── text_icon_withbg.dart │ ├── tap_anim_widget.dart │ ├── current_play_list.dart │ ├── song_item_tile.dart │ ├── favorite_icon_playlist.dart │ ├── play_list_item.dart │ ├── gradient_text.dart │ ├── song_item_grid.dart │ ├── song_item_text.dart │ ├── mv_item.dart │ ├── music_progress_bar_2.dart │ ├── music_progress_bar.dart │ ├── my_icon_button.dart │ ├── search_bar.dart │ ├── wave_widget.dart │ └── floating_player.dart ├── pages │ ├── song_list.dart │ ├── favorite_music.dart │ ├── rank_song_list.dart │ ├── mv_tab_page.dart │ ├── mv_player_page.dart │ ├── rank_page.dart │ ├── favorite_page.dart │ ├── play_list_page.dart │ ├── mv_page.dart │ ├── tabs_bottom.dart │ ├── artist_list_page.dart │ ├── play_list_tab_page.dart │ ├── history_page.dart │ ├── home_page.dart │ └── artist_detail.dart ├── dao │ ├── music_db.dart │ ├── music_db_playlist.dart │ ├── music_db_favorite.dart │ ├── music_db_history.dart │ └── api_cache.dart ├── test │ ├── test_positioned_1.dart │ ├── test_positioned_2.dart │ ├── test_scroll_position.dart │ └── video_demo.dart └── main.dart ├── .metadata ├── bugs.md ├── .gitignore └── pubspec.yaml /audioplayer/ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/speech/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /audioplayer/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'audioplayer' 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /audioplayer/macos/Classes/SwiftAudioplayerPlugin.swift: -------------------------------------------------------------------------------- 1 | ../../ios/Classes/SwiftAudioplayerPlugin.swift -------------------------------------------------------------------------------- /fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/fonts/iconfont.ttf -------------------------------------------------------------------------------- /images/music_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/images/music_2.jpg -------------------------------------------------------------------------------- /screenshot/03_mv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/03_mv.jpg -------------------------------------------------------------------------------- /screenshot/player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/player.jpg -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/keystore.jks -------------------------------------------------------------------------------- /audioplayer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .metadata -------------------------------------------------------------------------------- /screenshot/05_artists.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/05_artists.jpg -------------------------------------------------------------------------------- /screenshot/06_player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/06_player.jpg -------------------------------------------------------------------------------- /screenshot/07_player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/07_player.jpg -------------------------------------------------------------------------------- /screenshot/08_player.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/08_player.jpg -------------------------------------------------------------------------------- /screenshot/09_setting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/09_setting.jpg -------------------------------------------------------------------------------- /screenshot/10_search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/10_search.jpg -------------------------------------------------------------------------------- /screenshot/fullscreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/fullscreen.jpg -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /audioplayer/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/audioplayer/screenshot.png -------------------------------------------------------------------------------- /screenshot/01_home_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/01_home_page.jpg -------------------------------------------------------------------------------- /screenshot/02_play_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/02_play_list.jpg -------------------------------------------------------------------------------- /screenshot/FlutterMusic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/FlutterMusic.png -------------------------------------------------------------------------------- /android/speech/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/speech/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Speech 3 | 4 | -------------------------------------------------------------------------------- /images/placeholder_play_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/images/placeholder_play_list.jpg -------------------------------------------------------------------------------- /screenshot/qr_flutter_music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/qr_flutter_music.png -------------------------------------------------------------------------------- /screenshot/04_playlist_detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/screenshot/04_playlist_detail.jpg -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /audioplayer/ios/Classes/AudioplayerPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AudioplayerPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | 5 | 6 | android.enableJetifier=true 7 | android.useAndroidX=true -------------------------------------------------------------------------------- /audioplayer/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/speech/libs/bdasr_V3_20190515_c9eed5d.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/speech/libs/bdasr_V3_20190515_c9eed5d.jar -------------------------------------------------------------------------------- /audioplayer/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /audioplayer/macos/Classes/AudioplayerPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface AudioplayerPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_bg.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/speech/src/main/jniLibs/arm64-v8a/libvad.dnn.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/speech/src/main/jniLibs/arm64-v8a/libvad.dnn.so -------------------------------------------------------------------------------- /test/async_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | 4 | void main() { 5 | 6 | test('test hello!', (){ 7 | print('hello'); 8 | }); 9 | } -------------------------------------------------------------------------------- /audioplayer/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/speech/src/main/jniLibs/arm64-v8a/libBaiduSpeechSDK.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/android/speech/src/main/jniLibs/arm64-v8a/libBaiduSpeechSDK.so -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwh817/flutter_music_player/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /audioplayer/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 6 | -------------------------------------------------------------------------------- /lib/model/song.dart: -------------------------------------------------------------------------------- 1 | class Song{ 2 | Song({ 3 | this.id, 4 | this.name, 5 | this.artist 6 | }); 7 | 8 | BigInt id; 9 | String name; 10 | String artist; 11 | int album; 12 | int duration; 13 | String cover; 14 | String url; 15 | BigInt lastPlayTime; 16 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2d2a1ffec95cc70a3218872a2cd3f8de4933c42f 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /audioplayer/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import audioplayer 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | AudioplayerPlugin.register(with: registry.registrar(forPlugin: "AudioplayerPlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/speech/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /audioplayer/macos/Flutter/ephemeral/Flutter-Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/Users/rxlabz/dev/tools/flutter 3 | FLUTTER_APPLICATION_PATH=/Users/rxlabz/dev/projects/audioplayer2/audioplayer/audioplayer 4 | FLUTTER_BUILD_DIR=build 5 | FLUTTER_FRAMEWORK_DIR=/Users/rxlabz/dev/tools/flutter/bin/cache/artifacts/engine/darwin-x64 6 | FLUTTER_BUILD_NAME=0.6.0 7 | FLUTTER_BUILD_NUMBER=0.6.0 8 | -------------------------------------------------------------------------------- /lib/utils/shared_preference_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SharedPreferenceUtil { 4 | static SharedPreferences instance; 5 | 6 | /// SharedPreferences的初始化是异步的,很多地方要同步的方式使用很麻烦。 7 | /// 这里在启动时统一初始化一次,后面直接用。 8 | static init() async { 9 | instance = await SharedPreferences.getInstance(); 10 | } 11 | 12 | static SharedPreferences getInstance() { 13 | return instance; 14 | } 15 | 16 | 17 | } -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/screen_size.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class ScreenSize { 4 | static double width; 5 | static double height; 6 | 7 | static void getScreenSize(BuildContext context) { 8 | MediaQueryData mediaData = MediaQuery.of(context); 9 | final size = mediaData.size; 10 | width = size.width; 11 | height = size.height; 12 | 13 | double dp = mediaData.devicePixelRatio; 14 | print('屏幕宽高: ${size.width} * ${size.height}, 屏幕密度:$dp '); 15 | } 16 | 17 | 18 | } -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /audioplayer/macos/Flutter/ephemeral/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/rxlabz/dev/tools/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/rxlabz/dev/projects/audioplayer2/audioplayer/audioplayer" 5 | export "FLUTTER_BUILD_DIR=build" 6 | export "FLUTTER_FRAMEWORK_DIR=/Users/rxlabz/dev/tools/flutter/bin/cache/artifacts/engine/darwin-x64" 7 | export "FLUTTER_BUILD_NAME=0.6.0" 8 | export "FLUTTER_BUILD_NUMBER=0.6.0" 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':speech' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /audioplayer/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /android/app/src/main/kotlin/xwh/flutter/music/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package xwh.flutter.music 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | import xwh.lib.speech.AsrPlugin 8 | 9 | class MainActivity: FlutterActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | GeneratedPluginRegistrant.registerWith(this) 13 | 14 | this.registerMyPlugins() 15 | } 16 | 17 | 18 | // 注册自定义的插件 19 | private fun registerMyPlugins() { 20 | AsrPlugin.registerWith(registrarFor("xwh.lib.speech.AsrPlugin")) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /audioplayer/ios/Classes/AudioplayerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "AudioplayerPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "audioplayer-Swift.h" 9 | #endif 10 | 11 | @implementation AudioplayerPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftAudioplayerPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /audioplayer/macos/Classes/AudioplayerPlugin.m: -------------------------------------------------------------------------------- 1 | #import "AudioplayerPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "audioplayer-Swift.h" 9 | #endif 10 | 11 | @implementation AudioplayerPlugin 12 | + (void)registerWithRegistrar:(NSObject *)registrar { 13 | [SwiftAudioplayerPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /audioplayer/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: audioplayer 2 | description: A Flutter audio plugin (Swift/Java) to play remote or local audio files (ios, android, macos) 3 | version: 0.8.1 4 | author: Erick Ghaumez 5 | homepage: https://github.com/rxlabz/audioplayer 6 | 7 | environment: 8 | sdk: ">=2.0.0 <3.0.0" 9 | flutter: ">=1.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | flutter: 16 | plugin: 17 | platforms: 18 | android: 19 | package: bz.rxla.audioplayer 20 | pluginClass: AudioplayerPlugin 21 | ios: 22 | pluginClass: AudioplayerPlugin 23 | macos: 24 | pluginClass: AudioplayerPlugin 25 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/model/speech_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | class AsrManager { 4 | static const MethodChannel _channel = const MethodChannel('speech_plugin'); 5 | 6 | /// 初始化 7 | static Future init() async { 8 | return await _channel.invokeMethod('init'); 9 | } 10 | 11 | /// 开始录音 12 | static Future start({Map params}) async { 13 | return await _channel.invokeMethod('start', params ?? {}); 14 | } 15 | 16 | /// 停止录音 17 | static Future stop() async { 18 | return await _channel.invokeMethod('stop'); 19 | } 20 | 21 | /// 取消录音 22 | static Future cancel() async { 23 | return await _channel.invokeMethod('cancel'); 24 | } 25 | } -------------------------------------------------------------------------------- /android/speech/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | speech 4 | Project speech created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/utils/toast_util.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_music_player/model/color_provider.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class ToastUtil { 9 | static showToast(BuildContext context, String msg, {toastLength= Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER}) { 10 | Fluttertoast.showToast( 11 | msg: msg, 12 | toastLength: toastLength, 13 | gravity: gravity, 14 | backgroundColor: Provider.of(context, listen: false).getCurrentColor().withAlpha(200), 15 | textColor: Colors.white, 16 | fontSize: 14.0); 17 | } 18 | } -------------------------------------------------------------------------------- /audioplayer/android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'bz.rxla.audioplayer' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.0' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 30 26 | 27 | defaultConfig { 28 | minSdkVersion 16 29 | } 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation 'com.android.support:support-annotations:28.0.0' 37 | } 38 | -------------------------------------------------------------------------------- /lib/widget/loading_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///加载进度条组件 4 | class LoadingContainer extends StatelessWidget { 5 | final Widget child; 6 | final bool isLoading; 7 | final bool cover; 8 | 9 | const LoadingContainer( 10 | {Key key, 11 | @required this.isLoading, 12 | this.cover = false, 13 | @required this.child}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return !cover 19 | ? !isLoading ? child : _loadingView 20 | : Stack( 21 | children: [child, isLoading ? _loadingView : Container()], 22 | ); 23 | } 24 | 25 | Widget get _loadingView { 26 | return Center( 27 | child: CircularProgressIndicator(), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'Flutter' 7 | s.version = '1.0.0' 8 | s.summary = 'High-performance, high-fidelity mobile apps.' 9 | s.description = <<-DESC 10 | Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. 11 | DESC 12 | s.homepage = 'https://flutter.io' 13 | s.license = { :type => 'MIT' } 14 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 15 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 16 | s.ios.deployment_target = '8.0' 17 | s.vendored_frameworks = 'Flutter.framework' 18 | end 19 | -------------------------------------------------------------------------------- /android/speech/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /lib/widget/blur_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | /// 毛玻璃背景 6 | class BlurOvalWidget extends StatelessWidget { 7 | final Widget child; 8 | final double padding; 9 | final Color color; 10 | final double sigma; 11 | 12 | BlurOvalWidget( 13 | {this.child, 14 | this.padding: 0.0, 15 | this.sigma: 10.0, 16 | this.color: Colors.white10}); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ClipOval( 21 | child: BackdropFilter( 22 | filter: ImageFilter.blur( 23 | sigmaX: 8, 24 | sigmaY: 8, 25 | ), 26 | child: Container( 27 | color: color, 28 | padding: EdgeInsets.all(padding), 29 | child: child, 30 | ), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /audioplayer/macos/audioplayer.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint audioplayer.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'audioplayer' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'FlutterMacOS' 18 | 19 | s.platform = :osx, '10.11' 20 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 21 | s.swift_version = '5.0' 22 | end 23 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/model/video_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:video_player/video_player.dart'; 3 | 4 | class VideoControllerProvider with ChangeNotifier{ 5 | VideoPlayerController controller; 6 | 7 | setController(VideoPlayerController controller){ 8 | /* if (controller != null) { // 把上一个停掉 9 | controller.pause(); 10 | controller.dispose(); 11 | } */ 12 | this.controller = controller; 13 | //notifyListeners(); 14 | } 15 | 16 | VideoPlayerController getController(){ 17 | return this.controller; 18 | } 19 | 20 | // 判断参数是否是当前controller,如果是就把全局controller置空。 21 | bool clearController(VideoPlayerController itemController){ 22 | bool isCurrentController = itemController!= null && itemController == this.controller; 23 | if (isCurrentController) { 24 | this.controller = null; 25 | } 26 | return isCurrentController; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /bugs.md: -------------------------------------------------------------------------------- 1 | ## 问题Bugs: 2 | - snackBar改ListTile展示 3 | - MV视频页面添加类别展示,或者优化title 4 | - 获取歌曲下载进度 5 | - 6 | - 搜索功能、语音识别搜索 7 | - 收藏歌曲图片和歌词缓存到指定目录 8 | - 歌曲后台播放,避免退入后台 9 | - 缓存大小监测和清理 10 | 11 | - [2019-11-2] 实现歌曲收藏缓存、本地播放。 12 | - [2019-11-3] 优化api缓存 13 | 14 | 15 | - 语音搜索时暂停音乐 有歌词没有进度,查看是不是解析问题 16 | - 收藏页不刷新 17 | - 搜索框不可输入了 18 | - 播放和暂停时声音渐隐渐现 19 | - 判断一下个数,缓存太多才清理。 20 | - 首页几个页面keepAlive 21 | - 歌单hero动画切换背景图可用前一张模糊的。 22 | - 首页添加更多按钮 首页进来不要等待网络白屏,先显示部分或本地内容。 23 | - 通过和上一句比较长度,计算最后一句歌词长度。 24 | - 尝试在关闭播放页的时候把背景去掉,让动画好一点 25 | - 启动时进行缓存清理,隔一段时间判断,然后使用后台任务进行操作。。 26 | - 收藏的歌曲资源要单独存下来,不然会被回收掉。 27 | - 歌曲详情页等待状态 28 | - 接口失败时使用缓存 29 | - 浮动播放的图片不用placehold 30 | - 不在首页时首页动画要停掉 31 | - 慢网环境,歌词没及时切换,歌词进度还是之前的。 32 | - 语音搜索,先做java版 33 | - 判断是不是另外一首,不要用url,用id 34 | - 首页默认背景图要换浅色一点的,不然初次打开不好看。 35 | - 图片加载,默认图片不显示,会空白一会儿。 36 | - 等待中进度条看能否自定义。 37 | - 切歌时歌词要重置,不然下一首无歌词或加载失败就会显示前一首歌词。 38 | - 大图要设置默认图片,不然会全身绿色的背景。 39 | - 将播放器单独拿出来,主页面添加悬浮播放控件。 40 | - 查看源码,添加自定义Controller 暂停的时候点下一首,不会自动播放 创建表里面没有加新字段 -------------------------------------------------------------------------------- /audioplayer/ios/audioplayer.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint audioplayer.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'audioplayer' 7 | s.version = '0.0.1' 8 | s.summary = 'A new flutter plugin project.' 9 | s.description = <<-DESC 10 | A new flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '8.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /lib/utils/my_icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// iconfont图标,code为图标代码,在网页上查看 4 | class MyIcons{ 5 | 6 | static const IconData full_screen = const IconData( 7 | 0xe60b, 8 | fontFamily: 'myIcon', 9 | matchTextDirection: true 10 | ); 11 | static const IconData full_screen_exit = const IconData( 12 | 0xe7d0, 13 | fontFamily: 'myIcon', 14 | matchTextDirection: true 15 | ); 16 | static const IconData player_random = const IconData( 17 | 0xe61a, 18 | fontFamily: 'myIcon', 19 | matchTextDirection: true 20 | ); 21 | static const IconData player_cycle = const IconData( 22 | 0xe778, 23 | fontFamily: 'myIcon', 24 | matchTextDirection: true 25 | ); 26 | static const IconData player_single = const IconData( 27 | 0xe779, 28 | fontFamily: 'myIcon', 29 | matchTextDirection: true 30 | ); 31 | static const IconData player_list = const IconData( 32 | 0xe604, 33 | fontFamily: 'myIcon', 34 | matchTextDirection: true 35 | ); 36 | } -------------------------------------------------------------------------------- /android/speech/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 14 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | //google() 5 | //jcenter() 6 | maven { url 'https://maven.aliyun.com/repository/google' } 7 | maven { url 'https://maven.aliyun.com/repository/jcenter' } 8 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:4.1.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | //google() 20 | //jcenter() 21 | maven { url 'https://maven.aliyun.com/repository/google' } 22 | maven { url 'https://maven.aliyun.com/repository/jcenter' } 23 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } 24 | } 25 | } 26 | 27 | rootProject.buildDir = '../build' 28 | subprojects { 29 | project.buildDir = "${rootProject.buildDir}/${project.name}" 30 | } 31 | subprojects { 32 | project.evaluationDependsOn(':app') 33 | } 34 | 35 | task clean(type: Delete) { 36 | delete rootProject.buildDir 37 | } 38 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_music_player/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/utils/network_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity/connectivity.dart'; 2 | 3 | class NetworkUtil { 4 | 5 | /// 单例对象的写法 6 | // 私有静态instance 7 | static NetworkUtil _instance; 8 | 9 | // 对外访问点,指向私有静态方法 10 | factory NetworkUtil() { 11 | if (_instance == null) { 12 | _instance = NetworkUtil._(); 13 | } 14 | return _instance; 15 | } 16 | 17 | // 将默认构造函数私有化 18 | NetworkUtil._(); 19 | 20 | 21 | static ConnectivityResult networkState; 22 | 23 | var subscription; 24 | 25 | initNetworkListener(){ 26 | //监测网络变化 27 | subscription = Connectivity() 28 | .onConnectivityChanged 29 | .listen((ConnectivityResult result) { 30 | networkState = result; 31 | print('当前网络状态: $networkState'); 32 | /* if (result == ConnectivityResult.mobile) { // 手机网络 33 | 34 | } else if (result == ConnectivityResult.wifi) { // wifi 35 | 36 | } else { // 无网络 37 | 38 | } */ 39 | }); 40 | } 41 | 42 | ConnectivityResult getNetworkState(){ 43 | return networkState; 44 | } 45 | 46 | bool isNetworkAvailable(){ 47 | return networkState != ConnectivityResult.none; 48 | } 49 | 50 | void dispose() { 51 | //在页面销毁的时候一定要取消网络状态的监听 52 | subscription?.cancle(); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /lib/utils/navigator_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | /// 页面导航 3 | /// 可自定义切页效果 4 | class NavigatorUtil { 5 | static void push(BuildContext context, Widget page) { 6 | Navigator.of(context).push(MaterialPageRoute(builder: (context) => page)); 7 | } 8 | 9 | static void pushFade(BuildContext context, Widget page) { 10 | Navigator.of(context).push(CustomRouteFade(page)); 11 | } 12 | } 13 | 14 | // 渐变效果 15 | class CustomRouteFade extends PageRouteBuilder { 16 | final Widget widget; 17 | CustomRouteFade(this.widget) 18 | : super( 19 | transitionDuration: const Duration(milliseconds: 600), 20 | pageBuilder: (BuildContext context, Animation animation1, 21 | Animation animation2) { 22 | return widget; 23 | }, 24 | transitionsBuilder: (BuildContext context, 25 | Animation animation1, 26 | Animation animation2, 27 | Widget child) { 28 | return FadeTransition( 29 | opacity: Tween(begin: 0.0, end: 2.0).animate(CurvedAnimation( 30 | parent: animation1, curve: Curves.fastOutSlowIn)), 31 | child: child, 32 | ); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /lib/pages/song_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/widget/song_item_tile.dart'; 3 | import '../dao/music_163.dart'; 4 | 5 | class SongList extends StatefulWidget { 6 | SongList({Key key}) : super(key: key); 7 | 8 | _SongListState createState() => _SongListState(); 9 | } 10 | 11 | class _SongListState extends State { 12 | List _songs = []; 13 | 14 | _getSongs() async { 15 | await MusicDao.getTopSongs(0).then((result) { 16 | // 界面未加载,返回。 17 | if (!mounted) return; 18 | 19 | setState(() { 20 | _songs = result; 21 | }); 22 | }).catchError((e) { 23 | print('Failed: ${e.toString()}'); 24 | }); 25 | } 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _getSongs(); 31 | } 32 | 33 | Widget mWidget; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | if (_songs.length == 0) { 38 | // 显示进度条 39 | mWidget = Center(child: CircularProgressIndicator()); 40 | } else { 41 | mWidget = ListView.builder( 42 | itemCount: this._songs.length, 43 | itemExtent: 70.0, // 设定item的高度,这样可以减少高度计算。 44 | itemBuilder: (context, index) => SongItemTile(_songs, index), 45 | ); 46 | } 47 | return mWidget; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widget/text_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class TextIcon extends StatelessWidget { 6 | final IconData icon; 7 | final String title; 8 | final bool selected; 9 | final Function onPressed; 10 | const TextIcon( 11 | {Key key, this.icon, this.title, this.selected: false, this.onPressed}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | Color color = selected ? Provider.of(context).getCurrentColor() : Colors.black54; 17 | return InkWell( 18 | onTap: this.onPressed, 19 | child: Container( 20 | width: 66.0, 21 | padding: EdgeInsets.fromLTRB(4.0, 8.0, 4.0, 4.0), 22 | child: Column( 23 | mainAxisSize: MainAxisSize.min, 24 | children: [ 25 | Icon(icon, color: color, size: 22.0), 26 | Text( 27 | title, 28 | style: TextStyle( 29 | //fontSize: selected ? 14.0 : 12.0, 30 | fontSize: 14.0, 31 | height: 1.4, 32 | color: color), 33 | ) 34 | ], 35 | ))); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/widget/text_icon_withbg.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class TextIconWithBg extends StatelessWidget { 6 | final IconData icon; 7 | final String title; 8 | final Function onPressed; 9 | const TextIconWithBg({Key key, this.icon, this.title, this.onPressed}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GestureDetector( 15 | onTap: this.onPressed, 16 | child: Column( 17 | mainAxisSize: MainAxisSize.min, 18 | mainAxisAlignment: MainAxisAlignment.center, 19 | children: [ 20 | Container( 21 | margin: EdgeInsets.fromLTRB(12.0, 16.0, 12.0, 8.0), 22 | padding: EdgeInsets.all(10.0), 23 | decoration: BoxDecoration( 24 | shape: BoxShape.circle, 25 | color: Provider.of(context) 26 | .getCurrentColor()), 27 | child: Icon(icon, color: Colors.white, size: 24.0), 28 | ), 29 | Text( 30 | title, 31 | style: TextStyle(fontSize: 13.0, color: Colors.black87), 32 | ), 33 | SizedBox(height: 16.0), 34 | ], 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /android/speech/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | 4 | // 添加对Flutter的依赖 5 | def localProperties = new Properties() 6 | def localPropertiesFile = rootProject.file('local.properties') 7 | if (localPropertiesFile.exists()) { 8 | localPropertiesFile.withReader('UTF-8') { reader -> 9 | localProperties.load(reader) 10 | } 11 | } 12 | 13 | def flutterRoot = localProperties.getProperty('flutter.sdk') 14 | if (flutterRoot == null) { 15 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 16 | } 17 | 18 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 19 | 20 | 21 | android { 22 | compileSdkVersion 30 23 | 24 | defaultConfig { 25 | minSdkVersion 16 26 | targetSdkVersion 30 27 | versionCode 1 28 | versionName "1.0" 29 | 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | 40 | } 41 | 42 | // 配置flutter的源目录 43 | flutter { 44 | source '../..' 45 | } 46 | 47 | dependencies { 48 | implementation fileTree(dir: 'libs', include: ['*.jar']) 49 | 50 | implementation 'androidx.appcompat:appcompat:1.0.2' 51 | } 52 | -------------------------------------------------------------------------------- /lib/utils/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static Color unSelectedColor = Color(0xFF999999); 5 | 6 | static MaterialColor mainColor = Colors.green; 7 | static Color mainLightColor = Colors.greenAccent; 8 | static Color mainLightAccent = Colors.lightGreenAccent; 9 | static Color indicatorColor = Colors.orange; 10 | 11 | /* static Color mainColor = Colors.pink; 12 | static Color mainLightColor = Colors.pink; 13 | static Color mainLightAccent = Colors.pinkAccent; 14 | static Color toastBackground = Colors.pink[400]; 15 | static Color indicatorColor = Colors.pinkAccent; */ 16 | 17 | /* static Color mainColor = Colors.orange; 18 | static Color mainLightColor = Colors.orange; 19 | static Color mainLightAccent = Colors.deepOrangeAccent; 20 | static Color toastBackground = Colors.orange[400]; 21 | static Color indicatorColor = Colors.orange; */ 22 | 23 | 24 | /* static MaterialColor mainColor = Colors.purple; 25 | static Color mainLightColor = Colors.purple; 26 | static Color mainLightAccent = Colors.purpleAccent; 27 | static Color toastBackground = Colors.purple[400]; 28 | static Color indicatorColor = Colors.deepPurple; */ 29 | 30 | /* static Color mainColor = Colors.blue; 31 | static Color mainLightColor = Colors.lightBlue; 32 | static Color mainLightAccent = Colors.lightBlueAccent; 33 | static Color toastBackground = Colors.blue[400]; 34 | static Color indicatorColor = Colors.lightBlue; */ 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/pages/favorite_music.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_db_favorite.dart'; 3 | import 'package:flutter_music_player/widget/song_item_tile.dart'; 4 | 5 | class FavoriteMusic extends StatefulWidget { 6 | FavoriteMusic({Key key}) : super(key: key); 7 | 8 | _FavoriteMusicState createState() => _FavoriteMusicState(); 9 | } 10 | 11 | class _FavoriteMusicState extends State { 12 | List _songs; 13 | 14 | _getSongs() async { 15 | FavoriteDB().getFavoriteList().then((result) { 16 | // 界面未加载,返回。 17 | if (!mounted) return; 18 | 19 | setState(() { 20 | _songs = result; 21 | }); 22 | }).catchError((e) { 23 | print('Failed: ${e.toString()}'); 24 | }); 25 | } 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _getSongs(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | if (_songs == null) { 36 | return Container(); 37 | } 38 | if (_songs.length == 0) { 39 | return Center( 40 | child: Text( 41 | '您还没有收藏歌曲\n可点击播放页右上角进行收藏。', 42 | textAlign: TextAlign.center, 43 | style: TextStyle(color: Colors.grey, height: 1.5), 44 | )); 45 | } else { 46 | return ListView.builder( 47 | itemCount: this._songs.length, 48 | itemExtent: 70.0, // 设定item的高度,这样可以减少高度计算。 49 | itemBuilder: (context, index) => SongItemTile(_songs, index), 50 | ); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lib/pages/rank_song_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_163.dart'; 3 | import 'package:flutter_music_player/widget/song_item_tile.dart'; 4 | 5 | class RankSongList extends StatefulWidget { 6 | final int topId; 7 | final String topName; 8 | RankSongList(this.topId, this.topName, {Key key}) : super(key: key); 9 | 10 | @override 11 | _RankSongListState createState() => _RankSongListState(); 12 | } 13 | 14 | class _RankSongListState extends State { 15 | List _songs = []; 16 | 17 | _getSongs() async { 18 | await MusicDao.getTopSongs(widget.topId).then((result) { 19 | // 界面未加载,返回。 20 | if (!mounted) return; 21 | 22 | setState(() { 23 | _songs = result; 24 | }); 25 | }).catchError((e) { 26 | print('Failed: ${e.toString()}'); 27 | }); 28 | } 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _getSongs(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | appBar: AppBar( 40 | titleSpacing: 0.0, 41 | title: Text(widget.topName, style: TextStyle(fontSize: 16.0)), 42 | ), 43 | body: _songs.length == 0 44 | ? Center(child: CircularProgressIndicator()) 45 | : ListView.builder( 46 | itemCount: _songs.length, 47 | itemExtent: 70.0, // 设定item的高度,这样可以减少高度计算。 48 | itemBuilder: (context, index) => SongItemTile(_songs, index), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/pages/mv_tab_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_163.dart'; 3 | import 'package:flutter_music_player/widget/mv_item.dart'; 4 | 5 | class MVTabPage extends StatefulWidget { 6 | final String url; 7 | MVTabPage({Key key, this.url}) : super(key: key); 8 | 9 | @override 10 | _MVTabPageState createState() => _MVTabPageState(); 11 | } 12 | 13 | class _MVTabPageState extends State { 14 | List _mvList = []; 15 | 16 | _getMVList() async { 17 | await MusicDao.getMVList(widget.url).then((result) { 18 | // 界面未加载,返回。 19 | if (!mounted) return; 20 | 21 | setState(() { 22 | _mvList = result; 23 | }); 24 | }).catchError((e) { 25 | print('Failed: ${e.toString()}'); 26 | }); 27 | } 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | _getMVList(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return _mvList.length == 0 38 | ? Center(child: CircularProgressIndicator()) 39 | : ListView.builder( 40 | cacheExtent: 10.0, // 缓存区域,滚出多远后回收item,调用其dispose 41 | itemCount: this._mvList.length, 42 | //itemExtent: 70.0, // 设定item的高度,这样可以减少高度计算。 43 | itemBuilder: (context, index) => MVItem(this._mvList[index]), 44 | /* separatorBuilder: (context, index) => Divider( 45 | color: Color(0x0f000000), 46 | height: 12.0, // 间隔的高度 47 | thickness: 8.0, // 绘制的线的厚度 48 | ), */ 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/widget/tap_anim_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 点击会有动画的控件 4 | class TapAnim extends StatefulWidget { 5 | final Function onPressed; 6 | final Widget child; 7 | final int animDuration; 8 | 9 | TapAnim({this.child, this.onPressed, this.animDuration:160}); 10 | 11 | @override 12 | _TapAnimState createState() => _TapAnimState(); 13 | } 14 | 15 | class _TapAnimState extends State with SingleTickerProviderStateMixin { 16 | Animation animation; 17 | AnimationController _controller; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _controller = 23 | AnimationController(vsync: this, duration: Duration(milliseconds: widget.animDuration)) 24 | ..addListener(() { 25 | setState(() {}); 26 | }); 27 | 28 | animation = new Tween(begin: 0.0, end: 1.0).animate(_controller); 29 | animation.addStatusListener((status) { 30 | if (status == AnimationStatus.completed) { 31 | //动画执行结束时反向执行动画 32 | _controller.reverse(); 33 | } 34 | }); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | _controller.dispose(); 40 | super.dispose(); 41 | } 42 | 43 | void startAnim() { 44 | _controller.forward(); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return GestureDetector( 50 | onTap: () { 51 | startAnim(); 52 | widget.onPressed(); 53 | }, 54 | child: Transform.scale( 55 | scale: 1.0 - _controller.value * 0.2, 56 | child: widget.child, 57 | )); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /audioplayer/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2017 rxlabz. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Your Company nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | FlutterMusic 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/widget/current_play_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/music_controller.dart'; 3 | import 'package:flutter_music_player/widget/song_item_text.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class CurrentPlayList extends StatefulWidget { 7 | CurrentPlayList({Key key}) : super(key: key); 8 | 9 | @override 10 | _CurrentPlayListState createState() => _CurrentPlayListState(); 11 | } 12 | 13 | class _CurrentPlayListState extends State { 14 | final itemHeight = 40.0; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | MusicController musicController = 19 | Provider.of(context, listen: false); 20 | List songs = musicController.playList.songList; 21 | double initScroll = itemHeight * (musicController.getCurrentIndex() - 3); 22 | return ClipRRect( 23 | borderRadius: BorderRadius.only( 24 | topLeft: Radius.circular(16.0), 25 | topRight: Radius.circular(16.0)), 26 | child: Container( 27 | color: Colors.white.withOpacity(0.9), 28 | padding: EdgeInsets.symmetric(vertical: 20), 29 | child: ListView.builder( 30 | controller: ScrollController(initialScrollOffset: initScroll), 31 | itemCount: songs.length, 32 | itemExtent: itemHeight, // 设定item的高度,这样可以减少高度计算。 33 | itemBuilder: (context, index) => SongItemText( 34 | songs, 35 | index, 36 | onItemTap: () { 37 | Future.delayed(Duration(milliseconds: 300)).then((_) { 38 | Navigator.of(context).pop(); 39 | }); 40 | }, 41 | ), 42 | ), 43 | )); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | pubspec.lock 32 | .flutter-plugins 33 | .flutter-plugins-dependencies 34 | 35 | 36 | # Android related 37 | **/android/**/gradle-wrapper.jar 38 | **/android/.gradle 39 | **/android/captures/ 40 | **/android/gradlew 41 | **/android/gradlew.bat 42 | **/android/local.properties 43 | **/android/**/GeneratedPluginRegistrant.java 44 | 45 | # iOS/XCode related 46 | **/ios/**/*.mode1v3 47 | **/ios/**/*.mode2v3 48 | **/ios/**/*.moved-aside 49 | **/ios/**/*.pbxuser 50 | **/ios/**/*.perspectivev3 51 | **/ios/**/*sync/ 52 | **/ios/**/.sconsign.dblite 53 | **/ios/**/.tags* 54 | **/ios/**/.vagrant/ 55 | **/ios/**/DerivedData/ 56 | **/ios/**/Icon? 57 | **/ios/**/Pods/ 58 | **/ios/**/.symlinks/ 59 | **/ios/**/profile 60 | **/ios/**/xcuserdata 61 | **/ios/.generated/ 62 | **/ios/Flutter/App.framework 63 | **/ios/Flutter/Flutter.framework 64 | **/ios/Flutter/Generated.xcconfig 65 | **/ios/Flutter/app.flx 66 | **/ios/Flutter/app.zip 67 | **/ios/Flutter/flutter_assets/ 68 | **/ios/Flutter/flutter_export_environment.sh 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | -------------------------------------------------------------------------------- /lib/dao/music_db.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_music_player/dao/music_db_history.dart'; 2 | import 'package:flutter_music_player/dao/music_db_playlist.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | import 'music_db_favorite.dart'; 6 | 7 | class MusicDB { 8 | static const db_version = 4; 9 | static const db_file = 'music.db'; 10 | 11 | /// 单例对象的写法 12 | // 私有静态instance 13 | static MusicDB _instance; 14 | 15 | // 对外访问点,指向私有静态方法 16 | factory MusicDB() => _getInstance(); 17 | 18 | static MusicDB _getInstance() { 19 | if (_instance == null) { 20 | _instance = MusicDB._(); 21 | } 22 | return _instance; 23 | } 24 | 25 | // 将默认构造函数私有化 26 | MusicDB._(); 27 | 28 | // db对象不能直接生成,第一次会报错,因为db对象是异步生成的,直接调用的时候为null。 29 | Database db; 30 | // 定义一个get方法,完美解决获取db可能为空的情况。 31 | Future getDB() async { 32 | if (db == null) { 33 | db = await initDB(); 34 | } 35 | return db; 36 | } 37 | 38 | Future initDB() async { 39 | print('initDB'); 40 | return await openDatabase(db_file, version: db_version, 41 | onCreate: (Database db, int version) async { 42 | // 初始化,创建表 43 | FavoriteDB().createTable(db); 44 | HistoryDB().createTable(db); 45 | PlayListDB().createTable(db); 46 | }, onUpgrade: (Database db, int oldVersion, int newVersion) async { 47 | // 数据库升级,修改表结构。 48 | if (oldVersion == 1) { 49 | oldVersion++; 50 | await db.execute( 51 | 'ALTER TABLE ${FavoriteDB.table_name} ADD COLUMN createTime integer'); 52 | } 53 | if (oldVersion == 2) { 54 | oldVersion++; 55 | HistoryDB().createTable(db); 56 | } 57 | if (oldVersion == 3) { 58 | oldVersion++; 59 | PlayListDB().createTable(db); 60 | } 61 | }); 62 | } 63 | 64 | closeDB() async { 65 | await db?.close(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 14 | 21 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /audioplayer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.8.1 4 | - iOS : enable audio output to speakers thanks @mhatout 5 | - fixes iOS 'timeobservers' typo thanks @athornz 6 | 7 | ## 0.8.0 8 | - Android : new FlutterPlugin API & AndroidX migration 9 | 10 | ## 0.7.0 11 | - add Web support 12 | 13 | ## 0.6.0 14 | - switch to Swift thanks @matteti #114 15 | - add MacOS support thanks @matteti #115 16 | 17 | ## 0.5.2 18 | - fix objC warning 19 | - updated example 20 | 21 | ## 0.5.1 22 | - Allow Dart 2 SDK 23 | - Fix java lint warnings. 24 | 25 | ## 0.5.0 26 | - BREAKING Change: No more separate handlers for communicating the state of the player. Instead we rely on streams to publish state changes and position updates. 27 | - Code formatting and flow improvements. Preparation for testing. 28 | 29 | ## 0.4.0 30 | 31 | - Feat : merge PR from [mindon](https://github.com/mindon) with mute methods and various improvements 32 | - fixes Future errors with --preview-dart2 33 | - Example : add a slider to demonstrate the seek feature 34 | 35 | ## 0.3.0 36 | 37 | - merge PR from [johanhenselmans](https://github.com/johanhenselmans) to switch iOS to ObjectiveC 38 | - merge PR from [oaks](https://github.com/oakes) to add the seek feature 39 | 40 | ## 0.2.0 41 | 42 | - support for local files 43 | 44 | ## 0.1.0 45 | 46 | - update to the current Plugin API 47 | - move to https://github.com/rxlabz/audioplayer 48 | 49 | ## 0.0.2 50 | 51 | Separated handlers for position, duration, completion and errors 52 | 53 | - setDurationHandler(TimeChangeHandler handler) 54 | - setPositionHandler(TimeChangeHandler handler) 55 | - setCompletionHandler(VoidCallback callback) 56 | - setErrorHandler(ErrorHandler handler) 57 | 58 | - new typedef 59 | ```dart 60 | typedef void TimeChangeHandler(Duration duration); 61 | typedef void ErrorHandler(String message); 62 | ``` 63 | 64 | ## 0.0.1 65 | 66 | - first POC : 67 | - methods : play, pause, stop 68 | - a globalHandler for position, duration, completion and errors 69 | -------------------------------------------------------------------------------- /lib/test/test_positioned_1.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/test/test_positioned_2.dart'; 3 | 4 | class TestPage extends StatefulWidget { 5 | TestPage({Key key}) : super(key: key); 6 | 7 | _TestPageState createState() => _TestPageState(); 8 | } 9 | 10 | class _TestPageState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text('Flutter Music Player'), 16 | ), 17 | body: Stack( 18 | alignment: Alignment.center, 19 | children: [ 20 | /* Positioned( 21 | bottom: 30.0, 22 | child: Text("Test"), 23 | ), */ 24 | Positioned( 25 | bottom: 30.0, 26 | left: 20.0, 27 | right: 20.0, 28 | child: Row( 29 | children: [ 30 | Text("00:00", 31 | style: TextStyle(color: Colors.black, fontSize: 12)), 32 | Expanded( 33 | child: Slider.adaptive( 34 | value: 20.0, 35 | min: 0.0, 36 | max: 100.0, 37 | onChanged: (value) {}, 38 | ), 39 | ), 40 | Text( 41 | "12:34", 42 | style: TextStyle(color: Colors.black, fontSize: 12), 43 | ), 44 | ], 45 | ), 46 | ), 47 | Positioned( // 发现一个现象:如果这儿不是Positioned那么上面的Positioned布局就会失效,报overflow异常。 48 | bottom: 100.0, 49 | left: 20.0, 50 | right: 20.0, 51 | child: RaisedButton( 52 | child: Text("单独的页面"), 53 | onPressed: () { 54 | Navigator.of(context) 55 | .push(MaterialPageRoute(builder: (context) => TestPage2())); 56 | }, 57 | )) 58 | ], 59 | )); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/widget/song_item_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/model/song_util.dart'; 4 | import 'package:flutter_music_player/pages/player_page.dart'; 5 | 6 | class SongItemTile extends StatelessWidget { 7 | final List songList; 8 | final int index; 9 | final Function onItemTap; 10 | const SongItemTile(this.songList, this.index, {Key key, this.onItemTap}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | Map song = this.songList[index]; 16 | String image = SongUtil.getSongImage(song); 17 | return Container( 18 | decoration: BoxDecoration( 19 | color: Color(0x08ffffff), 20 | /* border: Border( 21 | bottom: BorderSide( 22 | color: Color(0x11000000), width: 0.5, style: BorderStyle.solid), 23 | ), */ 24 | ), 25 | child: ListTile( 26 | leading: ClipRRect( 27 | borderRadius: BorderRadius.circular(6.0), 28 | child: image.isEmpty 29 | ? Image.asset('images/music_2.jpg', fit: BoxFit.cover) 30 | : CachedNetworkImage( 31 | imageUrl: image, 32 | placeholder: (context, url) => 33 | Image.asset('images/music_2.jpg', fit: BoxFit.cover)), 34 | ), 35 | title: new Text( 36 | "${song['name']}", 37 | maxLines: 1, 38 | overflow: TextOverflow.ellipsis, 39 | style: TextStyle(fontSize: 14.0), 40 | ), 41 | subtitle: new Text( 42 | SongUtil.getArtistNames(song), 43 | maxLines: 1, 44 | overflow: TextOverflow.ellipsis, 45 | style: TextStyle(fontSize: 12.0), 46 | ), 47 | onTap: () { 48 | if (onItemTap != null) { 49 | this.onItemTap(); 50 | } 51 | PlayerPage.gotoPlayer(context, list: songList, index: index); 52 | }, 53 | )); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/pages/mv_player_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_163.dart'; 3 | import 'package:video_player/video_player.dart'; 4 | 5 | class MVPlayer extends StatefulWidget { 6 | final Map mv; 7 | MVPlayer(this.mv, {Key key}) : super(key: key); 8 | 9 | @override 10 | _MVPlayerState createState() => _MVPlayerState(); 11 | } 12 | 13 | class _MVPlayerState extends State { 14 | VideoPlayerController _controller; 15 | @override 16 | void initState() { 17 | super.initState(); 18 | 19 | MusicDao.getMVDetail(widget.mv['id']).then((url) { 20 | _controller = VideoPlayerController.network(url) 21 | ..initialize().then((_) { 22 | // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. 23 | setState(() {}); 24 | _controller.play(); 25 | }); 26 | }); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | bool initialized = _controller != null && _controller.value.isInitialized; 32 | bool playing = _controller != null && _controller.value.isPlaying; 33 | 34 | return MaterialApp( 35 | title: 'Video Demo', 36 | home: Scaffold( 37 | body: Center( 38 | child: initialized 39 | ? AspectRatio( 40 | aspectRatio: _controller.value.aspectRatio, 41 | child: Hero( 42 | tag: widget.mv, 43 | child: VideoPlayer(_controller), 44 | )) 45 | : Center(child: CircularProgressIndicator()), 46 | ), 47 | floatingActionButton: FloatingActionButton( 48 | onPressed: () { 49 | setState(() { 50 | playing ? _controller.pause() : _controller.play(); 51 | }); 52 | }, 53 | child: Icon( 54 | playing ? Icons.pause : Icons.play_arrow, 55 | ), 56 | ), 57 | ), 58 | ); 59 | } 60 | 61 | @override 62 | void dispose() { 63 | super.dispose(); 64 | _controller.dispose(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/widget/favorite_icon_playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_db_playlist.dart'; 3 | 4 | class FavoritePlayListIcon extends StatefulWidget { 5 | final Map play; 6 | const FavoritePlayListIcon(this.play, {Key key}) : super(key: key); 7 | 8 | @override 9 | _FavoritePlayListIconState createState() => _FavoritePlayListIconState(); 10 | } 11 | 12 | class _FavoritePlayListIconState extends State { 13 | bool isFavorited = false; 14 | Map play; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | //print('FavoritePlayListIcon initState'); 20 | } 21 | 22 | void _checkFavorite() { 23 | PlayListDB().getPlayListById(play['id']).then((fav) { 24 | //print('getFavoriteById : $fav'); 25 | setState(() { 26 | isFavorited = fav != null; 27 | }); 28 | }); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | if (widget.play != play) { 34 | play = widget.play; 35 | _checkFavorite(); 36 | } 37 | return IconButton( 38 | icon: Icon( 39 | Icons.favorite, 40 | color: isFavorited 41 | ? Colors.white 42 | : Colors.white30, 43 | ), 44 | onPressed: () { 45 | if (this.isFavorited) { 46 | _cancelFavorite(context); 47 | } else { 48 | _addFavorite(context); 49 | } 50 | }, 51 | ); 52 | } 53 | 54 | void _addFavorite(context) { 55 | PlayListDB().addPlayList(widget.play).then((re) { 56 | print('addFavorite re: $re , play: ${widget.play}'); 57 | setState(() { 58 | isFavorited = true; 59 | }); 60 | }).catchError((error) { 61 | print('addFavorite error: $error'); 62 | }); 63 | } 64 | 65 | void _cancelFavorite(context) { 66 | PlayListDB().deletePlayList(widget.play['id']).then((re) { 67 | setState(() { 68 | isFavorited = false; 69 | }); 70 | }).catchError((error) { 71 | print('deleteFavorite error: $error'); 72 | throw Exception('取消收藏失败'); 73 | }); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lib/model/play_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | enum CycleType { queue, one, random } 4 | 5 | class PlayList { 6 | List songList = []; 7 | int index = 0; 8 | CycleType cycleType = CycleType.queue; 9 | 10 | setPlayList(List list, int currentIndex) { 11 | songList = list; 12 | this.index = currentIndex; 13 | } 14 | 15 | setCurrentIndex(int index) { 16 | this.index = index; 17 | } 18 | 19 | Map getCurrentSong() { 20 | if (index < 0 || index >= songList.length) { 21 | return null; 22 | } 23 | return songList[index]; 24 | } 25 | 26 | Map next() { 27 | if (songList.length == 0) { 28 | return null; 29 | } 30 | index++; 31 | if (index >= songList.length) { 32 | index = 0; 33 | } 34 | return songList[index]; 35 | } 36 | 37 | Map previous() { 38 | if (songList.length == 0) { 39 | return null; 40 | } 41 | index--; 42 | if (index < 0) { 43 | index = songList.length - 1; 44 | } 45 | return songList[index]; 46 | } 47 | 48 | Map randomNext() { 49 | if (songList.length == 0) { 50 | return null; 51 | } 52 | int rdmIndex = 0; 53 | if (songList.length > 1) { 54 | rdmIndex = Random().nextInt(songList.length); 55 | if (rdmIndex == index) { 56 | // 如果和当前index相同,就+1。 57 | rdmIndex++; 58 | if (rdmIndex >= songList.length) { 59 | rdmIndex = 0; 60 | } 61 | } 62 | } 63 | index = rdmIndex; 64 | return songList[index]; 65 | } 66 | 67 | int getCurrentIndex() { 68 | return this.index; 69 | } 70 | 71 | void changCycleType() { 72 | if (cycleType == CycleType.queue) { 73 | cycleType = CycleType.one; 74 | } else if (cycleType == CycleType.one) { 75 | cycleType = CycleType.random; 76 | } else { 77 | cycleType = CycleType.queue; 78 | } 79 | } 80 | 81 | String getCycleName() { 82 | String cycleName; 83 | switch(cycleType) { 84 | case CycleType.queue: cycleName = '顺序播放';break; 85 | case CycleType.one: cycleName = '单曲循环';break; 86 | case CycleType.random: cycleName = '随机播放';break; 87 | } 88 | return cycleName; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/widget/play_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/pages/play_list_detail.dart'; 4 | import 'package:flutter_music_player/utils/navigator_util.dart'; 5 | 6 | 7 | class PlayListItem extends StatelessWidget { 8 | final Map play; 9 | final String heroTag; 10 | const PlayListItem(this.play, {Key key, this.heroTag}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | //print('PlayListItem build: ${play['name']}, $hashCode'); 15 | String tag = '${play['id']}_$heroTag'; 16 | return Stack( 17 | alignment: AlignmentDirectional.bottomStart, 18 | children: [ 19 | ClipRRect( 20 | borderRadius: BorderRadius.all(Radius.circular(4.0)), 21 | child: Hero( 22 | tag: tag, // 注意页面keepAlive之后全局唯一 23 | child: CachedNetworkImage(imageUrl: '${play['coverImgUrl']}?param=300y300'), 24 | ) 25 | ), 26 | ClipRRect( 27 | borderRadius: BorderRadius.only( 28 | bottomLeft: Radius.circular(4.0), 29 | bottomRight: Radius.circular(4.0)), 30 | child: Container( 31 | width: double.infinity, 32 | color: Color.fromARGB(80, 0, 0, 0), 33 | padding: EdgeInsets.all(6.0), 34 | child: Text( 35 | play['name'], 36 | overflow: TextOverflow.ellipsis, 37 | style: TextStyle(fontSize: 12.0, color: Colors.white), 38 | )), 39 | ), 40 | Positioned.fill( 41 | child: Material( 42 | color: Colors.transparent, 43 | child: InkWell( 44 | // 水波纹 45 | splashColor: Colors.white.withOpacity(0.3), 46 | highlightColor: Colors.white.withOpacity(0.1), 47 | onTap: () { 48 | NavigatorUtil.push(context, PlayListPage(playlist: play, heroTag: tag)); 49 | }), 50 | ), 51 | ), 52 | ], 53 | ); 54 | } 55 | } -------------------------------------------------------------------------------- /lib/pages/rank_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/pages/rank_song_list.dart'; 3 | import 'package:flutter_music_player/utils/navigator_util.dart'; 4 | 5 | class RankPage extends StatefulWidget { 6 | RankPage({Key key}) : super(key: key); 7 | 8 | @override 9 | _RankPageState createState() => _RankPageState(); 10 | } 11 | 12 | class _RankPageState extends State { 13 | @override 14 | void initState() { 15 | super.initState(); 16 | } 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | titleSpacing: 0.0, 23 | title: Text('排行榜', style: TextStyle(fontSize: 16.0)), 24 | ), 25 | body: ListView.separated( 26 | itemCount: ranks.length, 27 | itemBuilder: (context, index) => 28 | _buildItem(ranks.entries.elementAt(index)), 29 | separatorBuilder: (context, index) => 30 | Divider(height: 0.5, color: Colors.black12), 31 | ), 32 | ); 33 | } 34 | 35 | Widget _buildItem(MapEntry entry) { 36 | return ListTile( 37 | contentPadding: EdgeInsets.only(left: 20.0, top: 4.0, bottom: 4.0), 38 | leading: Icon(Icons.whatshot, color: Colors.deepOrangeAccent), 39 | title: Text(entry.value, style: TextStyle(fontSize: 14.0)), 40 | onTap: () { 41 | NavigatorUtil.push(context, RankSongList(entry.key, entry.value)); 42 | }, 43 | ); 44 | } 45 | } 46 | 47 | Map ranks = { 48 | 3779629: '云音乐新歌榜', 49 | 3778678: '云音乐热歌榜', 50 | 2: '网易原创歌曲榜', 51 | 3: '云音乐飙升榜', 52 | 4: '云音乐电音榜', 53 | 5: 'UK排行榜周榜', 54 | 6: '美国Billboard周榜', 55 | 7: 'KTV嗨榜', 56 | 8: 'iTunes榜', 57 | 9: 'Hit FM Top榜', 58 | 10: '日本Oricon周榜', 59 | 11: '韩国Melon排行榜周榜', 60 | 12: '韩国Mnet排行榜周榜', 61 | 13: '韩国Melon原声周榜', 62 | 14: '中国TOP排行榜(港台榜)', 63 | 15: '中国TOP排行榜(内地榜)', 64 | 16: '香港电台中文歌曲龙虎榜', 65 | 17: '华语金曲榜', 66 | 18: '中国嘻哈榜', 67 | 19: '法国 NRJ EuroHot 30周榜', 68 | 20: '台湾Hito排行榜', 69 | 21: 'Beatport全球电子舞曲榜', 70 | 22: '云音乐ACG音乐榜', 71 | 23: '云音乐说唱榜', 72 | 24: '云音乐古典音乐榜', 73 | 25: '云音乐电音榜', 74 | 26: '抖音排行榜', 75 | 27: '新声榜', 76 | 28: '云音乐韩语榜', 77 | 29: '英国Q杂志中文版周榜', 78 | 30: '电竞音乐榜', 79 | 31: '云音乐欧美热歌榜', 80 | 32: '云音乐欧美新歌榜', 81 | 33: '说唱TOP榜', 82 | }; 83 | -------------------------------------------------------------------------------- /lib/widget/gradient_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class GradientText extends StatefulWidget { 6 | final Text text; 7 | final double offsetX; 8 | final Color colorBg; 9 | _GradientTextState _state; 10 | GradientText({this.text, this.offsetX = 0.0, this.colorBg = Colors.white}); 11 | 12 | @override 13 | _GradientTextState createState() { 14 | _state = _GradientTextState(); 15 | return _state; 16 | } 17 | 18 | int retryCount = 0; 19 | void setOffsetX(double offsetX) { 20 | if (_state == null) { 21 | print('_LyricPageState is null, retryCount: $retryCount'); 22 | Future.delayed(Duration(milliseconds: 200)).then((_) { 23 | retryCount++; 24 | if (retryCount < 5) { 25 | setOffsetX(offsetX); 26 | } 27 | }); 28 | } else { 29 | retryCount = 0; 30 | _state.setOffsetX(offsetX); 31 | } 32 | } 33 | } 34 | 35 | class _GradientTextState extends State { 36 | double offsetX = 0.0; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | offsetX = widget.offsetX; 42 | } 43 | 44 | setOffsetX(offsetX) { 45 | if (!mounted) return; 46 | //print('setOffset: $offsetX'); 47 | setState(() { 48 | this.offsetX = offsetX; 49 | }); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | if (widget.text.data.isEmpty) { 55 | return widget.text; 56 | } 57 | 58 | ColorStyleProvider colorProvider = Provider.of(context); 59 | final Gradient gradient = LinearGradient( 60 | colors: [colorProvider.getLightColor(), widget.colorBg], 61 | stops: [0.5, 0.65]); // 设置渐变的起始位置 62 | 63 | /// 参考:https://juejin.im/post/5c860c0a6fb9a049e702ef39 64 | return ShaderMask( 65 | // 遮罩层src,通过不同的BlendMode(混合模式)叠在dst上,产生不同的效果。 66 | shaderCallback: (bounds) { 67 | //print('bounds: ${bounds.width}'); 68 | return gradient.createShader( 69 | Offset(-bounds.width / 2 + bounds.width * this.offsetX, 0.0) & 70 | bounds.size); 71 | }, 72 | blendMode: BlendMode.srcIn, 73 | child: widget.text, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/pages/favorite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:flutter_music_player/pages/play_list_tab_page.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'favorite_music.dart'; 6 | 7 | class FavoritePage extends StatefulWidget { 8 | FavoritePage({Key key}) : super(key: key); 9 | 10 | _FavoritePageState createState() => _FavoritePageState(); 11 | } 12 | 13 | const types = ['单曲', '歌单']; 14 | 15 | class _FavoritePageState extends State 16 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 17 | TabController tabController; //tab控制器 18 | 19 | @override 20 | bool get wantKeepAlive => false; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | //初始化controller并添加监听 26 | tabController = TabController(length: types.length, vsync: this); 27 | } 28 | 29 | Widget mWidget; 30 | @override 31 | Widget build(BuildContext context) { 32 | super.build(context); 33 | ColorStyleProvider colorStyleProvider = 34 | Provider.of(context); 35 | return Scaffold( 36 | appBar: AppBar( 37 | backgroundColor: Color(0x07000000), 38 | elevation: 0, 39 | title: TabBar( 40 | controller: tabController, //控制器 41 | indicatorColor: colorStyleProvider.getIndicatorColor(), 42 | labelColor: colorStyleProvider.getCurrentColor(), 43 | unselectedLabelColor: Colors.black45, 44 | labelStyle: TextStyle(fontWeight: FontWeight.w600), //选中的样式 45 | unselectedLabelStyle: TextStyle(fontSize: 14), //未选中的样式 46 | isScrollable: true, //是否可滑动 47 | //tab标签 48 | tabs: types 49 | .map((name) => Tab( 50 | text: name, 51 | )) 52 | .toList(), 53 | //点击事件 54 | onTap: (int i) { 55 | tabController.animateTo(i); 56 | }, 57 | ), 58 | ), 59 | body: TabBarView( 60 | controller: tabController, 61 | children: [ 62 | FavoriteMusic(), 63 | PlayListTabPage( 64 | type: PlayListTabPage.TYPE_DB, 65 | heroTag: 'from_fav', 66 | error: '您还没有收藏歌单\n可在歌单页右上角进行收藏。') 67 | ], 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/widget/song_item_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/model/song_util.dart'; 4 | import 'package:flutter_music_player/pages/player_page.dart'; 5 | 6 | class SongItemOfGrid extends StatelessWidget { 7 | final List songList; 8 | final int index; 9 | /* static final Image defaultCover = Image.asset('images/music_cover.jpg', 10 | fit: BoxFit.cover, 11 | color: Colors.black54, 12 | colorBlendMode: BlendMode.dstOut); */ 13 | 14 | SongItemOfGrid(this.songList, this.index); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | Map song = this.songList[index]; 19 | String image = SongUtil.getSongImage(song, size: 200); 20 | return Stack( 21 | alignment: AlignmentDirectional.bottomStart, 22 | children: [ 23 | ClipRRect( 24 | borderRadius: BorderRadius.all(Radius.circular(4.0)), 25 | child: CachedNetworkImage( 26 | imageUrl: image, 27 | fit: BoxFit.fill, 28 | //placeholder: (context, url) => Container(color: Colors.black12) 29 | ), 30 | ), 31 | ClipRRect( 32 | borderRadius: BorderRadius.only( 33 | bottomLeft: Radius.circular(4.0), 34 | bottomRight: Radius.circular(4.0)), 35 | child: Container( 36 | width: double.infinity, 37 | color: Color.fromARGB(80, 0, 0, 0), 38 | padding: EdgeInsets.all(6.0), 39 | child: Text( 40 | song['name'], 41 | overflow: TextOverflow.ellipsis, 42 | style: TextStyle(fontSize: 12.0, color: Colors.white), 43 | )), 44 | ), 45 | Positioned.fill( 46 | child: Material( 47 | color: Colors.transparent, 48 | child: InkWell( 49 | // ??? 50 | splashColor: Colors.white.withOpacity(0.3), 51 | highlightColor: Colors.white.withOpacity(0.1), 52 | onTap: () { 53 | PlayerPage.gotoPlayer(context, 54 | list: songList, index: index); 55 | }), 56 | ), 57 | ), 58 | ]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/dao/music_db_playlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import 'music_db.dart'; 3 | 4 | class PlayListDB { 5 | static const table_name = 't_play_list'; 6 | 7 | /// 单例对象的写法 8 | // 私有静态instance 9 | static PlayListDB _instance; 10 | 11 | // 对外访问点,指向私有静态方法 12 | factory PlayListDB() => _getInstance(); 13 | 14 | static PlayListDB _getInstance() { 15 | if (_instance == null) { 16 | _instance = PlayListDB._(); 17 | } 18 | return _instance; 19 | } 20 | 21 | // 将默认构造函数私有化 22 | PlayListDB._(); 23 | 24 | /// 在数据库onCreate的时候,创建表。 25 | /// 注意:onUpgrade中添加的字段要在这儿添加,不然第一次安装就没有那个字段了。 26 | createTable(Database db) { 27 | db.execute('''create table $table_name ( 28 | id integer primary key, 29 | name text not null, 30 | cover text, 31 | createTime integer) 32 | '''); 33 | } 34 | 35 | Future addPlayList(Map playList) async { 36 | var fav = { 37 | 'id': playList['id'], 38 | 'name': playList['name'], 39 | 'cover': playList['coverImgUrl'], 40 | // 查看sqflite文档,发现不支持DateTime字段,用int来存储。 41 | 'createTime': DateTime.now().millisecondsSinceEpoch, 42 | }; 43 | 44 | print('fav: $fav, playList:$playList'); 45 | return (await MusicDB().getDB()).insert(table_name, fav); 46 | } 47 | 48 | Future> getPlayListById(var id) async { 49 | Database db = await MusicDB().getDB(); 50 | List list = await db.query(table_name, where: 'id = ?', whereArgs: [id]); 51 | return list.length > 0 ? list[0] : null; 52 | } 53 | 54 | Future>> getPlayList() async { 55 | Database db = await MusicDB().getDB(); 56 | List list = await db.query(table_name, orderBy: 'createTime desc'); 57 | List playLists = list 58 | .map((fav) => 59 | {'id': fav['id'], 'name': fav['name'], 'coverImgUrl': fav['cover']}) 60 | .toList(); 61 | 62 | return playLists; 63 | } 64 | 65 | Future deletePlayList(var id) async { 66 | Database db = await MusicDB().getDB(); 67 | int re = await db.delete(table_name, where: 'id = ?', whereArgs: [id]); 68 | if (re <= 0) { 69 | throw Exception('删除失败'); 70 | } else { 71 | return re; 72 | } 73 | } 74 | 75 | Future clearPlayList() async { 76 | Database db = await MusicDB().getDB(); 77 | return await db.delete(table_name); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/utils/file_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'package:path_provider/path_provider.dart'; 4 | /// 本地文件工具类 5 | class FileUtil{ 6 | static final String songsDir = "songs"; 7 | static final String lyricDir = "lyrics"; 8 | static final String music = ".mp3"; 9 | 10 | /// 获取子目录路径 11 | static Future getSubDirPath(String subDirPath) async { 12 | // get the path to the document directory. 13 | String root = (await getApplicationDocumentsDirectory()).path; 14 | return '$root/$subDirPath'; 15 | } 16 | 17 | 18 | /// 相对app目录之下创建子目录 19 | static Future createLocalDir(String subDirPath) async { 20 | String dirPath = await getSubDirPath(subDirPath); 21 | Directory dir = new Directory(dirPath); 22 | if (await dir.exists()) { // 如果目录不存在,就创建 23 | return dir; 24 | } else { 25 | return dir.create(recursive: true); 26 | } 27 | 28 | } 29 | 30 | /// 判断文件是否存在 31 | static Future isFileExists(String filePath) async { 32 | return File(filePath).exists(); 33 | } 34 | 35 | /// 获取歌曲本地路径 36 | static Future getSongLocalPath(int songId) async { 37 | String dir = await getSubDirPath(songsDir); 38 | String fileName = '$songId.mp3'; 39 | String filePath = '$dir/$fileName'; 40 | return filePath; 41 | } 42 | 43 | /// 获取收藏歌曲歌词 44 | static Future getLyricLocalPath(int songId) async { 45 | String dir = await getSubDirPath(lyricDir); 46 | String fileName = '$songId.lyric'; 47 | String filePath = '$dir/$fileName'; 48 | return filePath; 49 | } 50 | 51 | 52 | /// 删除文件 53 | static Future deleteLocalSong(Map song) async { 54 | int songId = song['id']; 55 | deleteFile(await getSongLocalPath(songId)); 56 | deleteFile(await getLyricLocalPath(songId)); 57 | return true; 58 | } 59 | 60 | /// 删除文件 61 | static Future deleteFile(String path) async { 62 | File file = File(path); 63 | if (await file.exists()) { 64 | await file.delete(recursive: true); 65 | } 66 | return true; 67 | } 68 | 69 | 70 | /// 判断文件是否超时 71 | static bool isFileTimeout(File file, Duration duration) { 72 | DateTime lastModified = file.lastModifiedSync(); 73 | return isTimeOut(lastModified, duration); 74 | } 75 | 76 | /// 判断上一次事件是否超过 77 | static bool isTimeOut(DateTime lastTime, Duration duration) { 78 | DateTime now = DateTime.now(); 79 | return now.isAfter(lastTime.add(duration)); 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /lib/test/test_positioned_2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TestPage2 extends StatelessWidget { 4 | const TestPage2({Key key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | appBar: AppBar( 10 | title: Text('Flutter Music Player'), 11 | ), 12 | body: SafeArea( 13 | child: Stack( 14 | alignment: Alignment.topCenter, 15 | children: [ 16 | /* Positioned( 17 | bottom: 30.0, 18 | child: Text("Test"), 19 | ), */ 20 | Align( 21 | alignment: Alignment.bottomCenter, 22 | child: Row( 23 | children: [ 24 | Text("00:00", 25 | style: TextStyle(color: Colors.black, fontSize: 12)), 26 | Expanded( 27 | child: Slider.adaptive( 28 | value: 20.0, 29 | min: 0.0, 30 | max: 100.0, 31 | onChanged: (value) {}, 32 | ), 33 | ), 34 | Text( 35 | "12:34", 36 | style: TextStyle(color: Colors.black, fontSize: 12), 37 | ), 38 | ], 39 | ), 40 | ), 41 | GestureDetector( 42 | child: Text("返回"), 43 | onTap: () { 44 | Navigator.pop(context); 45 | }, 46 | ), 47 | Align( 48 | alignment: Alignment.bottomCenter, 49 | child: Padding( 50 | padding: EdgeInsets.fromLTRB(20, 20, 20, 30), 51 | child: Row( 52 | children: [ 53 | Text("00:00", 54 | style: TextStyle(color: Colors.black, fontSize: 12)), 55 | Expanded( 56 | child: Slider.adaptive( 57 | value: 20.0, 58 | min: 0.0, 59 | max: 100.0, 60 | onChanged: (value) {}, 61 | ), 62 | ), 63 | Text( 64 | "12:34", 65 | style: TextStyle(color: Colors.black, fontSize: 12), 66 | ), 67 | ], 68 | )), 69 | ), 70 | ], 71 | ))); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/music_controller.dart'; 3 | import 'package:flutter_music_player/utils/shared_preference_util.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'dao/api_cache.dart'; 6 | import 'model/color_provider.dart'; 7 | import 'model/video_controller.dart'; 8 | import 'pages/home_page.dart'; 9 | 10 | void main() { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | _initBeforeRunApp().then((re) { 13 | runApp(_buildProvider()); 14 | _doSomethingInBackground(); 15 | }); 16 | } 17 | 18 | /// 在启动之前要做的异步任务 19 | /// 获取主题颜色 20 | Future _initBeforeRunApp() async { 21 | // 要去SharedPrefrence里面去颜色数据,但是为异步任务,修改main方法,首先完成异步任务再启动app; 22 | await SharedPreferenceUtil.init(); 23 | ColorStyleProvider.initColorStyle(); 24 | return true; 25 | } 26 | 27 | /// 启动时要进行的后台任务 28 | /// 这里简单用async,如果任务比较耗时,考虑Isolate方案。 29 | _doSomethingInBackground() async { 30 | // 清理缓存 31 | await APICache.clearCache(); 32 | } 33 | 34 | 35 | /// 遇到一个坑,一直报错:flutter Could not find the correct Provider 36 | /// 原来是Provider要加在App上面,而不是HomePage上面。 37 | _buildProvider() { 38 | return MultiProvider( 39 | providers: [ 40 | ChangeNotifierProvider.value(value: ColorStyleProvider()), 41 | ChangeNotifierProvider.value(value: MusicController()), 42 | ChangeNotifierProvider.value( 43 | value: VideoControllerProvider()), 44 | ], 45 | child: MyApp(), 46 | ); 47 | } 48 | 49 | class MyApp extends StatefulWidget { 50 | MyApp({Key key}) : super(key: key); 51 | 52 | @override 53 | _MyAppState createState() => _MyAppState(); 54 | } 55 | 56 | class _MyAppState extends State { 57 | @override 58 | Widget build(BuildContext context) { 59 | ColorStyleProvider colorStyleProvider = Provider.of(context); 60 | return MaterialApp( 61 | title: 'Flutter Music', 62 | debugShowCheckedModeBanner: false, 63 | showPerformanceOverlay: colorStyleProvider.showPerformanceOverlay, // 是否打开性能测试层 64 | theme: ThemeData( 65 | brightness: Brightness.light, 66 | appBarTheme: AppBarTheme( 67 | brightness: Brightness.dark, 68 | iconTheme: IconThemeData(color: Colors.white), 69 | textTheme: TextTheme(title: TextStyle(color: Colors.white))), 70 | primarySwatch: colorStyleProvider.getCurrentColor(color: 'mainColor')), 71 | home: HomePage()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/pages/play_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:flutter_music_player/pages/play_list_tab_page.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class PlayListPage extends StatefulWidget { 7 | PlayListPage({Key key}) : super(key: key); 8 | 9 | _PlayListPageState createState() => _PlayListPageState(); 10 | } 11 | 12 | const List types = [ 13 | "全部", 14 | "流行", 15 | "华语", 16 | "民谣", 17 | "摇滚", 18 | "清新", 19 | "浪漫", 20 | "古风", 21 | "影视原声", 22 | "欧美", 23 | "儿童", 24 | "电子", 25 | "校园", 26 | "放松" 27 | ]; 28 | 29 | class _PlayListPageState extends State 30 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 31 | TabController tabController; //tab控制器 32 | 33 | @override 34 | bool get wantKeepAlive => true; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | //初始化controller并添加监听 40 | tabController = TabController(length: types.length, vsync: this); 41 | tabController.addListener(() => _onTabChanged()); 42 | } 43 | 44 | void _onTabChanged() { 45 | if (tabController.index.toDouble() == tabController.animation.value) {} 46 | } 47 | 48 | Widget mWidget; 49 | @override 50 | Widget build(BuildContext context) { 51 | super.build(context); 52 | ColorStyleProvider colorStyleProvider = Provider.of(context); 53 | return Scaffold( 54 | appBar: AppBar( 55 | backgroundColor: Color(0x07000000), 56 | elevation: 0, 57 | title: TabBar( 58 | controller: tabController, //控制器 59 | indicatorColor: colorStyleProvider.getIndicatorColor(), 60 | labelColor: colorStyleProvider.getCurrentColor(), 61 | unselectedLabelColor: Colors.black45, 62 | labelStyle: TextStyle(fontWeight: FontWeight.w600), //选中的样式 63 | unselectedLabelStyle: TextStyle(fontSize: 14), //未选中的样式 64 | isScrollable: true, //是否可滑动 65 | //tab标签 66 | tabs: types.map((item) { 67 | return Tab( 68 | text: item, 69 | ); 70 | }).toList(), 71 | //点击事件 72 | onTap: (int i) { 73 | tabController.animateTo(i); 74 | }, 75 | ), 76 | ), 77 | body: TabBarView( 78 | controller: tabController, 79 | children: types.map((item) { 80 | return PlayListTabPage(type: item); 81 | }).toList(), 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/utils/http_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_music_player/dao/api_cache.dart'; 6 | 7 | class HttpUtil { 8 | /// 获取api接口json数据 9 | /// useCache:是否使用缓存,默认使用。但对于经常变化和容易过期的资源,例如视频播放地址,不要使用。 10 | /// checkCacheTimeout:是否检查缓存过期,默认检查,如果过期重新获取。对于不变的资源例如歌词,可以设置false,减少不必要的请求。 11 | static Future getJsonData(String url, 12 | {bool useCache: true, checkCacheTimeout: true}) async { 13 | var data; 14 | if (useCache) { 15 | // 有些资源是动态的,如播放地址,会过期不能使用缓存。 16 | String cache = 17 | await APICache.getCache(url, checkCacheTimeout: checkCacheTimeout); 18 | if (cache != null) { 19 | try { 20 | data = jsonDecode(cache); 21 | } catch (e) { 22 | APICache.deleteCache(url); 23 | print(e); 24 | } 25 | } 26 | } 27 | // 缓存没取到,就请求网络。 28 | if (data == null) { 29 | /// options:请求参数 30 | /// 这儿文本要缓存处理,所以ResponseType不用默认的json格式 31 | /// 或者使用 jsonEncode将Map对象转为json字符串,不要用toString,会丢掉符号信息。 32 | BaseOptions options = new BaseOptions(); 33 | options.responseType = ResponseType.plain; 34 | options.connectTimeout = 10000; // 连接超时 35 | 36 | try { 37 | Response response = await Dio(options).get(url); 38 | if (response.statusCode == HttpStatus.ok) { 39 | var text = response.data; 40 | data = jsonDecode(text); 41 | 42 | if (useCache) { 43 | // 缓存到本地 44 | bool re = await APICache.saveCache(url, text); 45 | print('saveCache $url result: $re'); 46 | } 47 | } else { 48 | throw Exception('Request failed, errorCode: ${response.statusCode}'); 49 | } 50 | } catch (e) { 51 | /// 如果网络超时,会报 DioErrorType.CONNECT_TIMEOUT 52 | print(e); 53 | if (e == DioErrorType.connectTimeout || 54 | e == DioErrorType.sendTimeout || 55 | e == DioErrorType.receiveTimeout) { 56 | print('网络请求超时'); 57 | } 58 | } 59 | } 60 | 61 | return data; 62 | } 63 | 64 | static void download(String url, String savePath) { 65 | print('download $url to $savePath'); 66 | File localFile = File(savePath); 67 | localFile.exists().then((exists) { 68 | if (!exists) { 69 | localFile.createSync(recursive: true); 70 | } 71 | return Dio().download(url, savePath); 72 | }).then((response) { 73 | if (response.statusCode == 200) { 74 | print('下载成功'); 75 | } else { 76 | print('下载失败:${response.statusCode}'); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/pages/mv_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_163.dart'; 3 | import 'package:flutter_music_player/model/color_provider.dart'; 4 | import 'package:flutter_music_player/pages/mv_tab_page.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class MVPage extends StatefulWidget { 8 | MVPage({Key key}) : super(key: key); 9 | 10 | _MVPageState createState() => _MVPageState(); 11 | } 12 | 13 | Map types = { 14 | "最新": MusicDao.URL_MV_FIRST, 15 | "Top": MusicDao.URL_MV_TOP, 16 | '推荐': MusicDao.URL_MV_PERSONAL, 17 | }; 18 | 19 | const areas = ['内地', '港台', '欧美', '日本', '韩国']; 20 | 21 | class _MVPageState extends State 22 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 23 | TabController tabController; //tab控制器 24 | 25 | @override 26 | bool get wantKeepAlive => true; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | 32 | areas.forEach((item) { 33 | types[item] = MusicDao.URL_MV_AREA + item; 34 | }); 35 | 36 | //初始化controller并添加监听 37 | tabController = TabController(length: types.length, vsync: this); 38 | tabController.addListener(() => _onTabChanged()); 39 | } 40 | 41 | void _onTabChanged() { 42 | if (tabController.index.toDouble() == tabController.animation.value) {} 43 | } 44 | 45 | Widget mWidget; 46 | @override 47 | Widget build(BuildContext context) { 48 | super.build(context); 49 | ColorStyleProvider colorStyleProvider = 50 | Provider.of(context); 51 | return Scaffold( 52 | appBar: AppBar( 53 | backgroundColor: Color(0x07000000), 54 | elevation: 0, 55 | title: TabBar( 56 | controller: tabController, //控制器 57 | indicatorColor: colorStyleProvider.getIndicatorColor(), 58 | labelColor: colorStyleProvider.getCurrentColor(), 59 | unselectedLabelColor: Colors.black45, 60 | labelStyle: TextStyle(fontWeight: FontWeight.w600), //选中的样式 61 | unselectedLabelStyle: TextStyle(fontSize: 14), //未选中的样式 62 | isScrollable: true, //是否可滑动 63 | //tab标签 64 | tabs: types.keys.map((name) { 65 | return Tab( 66 | text: name, 67 | ); 68 | }).toList(), 69 | //点击事件 70 | onTap: (int i) { 71 | tabController.animateTo(i); 72 | }, 73 | ), 74 | ), 75 | body: TabBarView( 76 | controller: tabController, 77 | children: types.values.map((url) { 78 | return MVTabPage(url: url); 79 | }).toList(), 80 | ), 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/widget/song_item_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:flutter_music_player/model/music_controller.dart'; 4 | import 'package:flutter_music_player/model/song_util.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class SongItemText extends StatefulWidget { 8 | final List songList; 9 | final int index; 10 | final Function onItemTap; 11 | const SongItemText(this.songList, this.index, {Key key, this.onItemTap}) 12 | : super(key: key); 13 | 14 | @override 15 | _SongItemTextState createState() => _SongItemTextState(); 16 | } 17 | 18 | class _SongItemTextState extends State { 19 | bool selected = false; 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | MusicController musicController = 28 | Provider.of(context, listen: false); 29 | Map song = widget.songList[widget.index]; 30 | Color itemColor = Colors.black54; 31 | if (widget.index == musicController.getCurrentIndex()) { 32 | itemColor = Provider.of(context, listen: false) 33 | .getCurrentColor(); 34 | } 35 | return InkWell( 36 | onTap: () { 37 | musicController.playIndex(widget.index); 38 | widget.onItemTap(); 39 | }, 40 | child: Container( 41 | padding: EdgeInsets.symmetric(horizontal: 32), 42 | child: RichText( 43 | maxLines: 1, 44 | overflow: TextOverflow.ellipsis, 45 | text: TextSpan(children: [ 46 | TextSpan( 47 | text: song['name'], 48 | style: TextStyle(fontSize: 15.0, color: itemColor), 49 | ), 50 | TextSpan( 51 | text: ' - ' + SongUtil.getArtistNames(song), 52 | style: TextStyle(fontSize: 13.0, color: itemColor)) 53 | ])), 54 | /* child: Row( 55 | children: [ 56 | Text( 57 | "${song['name']}", 58 | maxLines: 1, 59 | overflow: TextOverflow.ellipsis, 60 | style: TextStyle(fontSize: 14.0, color: itemColor), 61 | ), 62 | Text( 63 | " - ${SongUtil.getArtistNames(song)}", 64 | maxLines: 1, 65 | overflow: TextOverflow.ellipsis, 66 | style: TextStyle(fontSize: 12.0, color: itemColor), 67 | ) 68 | ], 69 | ), */ 70 | )); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/dao/music_db_favorite.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_music_player/model/song_util.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | import 'music_db.dart'; 5 | 6 | 7 | class FavoriteDB { 8 | static const table_name = 't_favorite'; 9 | 10 | 11 | /// 单例对象的写法 12 | // 私有静态instance 13 | static FavoriteDB _instance; 14 | 15 | // 对外访问点,指向私有静态方法 16 | factory FavoriteDB() => _getInstance(); 17 | 18 | static FavoriteDB _getInstance() { 19 | if (_instance == null) { 20 | _instance = FavoriteDB._(); 21 | } 22 | return _instance; 23 | } 24 | 25 | // 将默认构造函数私有化 26 | FavoriteDB._(); 27 | 28 | /// 在数据库onCreate的时候,创建表。 29 | /// 注意:onUpgrade中添加的字段要在这儿添加,不然第一次安装就没有那个字段了。 30 | createTable(Database db) { 31 | db.execute('''create table $table_name ( 32 | id integer primary key, 33 | name text not null, 34 | artist text, 35 | cover text, 36 | url text, 37 | createTime integer) 38 | '''); 39 | } 40 | 41 | Future addFavorite(Map song) async { 42 | var fav = { 43 | 'id': song['id'], 44 | 'name': song['name'], 45 | 'artist': SongUtil.getArtistNames(song), 46 | 'cover': SongUtil.getSongImage(song, size: 0), 47 | 'url': SongUtil.getSongUrl(song), 48 | // 查看sqflite文档,发现不支持DateTime字段,用int来存储。 49 | 'createTime': DateTime.now().millisecondsSinceEpoch, 50 | }; 51 | 52 | return (await MusicDB().getDB()).insert(table_name, fav); 53 | } 54 | 55 | Future updateFavorite(Map song) async { 56 | var fav = { 57 | 'cover': song['imageUrl'], 58 | }; 59 | 60 | return (await MusicDB().getDB()).update(table_name, fav, where: 'id = ${song['id']}'); 61 | } 62 | 63 | Future>> getFavoriteList() async { 64 | Database db = await MusicDB().getDB(); 65 | List list = await db.query(table_name, orderBy: 'createTime desc'); 66 | List songs = list 67 | .map((fav) => { 68 | 'id': fav['id'], 69 | 'name': fav['name'], 70 | 'artistNames': fav['artist'], 71 | 'imageUrl': fav['cover'] 72 | }) 73 | .toList(); 74 | 75 | return songs; 76 | } 77 | 78 | Future> getFavoriteById(var id) async { 79 | Database db = await MusicDB().getDB(); 80 | List list = await db.query(table_name, where: 'id = ?', whereArgs: [id]); 81 | return list.length > 0 ? list[0] : null; 82 | } 83 | 84 | Future deleteFavorite(var id) async { 85 | Database db = await MusicDB().getDB(); 86 | int re = await db.delete(table_name, where: 'id = ?', whereArgs: [id]); 87 | if (re <= 0) { 88 | throw Exception('删除失败'); 89 | } else { 90 | return re; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /lib/pages/tabs_bottom.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:flutter_music_player/widget/text_icon.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class BottomTabs extends StatelessWidget { 7 | final int currentIndex; 8 | final ValueChanged tapCallback; 9 | final bool showFloatPlayer; 10 | 11 | BottomTabs(this.currentIndex, this.tapCallback, this.showFloatPlayer); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return showFloatPlayer 16 | ? _buildBottomAppBar(context) 17 | : _buildBottomNavigationBar(context); 18 | } 19 | 20 | _buildBottomAppBar(BuildContext context) { 21 | return BottomAppBar( 22 | color: Color(0xffffffff), 23 | shape: CircularNotchedRectangle(), 24 | notchMargin: 4.0, 25 | child: Row( 26 | mainAxisSize: MainAxisSize.max, 27 | mainAxisAlignment: MainAxisAlignment.spaceAround, 28 | children: [ 29 | TextIcon( 30 | icon: Icons.whatshot, 31 | title: '发现', 32 | selected: currentIndex == 0, 33 | onPressed: () => tapCallback(0), 34 | ), 35 | TextIcon( 36 | icon: Icons.library_music, 37 | title: '歌单', 38 | selected: currentIndex == 1, 39 | onPressed: () => tapCallback(1), 40 | ), 41 | SizedBox(width: 70.0), 42 | TextIcon( 43 | icon: Icons.movie, 44 | title: 'MV', 45 | selected: currentIndex == 2, 46 | onPressed: () => tapCallback(2), 47 | ), 48 | TextIcon( 49 | icon: Icons.favorite, 50 | title: '收藏', 51 | selected: currentIndex == 3, 52 | onPressed: () => tapCallback(3), 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | 59 | _buildBottomNavigationBar(BuildContext context) { 60 | return BottomNavigationBar( 61 | currentIndex: currentIndex, 62 | onTap: tapCallback, 63 | type: BottomNavigationBarType.fixed, 64 | fixedColor: Provider.of(context).getCurrentColor(), 65 | items: [ 66 | BottomNavigationBarItem( 67 | icon: Icon(Icons.whatshot), 68 | title: Text('发现'), 69 | ), 70 | BottomNavigationBarItem( 71 | icon: Icon(Icons.library_music), 72 | title: Text('歌单'), 73 | ), 74 | BottomNavigationBarItem( 75 | icon: Icon(Icons.movie), 76 | title: Text('MV'), 77 | ), 78 | BottomNavigationBarItem( 79 | icon: Icon(Icons.favorite), 80 | title: Text('收藏'), 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/pages/artist_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/dao/music_163.dart'; 4 | import 'package:flutter_music_player/model/song_util.dart'; 5 | import 'package:flutter_music_player/pages/artist_detail.dart'; 6 | import 'package:flutter_music_player/utils/navigator_util.dart'; 7 | 8 | class ArtistListPage extends StatefulWidget { 9 | ArtistListPage({Key key}) : super(key: key); 10 | 11 | @override 12 | _ArtistListPageState createState() => _ArtistListPageState(); 13 | } 14 | 15 | class _ArtistListPageState extends State { 16 | List artistList = []; 17 | _getSongs() async { 18 | await MusicDao.getArtistList().then((result) { 19 | // 界面未加载,返回。 20 | if (!mounted) return; 21 | 22 | setState(() { 23 | artistList = result; 24 | }); 25 | }).catchError((e) { 26 | print('Failed: ${e.toString()}'); 27 | }); 28 | } 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _getSongs(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | appBar: AppBar( 40 | titleSpacing: 0.0, 41 | title: Text('热门歌手', style: TextStyle(fontSize: 16.0)), 42 | ), 43 | body: artistList.length == 0 44 | ? Center(child: CircularProgressIndicator()) 45 | : ListView.separated( 46 | itemCount: artistList.length, 47 | itemBuilder: (context, index) => _buildItem(index), 48 | separatorBuilder: (context, index) => 49 | Divider(height: 0.5, color: Colors.black12), 50 | ), 51 | ); 52 | } 53 | 54 | Widget _buildItem(int index) { 55 | Map artist = artistList[index]; 56 | return ListTile( 57 | leading: ClipOval( 58 | child: CachedNetworkImage( 59 | imageUrl: SongUtil.getArtistImage(artist), 60 | placeholder: (context, url) => 61 | Image.asset('images/music_2.jpg', fit: BoxFit.cover))), 62 | title: Text(artist['name'], style: TextStyle(fontSize: 14.0)), 63 | subtitle: Row( 64 | crossAxisAlignment: CrossAxisAlignment.end, 65 | children: [ 66 | Container( 67 | margin: EdgeInsets.only(top: 4.0), 68 | width: 100, 69 | child: Text('单曲:${artist['musicSize']}', 70 | style: TextStyle(fontSize: 12.0)), 71 | ), 72 | Text( 73 | '专辑:${artist['albumSize']}', 74 | style: TextStyle(fontSize: 12.0), 75 | ) 76 | ], 77 | ), 78 | onTap: () { 79 | NavigatorUtil.push(context, ArtistDetailPage(artist['id'])); 80 | }, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/widget/mv_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/song_util.dart'; 3 | import 'package:flutter_music_player/utils/navigator_util.dart'; 4 | import 'package:flutter_music_player/widget/my_video_player.dart'; 5 | 6 | import 'fullscreen_video_player.dart'; 7 | class MVItem extends StatefulWidget { 8 | final Map mv; 9 | MVItem(this.mv, {Key key}) : super(key: key); 10 | 11 | @override 12 | _MVItemState createState() => _MVItemState(); 13 | } 14 | 15 | 16 | class _MVItemState extends State { 17 | 18 | @override 19 | void dispose() { 20 | super.dispose(); 21 | print('MVItem dispose ${widget.mv['name']}'); 22 | } 23 | 24 | @override 25 | void deactivate() { 26 | super.deactivate(); 27 | //print('MVItem deactivate ${widget.mv['name']}'); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | SizedBox(height: 8.0), 36 | Padding( 37 | padding: EdgeInsets.symmetric(horizontal: 10.0), 38 | child: AspectRatio( 39 | // 设定宽高比 40 | aspectRatio: 16 / 9, 41 | child: ClipRRect( 42 | // 圆角 43 | borderRadius: BorderRadius.circular(8.0), 44 | child: MyVideoPlayer( 45 | mv: widget.mv, 46 | onResizePressed: (controller){ 47 | NavigatorUtil.pushFade(context, FullScreenVideoPlayer(controller, mv: widget.mv)); 48 | } 49 | ), 50 | ), 51 | ) 52 | ), 53 | Padding( 54 | padding: EdgeInsets.fromLTRB(14.0, 6.0, 14.0, 4.0), 55 | child: Text( 56 | widget.mv['name'], 57 | style: TextStyle( 58 | fontSize: 16.0, 59 | color: Colors.black54, 60 | fontWeight: FontWeight.w600), 61 | maxLines: 1, 62 | overflow: TextOverflow.ellipsis, 63 | ) 64 | ), 65 | Padding( 66 | padding: EdgeInsets.fromLTRB(14.0, 0.0, 14.0, 8.0), 67 | child: Text( 68 | SongUtil.getArtistNames(widget.mv), 69 | style: TextStyle(fontSize: 14.0, color: Colors.black54), 70 | maxLines: 1, 71 | overflow: TextOverflow.ellipsis, 72 | ) 73 | ), 74 | Divider( 75 | color: Color(0x0f000000), 76 | height: 12.0, // 间隔的高度 77 | thickness: 8.0, // 绘制的线的厚度 78 | ) 79 | ], 80 | ); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /android/speech/src/main/java/xwh/lib/speech/AsrPlugin.java: -------------------------------------------------------------------------------- 1 | package xwh.lib.speech; 2 | 3 | import android.app.Activity; 4 | import android.util.Log; 5 | 6 | import io.flutter.plugin.common.MethodCall; 7 | import io.flutter.plugin.common.MethodChannel; 8 | import io.flutter.plugin.common.PluginRegistry; 9 | 10 | public class AsrPlugin implements MethodChannel.MethodCallHandler{ 11 | 12 | private AsrManager asrManager; 13 | private Activity activity; 14 | private MethodChannel.Result result; // 注意result对象时一次性的。 15 | private boolean isFinished = false; 16 | 17 | public static void registerWith(PluginRegistry.Registrar registrar) { 18 | MethodChannel methodChannel = new MethodChannel(registrar.messenger(), "speech_plugin"); 19 | AsrPlugin instance = new AsrPlugin(registrar); 20 | methodChannel.setMethodCallHandler(instance); 21 | } 22 | 23 | public AsrPlugin(PluginRegistry.Registrar registrar) { 24 | activity = registrar.activity(); 25 | asrManager = AsrManager.getInstance(); 26 | asrManager.setSpeechListener(new AsrManager.SpeechListener() { 27 | @Override 28 | public void onResult(String text) { 29 | if (isFinished) { // result对象不能重复回复,不然报错 30 | return; 31 | } 32 | isFinished = true; 33 | Log.d("AsrPlugin", "onResult: " + text); 34 | AsrPlugin.this.result.success(text); 35 | } 36 | 37 | @Override 38 | public void onError(String error) { 39 | if (isFinished) { 40 | return; 41 | } 42 | isFinished = true; 43 | Log.d("AsrPlugin", "onError: " + error); 44 | AsrPlugin.this.result.error(error, null, null); 45 | } 46 | 47 | @Override 48 | public void onEnd() { 49 | if (!isFinished) { 50 | isFinished = true; 51 | AsrPlugin.this.result.error("未识别到内容", null, null); 52 | } 53 | Log.d("AsrPlugin", "onEnd"); 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { 60 | switch (methodCall.method) { 61 | case "init": 62 | asrManager.init(activity); 63 | break; 64 | case "start": 65 | this.result = result; 66 | isFinished = false; 67 | asrManager.start(); 68 | break; 69 | case "stop": 70 | asrManager.stop(); 71 | break; 72 | case "cancel": 73 | asrManager.cancel(); 74 | break; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Using a CDN with CocoaPods 1.7.2 or later can save a lot of time on pod installation, but it's experimental rather than the default. 2 | # source 'https://cdn.cocoapods.org/' 3 | 4 | # Uncomment this line to define a global platform for your project 5 | # platform :ios, '9.0' 6 | 7 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 8 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 9 | 10 | project 'Runner', { 11 | 'Debug' => :debug, 12 | 'Profile' => :release, 13 | 'Release' => :release, 14 | } 15 | 16 | def parse_KV_file(file, separator='=') 17 | file_abs_path = File.expand_path(file) 18 | if !File.exists? file_abs_path 19 | return []; 20 | end 21 | pods_ary = [] 22 | skip_line_start_symbols = ["#", "/"] 23 | File.foreach(file_abs_path) { |line| 24 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 25 | plugin = line.split(pattern=separator) 26 | if plugin.length == 2 27 | podname = plugin[0].strip() 28 | path = plugin[1].strip() 29 | podpath = File.expand_path("#{path}", file_abs_path) 30 | pods_ary.push({:name => podname, :path => podpath}); 31 | else 32 | puts "Invalid plugin specification: #{line}" 33 | end 34 | } 35 | return pods_ary 36 | end 37 | 38 | target 'Runner' do 39 | use_frameworks! 40 | 41 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 42 | # referring to absolute paths on developers' machines. 43 | system('rm -rf .symlinks') 44 | system('mkdir -p .symlinks/plugins') 45 | 46 | # Flutter Pods 47 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 48 | if generated_xcode_build_settings.empty? 49 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first." 50 | end 51 | generated_xcode_build_settings.map { |p| 52 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 53 | symlink = File.join('.symlinks', 'flutter') 54 | File.symlink(File.dirname(p[:path]), symlink) 55 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 56 | end 57 | } 58 | 59 | # Plugin Pods 60 | plugin_pods = parse_KV_file('../.flutter-plugins') 61 | plugin_pods.map { |p| 62 | symlink = File.join('.symlinks', 'plugins', p[:name]) 63 | File.symlink(p[:path], symlink) 64 | pod p[:name], :path => File.join(symlink, 'ios') 65 | } 66 | end 67 | 68 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 69 | install! 'cocoapods', :disable_input_output_paths => true 70 | 71 | post_install do |installer| 72 | installer.pods_project.targets.each do |target| 73 | target.build_configurations.each do |config| 74 | config.build_settings['ENABLE_BITCODE'] = 'NO' 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/pages/play_list_tab_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_163.dart'; 3 | import 'package:flutter_music_player/dao/music_db_playlist.dart'; 4 | import 'package:flutter_music_player/widget/play_list_item.dart'; 5 | 6 | class PlayListTabPage extends StatefulWidget { 7 | static const TYPE_DB = 'db'; // 表示从数据库去取 8 | final String type; 9 | final String heroTag; 10 | final String error; 11 | PlayListTabPage( 12 | {Key key, @required this.type, this.heroTag = 'from_list', this.error}) 13 | : super(key: key); 14 | 15 | @override 16 | _PlayListTabPageState createState() => _PlayListTabPageState(); 17 | } 18 | 19 | class _PlayListTabPageState extends State { 20 | List _playlist; 21 | 22 | _getPlaylists() async { 23 | try { 24 | if (widget.type == PlayListTabPage.TYPE_DB) { 25 | _playlist = await PlayListDB().getPlayList(); 26 | } else { 27 | _playlist = await MusicDao.getPlayList(widget.type); 28 | } 29 | } catch (e) { 30 | print('Failed: $e'); 31 | } 32 | 33 | // 界面未加载或者已关闭,返回。 34 | if (!mounted) return; 35 | setState(() { 36 | _playlist = _playlist; 37 | }); 38 | } 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | print("PlayList: initState ${widget.type}"); 44 | _getPlaylists(); 45 | } 46 | 47 | @override 48 | void dispose() { 49 | super.dispose(); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | if (this._playlist == null) { 55 | return Center( 56 | child: CircularProgressIndicator(), 57 | ); 58 | } else if (this._playlist.length == 0) { 59 | return _getErrorView(widget.error); 60 | } else { 61 | return GridView.builder( 62 | itemCount: this._playlist == null ? 0 : this._playlist.length, 63 | padding: EdgeInsets.all(6.0), // 四周边距,注意Card也有默认的边距 64 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 65 | // 网格样式 66 | crossAxisCount: 2, // 列数 67 | mainAxisSpacing: 2.0, // 主轴的间距 68 | crossAxisSpacing: 2.0, // cross轴间距 69 | childAspectRatio: 1 // item横竖比 70 | ), 71 | itemBuilder: (context, index) => _bulidItem(context, index), 72 | ); 73 | } 74 | } 75 | 76 | _bulidItem(BuildContext context, int index) { 77 | Map play = _playlist[index]; 78 | return Card( 79 | elevation: 4.0, 80 | child: PlayListItem(play, heroTag: widget.heroTag), 81 | ); 82 | } 83 | 84 | _getErrorView(String error) { 85 | if (error == null) { 86 | error = '加载失败'; 87 | } 88 | return Center( 89 | child: Text( 90 | error, 91 | textAlign: TextAlign.center, 92 | style: TextStyle(color: Colors.grey, height: 1.5), 93 | )); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/widget/music_progress_bar_2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MyProgressBar extends StatefulWidget { 4 | final Function onChanged; 5 | final Function onChangeStart; 6 | final Function onChangeEnd; 7 | 8 | final int duration; 9 | final int position; 10 | 11 | MyProgressBar({Key key, this.duration:1, this.position:0, this.onChanged, this.onChangeStart, this.onChangeEnd}): super(key: key); 12 | 13 | _MyProgressBarState createState() => _MyProgressBarState(); 14 | 15 | } 16 | 17 | class _MyProgressBarState extends State { 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | print('MyProgressBar initState'); 23 | } 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | // 坑很多:Slider注意范围越界的问题,而且duration不能为0.0 28 | // 歌曲切换的时候duration可能返回0。 29 | // 播放出错的时候,可能返回负数。 30 | double position = widget.position < 0 ? 0.0 : widget.position.toDouble(); 31 | double duration = widget.duration <= 0 ? 1.0 : widget.duration.toDouble(); 32 | if (position > duration) { 33 | position = 0; 34 | } 35 | //print('duration: $duration, position: $position'); 36 | 37 | final ThemeData theme = Theme.of(context); 38 | return Row( 39 | crossAxisAlignment: CrossAxisAlignment.center, 40 | children: [ 41 | Text(_getFormatTime(position.toInt()), 42 | style: TextStyle(color: Colors.white, fontSize: 12)), 43 | Expanded( 44 | child: SliderTheme( 45 | data: theme.sliderTheme.copyWith( 46 | thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0), 47 | overlayShape: RoundSliderOverlayShape(overlayRadius: 18.0), 48 | ), 49 | child: Slider.adaptive( 50 | value: position, 51 | min: 0.0, 52 | max: duration, 53 | onChanged: (double value) { 54 | widget.onChanged(value); 55 | }, 56 | onChangeStart: (double value) { 57 | widget.onChangeStart(value); 58 | }, 59 | onChangeEnd: (double value) { 60 | widget.onChangeEnd(value); 61 | }, 62 | ), 63 | ) 64 | ), 65 | Text( 66 | _getFormatTime(duration.toInt()), 67 | style: TextStyle(color: Colors.white, fontSize: 12), 68 | ), 69 | ], 70 | ); 71 | } 72 | 73 | String _getFormatTime(int milliseconds) { 74 | if (milliseconds == null) { 75 | milliseconds =0; 76 | } 77 | int seconds = milliseconds ~/ 1000; 78 | int minute = seconds ~/ 60; 79 | int hour = minute ~/ 60; 80 | String strHour = hour == 0 ? '' : '$hour:'; 81 | return "$strHour${_getTimeString(minute % 60)}:${_getTimeString(seconds % 60)}"; 82 | } 83 | 84 | String _getTimeString(int value) { 85 | return value > 9 ? "$value" : "0$value"; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/model/color_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_music_player/utils/shared_preference_util.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | /// 集齐了7色:红橙黄绿青蓝紫 9 | enum ColorStyle { pink, orange, lime, green, blue, indigo, purple } 10 | 11 | class ColorStyleProvider with ChangeNotifier { 12 | static ColorStyle currentStyle = ColorStyle.green; 13 | static const pref_color = 'colorStyle'; 14 | bool showPerformanceOverlay = false; // 是否在界面上显示性能调试层 15 | 16 | static final Map styles = { 17 | ColorStyle.pink: { 18 | 'mainColor': Colors.pink, 19 | 'mainLightColor': Colors.pinkAccent, 20 | 'indicatorColor': Colors.pink, 21 | }, 22 | ColorStyle.orange: { 23 | 'mainColor': Colors.deepOrange, 24 | 'mainLightColor': Colors.deepOrangeAccent, 25 | 'indicatorColor': Colors.orange, 26 | }, 27 | ColorStyle.lime: { 28 | 'mainColor': Colors.lightGreen, 29 | 'mainLightColor': Colors.limeAccent, 30 | 'indicatorColor': Colors.lime, 31 | }, 32 | ColorStyle.green: { 33 | 'mainColor': Colors.green, 34 | 'mainLightColor': Colors.lightGreenAccent, 35 | 'indicatorColor': Colors.orange, 36 | }, 37 | ColorStyle.blue: { 38 | 'mainColor': Colors.blue, 39 | 'mainLightColor': Colors.blueAccent, 40 | 'indicatorColor': Colors.blue, 41 | }, 42 | ColorStyle.indigo: { 43 | 'mainColor': Colors.indigo, 44 | 'mainLightColor': Colors.indigoAccent, 45 | 'indicatorColor': Colors.indigo, 46 | }, 47 | ColorStyle.purple: { 48 | 'mainColor': Colors.purple, 49 | 'mainLightColor': Colors.purpleAccent, 50 | 'indicatorColor': Colors.purple, 51 | }, 52 | }; 53 | 54 | ColorStyle getCurrentStyle() { 55 | return currentStyle; 56 | } 57 | 58 | MaterialColor getCurrentColor({String color = 'mainColor'}) { 59 | return getColor(getCurrentStyle(), color:color); 60 | } 61 | 62 | Color getLightColor() { 63 | return getColor(getCurrentStyle(), color:'mainLightColor'); 64 | } 65 | 66 | Color getIndicatorColor() { 67 | return getColor(getCurrentStyle(), color:'indicatorColor'); 68 | } 69 | 70 | Color getColor(ColorStyle style, {String color = 'mainColor'}) { 71 | Map group = styles[style]; 72 | return group[color]; 73 | } 74 | 75 | setStyle(ColorStyle style) { 76 | currentStyle = style; 77 | SharedPreferenceUtil.getInstance().setInt(pref_color, style.index); 78 | notifyListeners(); 79 | } 80 | 81 | static ColorStyle initColorStyle() { 82 | SharedPreferences prefs = SharedPreferenceUtil.getInstance(); 83 | if (prefs.containsKey(pref_color)) { 84 | int styleIndex = prefs.getInt(pref_color); 85 | currentStyle = ColorStyle.values[styleIndex]; 86 | } 87 | return currentStyle; 88 | } 89 | 90 | setShowPerformanceOverlay(bool visible) { 91 | this.showPerformanceOverlay = visible; 92 | notifyListeners(); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /lib/pages/history_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/dao/music_db_history.dart'; 3 | import 'package:flutter_music_player/widget/song_item_tile.dart'; 4 | 5 | class HistoryPage extends StatefulWidget { 6 | HistoryPage({Key key}) : super(key: key); 7 | 8 | _HistoryPageState createState() => _HistoryPageState(); 9 | } 10 | 11 | class _HistoryPageState extends State { 12 | List _songs; 13 | 14 | _getSongs() async { 15 | HistoryDB().getHistoryList().then((result) { 16 | // 界面未加载,返回。 17 | if (!mounted) return; 18 | 19 | setState(() { 20 | _songs = result; 21 | }); 22 | }).catchError((e) { 23 | print('Failed: ${e.toString()}'); 24 | }); 25 | } 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _getSongs(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Scaffold( 36 | appBar: AppBar( 37 | titleSpacing: 0.0, 38 | title: Text( 39 | '历史播放', 40 | style: TextStyle(fontSize: 16.0), 41 | ), 42 | actions: [ 43 | IconButton( 44 | icon: Icon(Icons.clear_all), 45 | onPressed: () { 46 | _clearHistory(context); 47 | }, 48 | ) 49 | ], 50 | ), 51 | body: _buildList(), 52 | ); 53 | } 54 | 55 | Widget _buildList() { 56 | if (_songs == null) { 57 | return Container(); 58 | } 59 | if (_songs.length == 0) { 60 | return Center( 61 | child: Text( 62 | '您还没有播放过歌曲', 63 | textAlign: TextAlign.center, 64 | style: TextStyle(color: Colors.grey, height: 1.5), 65 | )); 66 | } else { 67 | return ListView.builder( 68 | itemCount: this._songs.length, 69 | itemExtent: 70.0, // 设定item的高度,这样可以减少高度计算。 70 | itemBuilder: (context, index) => SongItemTile(_songs, index), 71 | ); 72 | } 73 | } 74 | 75 | void _clearHistory(context) { 76 | showDialog( 77 | context: context, 78 | barrierDismissible: true, 79 | builder: (_) => AlertDialog( 80 | title: Text('确认', style: TextStyle(fontSize: 16.0)), 81 | content: Text('清除所有播放记录?', style: TextStyle(fontSize: 14.0)), 82 | actions: [ 83 | new TextButton( 84 | child: new Text("不清除"), 85 | onPressed: () { 86 | Navigator.of(context).pop(); 87 | }, 88 | ), 89 | new TextButton( 90 | child: new Text("清除", style: TextStyle(color: Colors.red)), 91 | onPressed: () { 92 | HistoryDB().clearHistory().then((re) { 93 | Navigator.of(context).pop(); 94 | _getSongs(); 95 | }); 96 | }), 97 | ], 98 | )); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/model/song_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_music_player/utils/file_util.dart'; 2 | 3 | class SongUtil { 4 | static String getArtistNames(Map song) { 5 | if (song.containsKey('artistNames')) { 6 | return song['artistNames']; 7 | } 8 | 9 | String names = ''; 10 | List arList; 11 | 12 | if (song.containsKey('ar')) { 13 | arList = song['ar']; 14 | } else if (song.containsKey('artists')) { 15 | arList = song['artists']; 16 | } else { 17 | arList = song['song']['artists']; 18 | } 19 | 20 | if (arList != null) { 21 | bool isFirst = true; 22 | arList.forEach((ar) { 23 | if (isFirst) { 24 | isFirst = false; 25 | names = ar['name']; 26 | } else { 27 | names += " " + ar['name']; 28 | } 29 | }); 30 | } 31 | 32 | // 取了之后存下来,不用重复取了。 33 | song['artistNames'] = names; 34 | 35 | // 测试,不要在build里面调用相同的函数,会频繁执行。 36 | //print("getAritistNames: $names"); 37 | return names; 38 | } 39 | 40 | static String getSongImage(Map song, {int size:100, int width:0, int height:0}) { 41 | String imgUrl; 42 | if (song.containsKey('imageUrl')) { 43 | imgUrl = song['imageUrl']; 44 | } else { 45 | try { 46 | if (song.containsKey('al')) { 47 | imgUrl = song['al']['picUrl']; 48 | } else if (song.containsKey('song')) {// URL_NEW_SONGS里面的数据结构 49 | imgUrl = song['song']['album']['picUrl']; 50 | } 51 | if (imgUrl != null) { 52 | song['imageUrl'] = imgUrl; // 取一次之后存下来,不用后面计算。 53 | } 54 | } catch(e) { 55 | print(e); 56 | print(song['name']); 57 | return ''; 58 | } 59 | } 60 | 61 | if (imgUrl == null || imgUrl.length == 0) { 62 | return ''; 63 | } 64 | if (width > 0 && height > 0) { 65 | imgUrl += '?param=${width}y$height'; 66 | } else if (size > 0) { 67 | imgUrl += '?param=${size}y$size'; 68 | } 69 | 70 | //print('imageUrl: $imgUrl'); 71 | return imgUrl; 72 | } 73 | 74 | 75 | static String getSongUrl(Map song) { 76 | return "https://music.163.com/song/media/outer/url?id=${song['id']}.mp3"; 77 | } 78 | 79 | 80 | static Future getPlayPath(Map song) async{ 81 | String localPath = await FileUtil.getSongLocalPath(song['id']); 82 | if (await FileUtil.isFileExists(localPath)) { 83 | return localPath; 84 | } else { 85 | return getSongUrl(song); 86 | } 87 | } 88 | 89 | 90 | static Future isSongDownloaded(int id) async{ 91 | String localPath = await FileUtil.getSongLocalPath(id); 92 | return FileUtil.isFileExists(localPath); 93 | } 94 | 95 | 96 | static String getArtistImage(Map artist, {int size:100, int width:0, int height:0}) { 97 | String imgUrl = artist['picUrl']; 98 | if (width > 0 && height > 0) { 99 | imgUrl += '?param=${width}y$height'; 100 | } else if (size > 0) { 101 | imgUrl += '?param=${size}y$size'; 102 | } 103 | return imgUrl; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /lib/test/test_scroll_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TestScrollPosition extends StatefulWidget { 4 | @override 5 | _TestScrollPositionState createState() => _TestScrollPositionState(); 6 | } 7 | 8 | class _TestScrollPositionState extends State { 9 | ScrollController _controller; 10 | 11 | final double itemHeight = 100.0; 12 | final int visibleItemSize = 5; 13 | List items; 14 | int selectedIndex = 0; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | _controller = ScrollController(); 20 | items = List.generate(10, (i) => i + 1); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text("Test Scroll Position"), 28 | actions: [ 29 | FlatButton( 30 | child: Text( 31 | 'up', 32 | style: TextStyle(color: Colors.white), 33 | ), 34 | onPressed: () => _scroll(-1), 35 | ), 36 | FlatButton( 37 | child: Text( 38 | 'down', 39 | style: TextStyle(color: Colors.white), 40 | ), 41 | onPressed: () => _scroll(1), 42 | ), 43 | ], 44 | ), 45 | body: Container( 46 | height: itemHeight * visibleItemSize, 47 | color: Colors.black45, 48 | child: CustomScrollView(controller: _controller, slivers: [ 49 | SliverList( 50 | delegate: SliverChildListDelegate( 51 | items.map((item) => _getItem(item)).toList()), 52 | ), 53 | ]), 54 | )); 55 | } 56 | 57 | Widget _getItem(int item) { 58 | return Container( 59 | height: itemHeight, 60 | //margin: EdgeInsets.all(2.0), 61 | //color: Colors.greenAccent, 62 | alignment: Alignment.center, 63 | child: Text( 64 | '$item', 65 | style: TextStyle( 66 | fontSize: 36.0, 67 | color: 68 | (item == selectedIndex + 1) ? Colors.white : Colors.white60), 69 | )); 70 | } 71 | 72 | void _scroll(int oriantation) { 73 | int index = selectedIndex + oriantation; 74 | 75 | // 选中的Index是否超出边界 76 | if (index < 0 || index >= items.length) { 77 | return; 78 | } 79 | 80 | int offset = visibleItemSize~/2; 81 | int topIndex = index - offset; // 选中元素居中时,top的Index 82 | int bottomIndex = index + offset; 83 | 84 | setState(() { 85 | selectedIndex = index; 86 | }); 87 | 88 | print("scoll:${_controller.offset}"); 89 | 90 | if (topIndex < 0 && _controller.offset<=0) { 91 | return; 92 | } 93 | if (bottomIndex >= items.length && _controller.offset >= (items.length - visibleItemSize)*itemHeight) { 94 | return; 95 | } 96 | 97 | _controller.animateTo(topIndex * itemHeight, 98 | duration: Duration(seconds: 1), curve: Curves.easeInOut); 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/dao/music_db_history.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_music_player/model/song_util.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | import 'music_db.dart'; 5 | 6 | class HistoryDB { 7 | static const table_name = 't_history'; 8 | 9 | /// 单例对象的写法 10 | // 私有静态instance 11 | static HistoryDB _instance; 12 | 13 | // 对外访问点,指向私有静态方法 14 | factory HistoryDB() => _getInstance(); 15 | 16 | static HistoryDB _getInstance() { 17 | if (_instance == null) { 18 | _instance = HistoryDB._(); 19 | } 20 | return _instance; 21 | } 22 | 23 | // 将默认构造函数私有化 24 | HistoryDB._(); 25 | 26 | /// 在数据库onCreate的时候,创建表。 27 | /// 注意:onUpgrade中添加的字段要在这儿添加,不然第一次安装就没有那个字段了。 28 | createTable(Database db) { 29 | db.execute('''create table $table_name ( 30 | id integer primary key, 31 | name text not null, 32 | artist text, 33 | cover text, 34 | url text, 35 | createTime integer) 36 | '''); 37 | } 38 | 39 | Future addHistory(Map song) async { 40 | if ((await getHistoryById(song['id']) != null)) { 41 | return updateHistory(song); 42 | } else { 43 | var fav = { 44 | 'id': song['id'], 45 | 'name': song['name'], 46 | 'artist': SongUtil.getArtistNames(song), 47 | 'cover': SongUtil.getSongImage(song, size: 0), 48 | 'url': SongUtil.getSongUrl(song), 49 | // 查看sqflite文档,发现不支持DateTime字段,用int来存储。 50 | 'createTime': DateTime.now().millisecondsSinceEpoch, 51 | }; 52 | return (await MusicDB().getDB()).insert(table_name, fav); 53 | } 54 | } 55 | 56 | Future> getHistoryById(var id) async { 57 | Database db = await MusicDB().getDB(); 58 | List list = await db.query(table_name, where: 'id = ?', whereArgs: [id]); 59 | return list.length > 0 ? list[0] : null; 60 | } 61 | 62 | Future updateHistory(Map song) async { 63 | // 更新播放时间 64 | var fav = { 65 | 'createTime': DateTime.now().millisecondsSinceEpoch, 66 | }; 67 | 68 | return (await MusicDB().getDB()) 69 | .update(table_name, fav, where: 'id = ${song['id']}'); 70 | } 71 | 72 | Future>> getHistoryList() async { 73 | Database db = await MusicDB().getDB(); 74 | List list = await db.query(table_name, orderBy: 'createTime desc'); 75 | List songs = list 76 | .map((fav) => { 77 | 'id': fav['id'], 78 | 'name': fav['name'], 79 | 'artistNames': fav['artist'], 80 | 'imageUrl': fav['cover'] 81 | }) 82 | .toList(); 83 | 84 | return songs; 85 | } 86 | 87 | Future deleteHistory(var id) async { 88 | Database db = await MusicDB().getDB(); 89 | int re = await db.delete(table_name, where: 'id = ?', whereArgs: [id]); 90 | if (re <= 0) { 91 | throw Exception('删除失败'); 92 | } else { 93 | return re; 94 | } 95 | } 96 | 97 | Future clearHistory() async { 98 | Database db = await MusicDB().getDB(); 99 | return await db.delete(table_name); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/widget/music_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MusicProgressBar extends StatefulWidget { 4 | 5 | _MusicProgressBarState _state; 6 | Function onChanged; 7 | Function onChangeStart; 8 | Function onChangeEnd; 9 | 10 | MusicProgressBar({Key key, this.onChanged, this.onChangeStart, this.onChangeEnd}): super(key: key); 11 | 12 | _MusicProgressBarState createState(){ 13 | _state = _MusicProgressBarState(); 14 | return _state; 15 | } 16 | 17 | void setDuration(int duration){ 18 | _state?.setDuration(duration); 19 | } 20 | 21 | void updatePosition(int position){ 22 | _state?.setPosition(position); 23 | } 24 | 25 | 26 | } 27 | 28 | class _MusicProgressBarState extends State { 29 | int duration = 0; 30 | int position = 0; 31 | bool isTaping = false; // 是否在手动拖动(拖动的时候进度条不要自己动 32 | 33 | void setDuration(int duration){ 34 | setState(() { 35 | this.duration = duration; 36 | }); 37 | } 38 | 39 | void setPosition(int position, {bool byHander:false}){ 40 | if (isTaping && !byHander) { 41 | return; 42 | } 43 | setState(() { 44 | this.position = position; 45 | }); 46 | } 47 | 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | final ThemeData theme = Theme.of(context); 52 | return Row( 53 | crossAxisAlignment: CrossAxisAlignment.center, 54 | children: [ 55 | Text(_getFormatTime(position), 56 | style: TextStyle(color: Colors.white, fontSize: 12)), 57 | Expanded( 58 | child: SliderTheme( 59 | data: theme.sliderTheme.copyWith( 60 | thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0), 61 | overlayShape: RoundSliderOverlayShape(overlayRadius: 18.0), 62 | ), 63 | child: Slider.adaptive( 64 | value: position.toDouble(), 65 | min: 0.0, 66 | max: duration == 0 ? 1.0 : duration.toDouble(), 67 | onChanged: (double value) { 68 | setPosition(value.toInt(), byHander: true); 69 | widget.onChanged(value); 70 | }, 71 | onChangeStart: (double value) { 72 | isTaping = true; 73 | widget.onChangeStart(value); 74 | }, 75 | onChangeEnd: (double value) { 76 | isTaping = false; 77 | widget.onChangeEnd(value); 78 | }, 79 | ), 80 | ) 81 | ), 82 | Text( 83 | _getFormatTime(duration), 84 | style: TextStyle(color: Colors.white, fontSize: 12), 85 | ), 86 | ], 87 | ); 88 | } 89 | 90 | String _getFormatTime(int seconds) { 91 | int minute = seconds ~/ 60; 92 | int hour = minute ~/ 60; 93 | String strHour = hour == 0 ? '' : '$hour:'; 94 | return "$strHour${_getTimeString(minute % 60)}:${_getTimeString(seconds % 60)}"; 95 | } 96 | 97 | String _getTimeString(int value) { 98 | return value > 9 ? "$value" : "0$value"; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/dao/api_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:flutter_music_player/utils/file_util.dart'; 6 | import 'package:flutter_music_player/utils/network_util.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | class APICache { 10 | static const String dirName = 'cache'; 11 | static const Duration CACHE_TIMEOUT_API = Duration(hours: 6); // api缓存超时时长。 12 | static const Duration CACHE_TIMEOUT_FILE = Duration(days: 7); // 文件缓存超时时长。 13 | 14 | static Future getLocalFile(String url) async { 15 | String fileName = md5.convert(utf8.encode(url)).toString(); 16 | String dir = (await FileUtil.createLocalDir(dirName)).path; 17 | return new File('$dir/$fileName'); 18 | } 19 | 20 | static Future getCache(String url, {checkCacheTimeout: true}) async { 21 | String cache; 22 | File file = await getLocalFile(url); 23 | if (await file.exists()) { 24 | // 判断网络和缓存时间 25 | if (checkCacheTimeout && 26 | NetworkUtil().isNetworkAvailable() && 27 | FileUtil.isFileTimeout(file, CACHE_TIMEOUT_API)) { 28 | // 缓存超时了,并且网络可用,丢掉之前的。 29 | //file.delete(); // 网络请求成功才删除。 30 | print('缓存超时:$url'); 31 | } else { 32 | cache = await file.readAsString(); 33 | print('从缓存获取:$url'); 34 | } 35 | } 36 | return cache; 37 | } 38 | 39 | static Future saveCache(String url, String cache) async { 40 | File file = await getLocalFile(url); 41 | print('saveCache to: ${file.path}'); 42 | File fileCached; 43 | try { 44 | fileCached = await file.writeAsString(cache); 45 | } catch (e) { 46 | print(e); 47 | } 48 | return fileCached?.exists(); 49 | } 50 | 51 | static Future deleteCache(String url) async { 52 | File file = await getLocalFile(url); 53 | print('deleteCache: ${file.path}'); 54 | return file.delete(); 55 | } 56 | 57 | /// 清理一段时间之前的缓存 58 | /// 例如歌词文件 59 | static Future clearCache() async { 60 | int count = 0; 61 | // 两天才检查一次,不用每次启动都遍历一次文件夹。 62 | SharedPreferences prefs = await SharedPreferences.getInstance(); 63 | int lastTime = prefs.getInt('lastClearCacheTime') ?? 0; 64 | DateTime lastClearTime = DateTime.fromMillisecondsSinceEpoch(lastTime); 65 | if (FileUtil.isTimeOut(lastClearTime, CACHE_TIMEOUT_FILE)) { 66 | String path = await FileUtil.getSubDirPath(dirName); 67 | Directory dir = Directory(path); 68 | if (await dir.exists()) { 69 | List list = dir.listSync(); 70 | if (list.length > 300) { // 文件太多才清理。 71 | list.forEach((item) { 72 | File file = File(item.path); 73 | if (FileUtil.isFileTimeout(file, CACHE_TIMEOUT_FILE)) { 74 | file.delete(); 75 | print('缓存文件过期,cache: ${file.path}'); 76 | count++; 77 | } 78 | }); 79 | } 80 | } 81 | prefs.setInt('lastClearCacheTime', DateTime.now().millisecondsSinceEpoch); 82 | 83 | if (count == 0) { 84 | print('没有缓存过期'); 85 | } 86 | } else { 87 | print('Last time of clear cache is : ${lastClearTime.toString()}'); 88 | } 89 | 90 | return count; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/widget/my_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 点击会有动画的按钮,并支持图片切换。 4 | class MyIconButton extends StatefulWidget { 5 | final Function onPressed; 6 | final List icons; 7 | final IconData icon; 8 | final int iconIndex; 9 | final double size; 10 | final Color color; 11 | final bool animEnable; 12 | 13 | /// * [icon], 如果只需要展示一张图片。 14 | /// * [icons], 如果要展示多张图片,点击时会依次向后切换。 15 | MyIconButton( 16 | {Key key, 17 | this.icon, 18 | this.icons, 19 | this.iconIndex: 0, 20 | this.size: 24, 21 | this.onPressed, 22 | this.animEnable: true, 23 | this.color: Colors.white}); 24 | 25 | @override 26 | _MyIconButtonState createState() => _MyIconButtonState(); 27 | } 28 | 29 | class _MyIconButtonState extends State 30 | with SingleTickerProviderStateMixin { 31 | Animation animation; 32 | AnimationController _controller; 33 | int iconIndex = 0; 34 | bool isAnimRunning = false; // 动画中途不要被打断,避免闪烁的情况。 35 | final double defaultOpacity = 0.8; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | iconIndex = widget.iconIndex; 41 | _controller = 42 | AnimationController(vsync: this, duration: Duration(milliseconds: 200)) 43 | ..addListener(() { 44 | setState(() {}); 45 | }); 46 | 47 | animation = new Tween(begin: 0.0, end: 1.0).animate(_controller); 48 | animation.addStatusListener((status) { 49 | if (status == AnimationStatus.completed) { 50 | // 如果只有一张图,不需要动画 51 | if (widget.icons != null && widget.icons.length > 1) { 52 | int nextIndex; 53 | if (iconIndex != widget.iconIndex) { 54 | nextIndex = widget.iconIndex; 55 | } 56 | setState(() { 57 | iconIndex = nextIndex; 58 | }); 59 | 60 | //print('Anim completed, iconIndex: $iconIndex '); 61 | } 62 | //动画执行结束时反向执行动画 63 | _controller.reverse(); 64 | } else if (status == AnimationStatus.reverse) { 65 | //print('Anim reverse, iconIndex: $iconIndex '); 66 | isAnimRunning = false; 67 | } 68 | }); 69 | } 70 | 71 | @override 72 | void dispose() { 73 | _controller.dispose(); 74 | super.dispose(); 75 | } 76 | 77 | void startAnim() { 78 | _controller.forward(); 79 | isAnimRunning = true; 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | //print('IconIndex: $iconIndex , widgetIconIndex: ${widget.iconIndex}, isRunning: $isAnimRunning'); 85 | 86 | if (!isAnimRunning && widget.iconIndex != iconIndex) { 87 | //iconIndex = widget.iconIndex; 88 | // 当外部有状态改变时,自动触发动画 89 | startAnim(); 90 | } 91 | 92 | return GestureDetector( 93 | onTap: () { 94 | if (widget.animEnable) { 95 | startAnim(); 96 | } 97 | widget.onPressed(); 98 | }, 99 | child: Transform.scale( 100 | scale: 1.0 - _controller.value * 0.2, 101 | child: Icon( 102 | widget.icons == null ? widget.icon : widget.icons[iconIndex], 103 | size: widget.size, 104 | color: widget.color 105 | .withOpacity((1.0 - _controller.value) * defaultOpacity)))); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_music_player 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.2 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | audioplayer: 23 | path: ./audioplayer 24 | flutter_swiper: ^1.1.6 25 | cached_network_image: ^3.0.0 26 | video_player: ^2.1.1 27 | crypto: ^3.0.1 28 | connectivity: ^3.0.3 # 网络监测 29 | orientation: ^1.3.0 # 屏幕横竖屏 30 | sqflite: ^2.0.0+3 31 | dio: ^4.0.0 32 | fluttertoast: ^8.0.6 33 | provider: ^5.0.0 34 | shared_preferences: ^2.0.5 35 | flutter_screenutil: ^5.0.0 36 | 37 | # The following adds the Cupertino Icons font to your application. 38 | # Use with the CupertinoIcons class for iOS style icons. 39 | cupertino_icons: ^1.0.2 40 | 41 | dev_dependencies: 42 | flutter_test: 43 | sdk: flutter 44 | 45 | # For information on the generic Dart part of this file, see the 46 | # following page: https://dart.dev/tools/pub/pubspec 47 | 48 | # The following section is specific to Flutter. 49 | flutter: 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | assets: 57 | - images/music_2.jpg 58 | - images/placeholder_play_list.jpg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware. 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | # To add custom fonts to your application, add a fonts section here, 65 | # in this "flutter" section. Each entry in this list should have a 66 | # "family" key with the font family name, and a "fonts" key with a 67 | # list giving the asset and other descriptors for the font. For 68 | # example: 69 | # fonts: 70 | # - family: Schyler 71 | # fonts: 72 | # - asset: fonts/Schyler-Regular.ttf 73 | # - asset: fonts/Schyler-Italic.ttf 74 | # style: italic 75 | # - family: Trajan Pro 76 | # fonts: 77 | # - asset: fonts/TrajanPro.ttf 78 | # - asset: fonts/TrajanPro_Bold.ttf 79 | # weight: 700 80 | # 81 | # For details regarding fonts from package dependencies, 82 | # see https://flutter.dev/custom-fonts/#from-packages 83 | fonts: 84 | - family: myIcon #指定一个字体名 85 | fonts: 86 | - asset: fonts/iconfont.ttf 87 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | signingConfigs { 40 | config { 41 | keyAlias 'key_music' 42 | keyPassword 'pwd123456' 43 | storeFile file('./keystore.jks') 44 | storePassword 'pwd123456' 45 | } 46 | } 47 | 48 | defaultConfig { 49 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 50 | applicationId "xwh.flutter.music" 51 | minSdkVersion 16 52 | targetSdkVersion 30 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 56 | 57 | // 选择打包时选取的so文件,x86时模拟上的环境。尽量只选需要的,不然安装包会太大 58 | ndk { 59 | abiFilters "arm64-v8a" ,"armeabi-v7a"//,"x86_64","x86" 60 | } 61 | } 62 | 63 | buildTypes { 64 | release { 65 | // TODO: Add your own signing config for the release build. 66 | // Signing with the debug keys for now, so `flutter run --release` works. 67 | signingConfig signingConfigs.config 68 | } 69 | debug { 70 | signingConfig signingConfigs.config 71 | } 72 | } 73 | 74 | // 插件和app都依赖了libflutter.so文件,这里指定一下 75 | // 如果编译失败,修改这里!!!! 76 | packagingOptions { 77 | pickFirst 'lib/x86_64/libflutter.so' 78 | pickFirst 'lib/x86/libflutter.so' 79 | pickFirst 'lib/arm64-v8a/libflutter.so' 80 | pickFirst 'lib/armeabi-v7a/libflutter.so' 81 | pickFirst 'lib/arm64-v8a/libapp.so' 82 | exclude 'lib/armeabi-v7a/libapp.so' 83 | exclude 'lib/arm64-v8a/libapp.so' 84 | exclude 'lib/x86_64/libapp.so' 85 | } 86 | } 87 | 88 | flutter { 89 | source '../..' 90 | } 91 | 92 | dependencies { 93 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 94 | testImplementation 'junit:junit:4.12' 95 | androidTestImplementation 'androidx.test:runner:1.1.0' 96 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 97 | 98 | implementation project(':speech') 99 | } 100 | -------------------------------------------------------------------------------- /lib/widget/search_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/color_provider.dart'; 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class SearchBar extends StatefulWidget { 7 | final bool enable; 8 | final bool autofocus; 9 | final String text; 10 | final ValueChanged onChanged; 11 | final Function onSpeechPressed; 12 | final TextEditingController controller; 13 | SearchBar( 14 | {Key key, 15 | this.enable = true, 16 | this.autofocus = false, 17 | this.text, 18 | this.onChanged, 19 | this.onSpeechPressed, 20 | this.controller}) 21 | : super(key: key); 22 | 23 | @override 24 | _SearchBarState createState() => _SearchBarState(); 25 | } 26 | 27 | class _SearchBarState extends State { 28 | bool showClear = false; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | 38 | Color mainColor = 39 | Provider.of(context).getCurrentColor(); 40 | return Container( 41 | height: 36.0, 42 | padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), 43 | decoration: BoxDecoration( 44 | color: Colors.white.withAlpha(220), 45 | borderRadius: BorderRadius.circular(18.0), 46 | ), 47 | child: Row( 48 | children: [ 49 | Icon(Icons.search, size: 22.0, color: mainColor), 50 | Expanded( 51 | child: TextField( 52 | controller: widget.controller, 53 | onChanged: _onChanged, 54 | maxLines: 1, 55 | enabled: widget.enable, 56 | autofocus: widget.autofocus, 57 | textAlignVertical: TextAlignVertical.bottom, 58 | style: TextStyle( 59 | fontSize: ScreenUtil().setSp(16), 60 | color: Colors.black87, 61 | fontWeight: FontWeight.w300), 62 | //输入文本的样式 63 | decoration: InputDecoration( 64 | contentPadding: 65 | EdgeInsets.only(left: 6.0, bottom: 24.0-ScreenUtil().setHeight(12)), // 可控制文字在框中的位置 66 | border: InputBorder.none, 67 | hintText: '请输入你想听的', 68 | hintStyle: TextStyle(fontSize: ScreenUtil().setSp(16)), 69 | )), 70 | ), 71 | InkWell( 72 | child: Icon(showClear ? Icons.clear : Icons.mic, 73 | size: 22.0, color: mainColor), 74 | onTap: () { 75 | if (showClear) { 76 | widget.controller?.clear(); 77 | setState(() { 78 | showClear = false; 79 | }); 80 | } else { 81 | // 语音识别按钮 82 | widget.onSpeechPressed(); 83 | } 84 | }), 85 | ], 86 | ), 87 | ); 88 | } 89 | 90 | _onChanged(String text) { 91 | if (text.length > 0) { 92 | setState(() { 93 | showClear = true; 94 | }); 95 | } else { 96 | setState(() { 97 | showClear = false; 98 | }); 99 | } 100 | 101 | if (widget.onChanged != null) { 102 | widget.onChanged(text); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audioplayer (0.0.1): 3 | - Flutter 4 | - connectivity (0.0.1): 5 | - Flutter 6 | - Reachability 7 | - Flutter (1.0.0) 8 | - fluttertoast (0.0.2): 9 | - Flutter 10 | - FMDB (2.7.5): 11 | - FMDB/standard (= 2.7.5) 12 | - FMDB/standard (2.7.5) 13 | - orientation (0.0.1): 14 | - Flutter 15 | - path_provider (0.0.1): 16 | - Flutter 17 | - Reachability (3.2) 18 | - shared_preferences (0.0.1): 19 | - Flutter 20 | - shared_preferences_macos (0.0.1): 21 | - Flutter 22 | - shared_preferences_web (0.0.1): 23 | - Flutter 24 | - sqflite (0.0.1): 25 | - Flutter 26 | - FMDB (~> 2.7.2) 27 | - video_player (0.0.1): 28 | - Flutter 29 | - video_player_web (0.0.1): 30 | - Flutter 31 | 32 | DEPENDENCIES: 33 | - audioplayer (from `.symlinks/plugins/audioplayer/ios`) 34 | - connectivity (from `.symlinks/plugins/connectivity/ios`) 35 | - Flutter (from `.symlinks/flutter/ios`) 36 | - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) 37 | - orientation (from `.symlinks/plugins/orientation/ios`) 38 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 39 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 40 | - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) 41 | - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) 42 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 43 | - video_player (from `.symlinks/plugins/video_player/ios`) 44 | - video_player_web (from `.symlinks/plugins/video_player_web/ios`) 45 | 46 | SPEC REPOS: 47 | trunk: 48 | - FMDB 49 | - Reachability 50 | 51 | EXTERNAL SOURCES: 52 | audioplayer: 53 | :path: ".symlinks/plugins/audioplayer/ios" 54 | connectivity: 55 | :path: ".symlinks/plugins/connectivity/ios" 56 | Flutter: 57 | :path: ".symlinks/flutter/ios" 58 | fluttertoast: 59 | :path: ".symlinks/plugins/fluttertoast/ios" 60 | orientation: 61 | :path: ".symlinks/plugins/orientation/ios" 62 | path_provider: 63 | :path: ".symlinks/plugins/path_provider/ios" 64 | shared_preferences: 65 | :path: ".symlinks/plugins/shared_preferences/ios" 66 | shared_preferences_macos: 67 | :path: ".symlinks/plugins/shared_preferences_macos/ios" 68 | shared_preferences_web: 69 | :path: ".symlinks/plugins/shared_preferences_web/ios" 70 | sqflite: 71 | :path: ".symlinks/plugins/sqflite/ios" 72 | video_player: 73 | :path: ".symlinks/plugins/video_player/ios" 74 | video_player_web: 75 | :path: ".symlinks/plugins/video_player_web/ios" 76 | 77 | SPEC CHECKSUMS: 78 | audioplayer: f4462b84216b9c55f02bbbdc7ab60eec7427b2d4 79 | connectivity: 6e94255659cc86dcbef1d452ad3e0491bb1b3e75 80 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 81 | fluttertoast: b644586ef3b16f67fae9a1f8754cef6b2d6b634b 82 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 83 | orientation: 46fe7528896a5c3e6215366bcb462c182ebfd4e9 84 | path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d 85 | Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 86 | shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 87 | shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 88 | shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 89 | sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 90 | video_player: 69c5f029fac4ffe4fc8a85ea7f7b793709661549 91 | video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7 92 | 93 | PODFILE CHECKSUM: 10ae9c18d12c9ffc2275c9a159a3b1e281990db0 94 | 95 | COCOAPODS: 1.9.0 96 | -------------------------------------------------------------------------------- /audioplayer/README.md: -------------------------------------------------------------------------------- 1 | # AudioPlayer 2 | 3 | A Flutter audio plugin (Swift/Java) to play remote or local audio files on iOS / Android / MacOS / Web. 4 | 5 | [Online demo](https://rxlabz.github.io/audioplayer/) 6 | 7 | ## Features 8 | 9 | - [x] Android / iOS / MacOS / Web 10 | - [x] play remote file 11 | - [x] play local file ( not for the web) 12 | - [x] stop 13 | - [x] pause 14 | - [x] onComplete 15 | - [x] onDuration / onCurrentPosition 16 | - [x] seek 17 | - [x] mute 18 | 19 | ![screenshot](https://rxlabz.github.io/audioplayer/audioplayer.png) 20 | 21 | ## Usage 22 | 23 | [Example](https://github.com/rxlabz/audioplayer/blob/master/example/lib/main.dart) 24 | 25 | To use this plugin : 26 | 27 | - Add the dependency to your [pubspec.yaml](https://github.com/rxlabz/audioplayer/blob/master/example/pubspec.yaml) file. 28 | 29 | ```yaml 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | audioplayer: 0.8.1 34 | audioplayer_web: 0.7.1 35 | ``` 36 | 37 | - Instantiate an AudioPlayer instance 38 | 39 | ```dart 40 | //... 41 | AudioPlayer audioPlugin = AudioPlayer(); 42 | //... 43 | ``` 44 | 45 | ### Player Controls 46 | 47 | ```dart 48 | audioPlayer.play(url); 49 | 50 | audioPlayer.pause(); 51 | 52 | audioPlayer.stop(); 53 | ``` 54 | 55 | ### Status and current position 56 | 57 | The dart part of the plugin listen for platform calls : 58 | 59 | ```dart 60 | //... 61 | _positionSubscription = audioPlayer.onAudioPositionChanged.listen( 62 | (p) => setState(() => position = p) 63 | ); 64 | 65 | _audioPlayerStateSubscription = audioPlayer.onPlayerStateChanged.listen((s) { 66 | if (s == AudioPlayerState.PLAYING) { 67 | setState(() => duration = audioPlayer.duration); 68 | } else if (s == AudioPlayerState.STOPPED) { 69 | onComplete(); 70 | setState(() { 71 | position = duration; 72 | }); 73 | } 74 | }, onError: (msg) { 75 | setState(() { 76 | playerState = PlayerState.stopped; 77 | duration = new Duration(seconds: 0); 78 | position = new Duration(seconds: 0); 79 | }); 80 | }); 81 | ``` 82 | 83 | Do not forget to cancel all the subscriptions when the widget is disposed. 84 | 85 | 86 | ## iOS 87 | 88 | ### :warning: iOS App Transport Security 89 | 90 | By default iOS forbids loading from non-https url. To cancel this restriction edit your .plist and add : 91 | 92 | ```xml 93 | NSAppTransportSecurity 94 | 95 | NSAllowsArbitraryLoads 96 | 97 | 98 | ``` 99 | 100 | ### Background mode 101 | 102 | cf. [enable background audio](https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/creating_a_basic_video_player_ios_and_tvos/enabling_background_audio) 103 | 104 | ## MacOS 105 | 106 | Add this to entitlements files ( cf. [DebugProfile.entitlements](example/macos/Runner/DebugProfile.entitlements) ) 107 | ```xml 108 | com.apple.security.network.client 109 | 110 | ``` 111 | 112 | cf. [Flutter MacOS security](https://github.com/google/flutter-desktop-embedding/blob/master/macOS-Security.md) 113 | 114 | ## Troubleshooting 115 | 116 | - If you get a MissingPluginException, try to `flutter build apk` on Android, or `flutter build ios` 117 | 118 | ## Getting Started with Flutter 119 | 120 | For help getting started with Flutter, view our online 121 | [documentation](http://flutter.io/). 122 | 123 | For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). 124 | -------------------------------------------------------------------------------- /lib/test/video_demo.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:video_player/video_player.dart'; 9 | 10 | class VideoDemo extends StatefulWidget { 11 | String url; 12 | VideoDemo({Key key, this.url}) : super(key: key); 13 | 14 | @override 15 | _VideoDemoState createState() => _VideoDemoState(); 16 | } 17 | 18 | class _VideoDemoState extends State 19 | with SingleTickerProviderStateMixin { 20 | VideoPlayerController controller; 21 | bool isDisposed = false; 22 | 23 | Future initController( 24 | VideoPlayerController controller, String name) async { 25 | print( 26 | '> VideoDemo initController "${widget.url}" ${isDisposed ? "DISPOSED" : ""}'); 27 | //controller.setLooping(true); 28 | //controller.setVolume(0.0); 29 | controller.play(); 30 | await controller.initialize(); 31 | if (mounted) { 32 | print( 33 | '< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}'); 34 | setState(() {}); 35 | } else { 36 | print('not mounted!!'); 37 | } 38 | } 39 | 40 | @override 41 | void initState() { 42 | super.initState(); 43 | 44 | controller = VideoPlayerController.network(widget.url); 45 | 46 | initController(controller, 'bee'); 47 | } 48 | 49 | @override 50 | void dispose() { 51 | print('> VideoDemo dispose'); 52 | isDisposed = true; 53 | controller.dispose(); 54 | print('< VideoDemo dispose'); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Scaffold( 61 | appBar: AppBar( 62 | title: const Text('Videos'), 63 | ), 64 | body: GestureDetector( 65 | onTap: () => pushFullScreenWidget(context), 66 | child: _buildInlineVideo(), 67 | ), 68 | ); 69 | } 70 | 71 | Widget _buildInlineVideo() { 72 | return Padding( 73 | padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0), 74 | child: Center( 75 | child: AspectRatio( 76 | aspectRatio: 16 / 9, 77 | child: Hero( 78 | tag: controller, 79 | child: VideoPlayer(controller), 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | 86 | Widget _buildFullScreenVideo() { 87 | return Center( 88 | child: AspectRatio( 89 | aspectRatio: controller.value.aspectRatio, 90 | child: Hero( 91 | tag: controller, 92 | child: VideoPlayer(controller), 93 | ), 94 | )); 95 | } 96 | 97 | void pushFullScreenWidget(context) { 98 | SystemChrome.setPreferredOrientations( 99 | [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); 100 | 101 | final TransitionRoute route = PageRouteBuilder( 102 | settings: RouteSettings(name: "Test"), 103 | pageBuilder: (context, animation, secondaryAnimation) => 104 | _buildFullScreenVideo()); 105 | 106 | route.completed.then((void value) { 107 | //controller.setVolume(0.0); 108 | SystemChrome.setPreferredOrientations( 109 | [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); 110 | }); 111 | 112 | //controller.setVolume(1.0); 113 | Navigator.of(context).push(route); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/model/Lyric.dart: -------------------------------------------------------------------------------- 1 | class Lyric { 2 | String lyric; 3 | List items = []; 4 | 5 | Lyric(this.lyric) { 6 | build(); 7 | } 8 | 9 | Lyric.empty(); // 空的构造函数 10 | 11 | Lyric.test() { // test数据的构造函数 12 | items = List.generate( 13 | 50, (index) => LyricItem(index, index * 1000, index.toString() * 10)); 14 | } 15 | 16 | String getItemsString() { 17 | StringBuffer stringBuffer = new StringBuffer(); 18 | items.forEach((LyricItem item) { 19 | stringBuffer.writeln(item.content); 20 | }); 21 | 22 | return stringBuffer.toString(); 23 | } 24 | 25 | void build() { 26 | if (lyric == null || lyric.length == 0) { 27 | return; 28 | } 29 | 30 | List lines = lyric.split('\n'); 31 | int index = 0; 32 | lines.forEach((line) { 33 | List strs = line.split(']'); 34 | if (strs.length >= 2) { // 可能一行多句歌词的情况 35 | String content = strs[strs.length -1]; 36 | 37 | for(int i=0; i=0) { // 如果时间戳不正确就丢掉 41 | this.items.add(new LyricItem(index, position, content)); 42 | index++; 43 | } else { 44 | //print('Lyric: 不解析的歌词:$line'); 45 | } 46 | } 47 | 48 | } 49 | }); 50 | 51 | _sortPosition(); 52 | 53 | _initDuraton(); 54 | 55 | } 56 | 57 | /// 对position排序,有些SB歌词时间戳居然不是有序的。 58 | /// 或者一行多句歌词的情况 59 | _sortPosition(){ 60 | bool isSorted = true; // 大部分是有序的 61 | for(int i=0; i left.position - right.position); 70 | 71 | for(int i=0; i strs = str.split(':'); 82 | if (strs.length == 2) { 83 | int minute = int.parse(strs[0]); 84 | position += minute * 60 * 1000; 85 | 86 | List secondStrs = strs[1].split('.'); 87 | if (secondStrs.length == 2) { 88 | int millsecond = 89 | int.parse(secondStrs[0]) * 1000 + int.parse(secondStrs[1]); 90 | position += millsecond; 91 | } 92 | } 93 | } catch (e) { 94 | position = -1; 95 | //print(str + e.toString()); 96 | } 97 | 98 | return position; 99 | } 100 | 101 | // 计算每段歌词的显示时间 102 | void _initDuraton() { 103 | for(int i=0; i 1) { 111 | LyricItem preItem = items[items.length -2]; 112 | LyricItem lastItem = items[items.length -1]; 113 | int duration = preItem.duration; 114 | if (preItem.content.length > 0) { 115 | duration = duration * lastItem.content.length ~/ preItem.content.length; 116 | } 117 | lastItem.duration = duration; 118 | } 119 | 120 | } 121 | } 122 | 123 | class LyricItem { 124 | int index; 125 | int position; 126 | String content; 127 | int duration; // 歌词显示的时间长度 128 | 129 | LyricItem(this.index, this.position, this.content); 130 | 131 | @override 132 | String toString() { 133 | return '$index $position $content $duration'; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/widget/wave_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WaveWidget extends StatefulWidget { 4 | final bool isRunning; 5 | WaveWidget({Key key, this.isRunning:false}) : super(key: key); 6 | 7 | @override 8 | _WaveWidgetState createState() => _WaveWidgetState(); 9 | } 10 | 11 | class _WaveWidgetState extends State 12 | with SingleTickerProviderStateMixin { 13 | Animation _doubleAnimation; 14 | AnimationController _controller; 15 | Function _listener; 16 | bool isRunning = false; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | _controller = 22 | AnimationController(vsync: this, duration: Duration(milliseconds: 600)); 23 | _doubleAnimation = Tween(begin: mInitRadius, end: mInitRadius + mItemMargin).animate(_controller); 24 | _listener = () { 25 | //mFirstRadius = _controller.value; // 不要在controller取值,永远为0~1 !!!! 26 | 27 | /// 如果动画是repeat执行,不会触发AnimationStatus 28 | /// 这里用值了判断,如果比新的值大,说明是下一次开始 29 | if (mFirstRadius > _doubleAnimation.value) { 30 | if (mCircleCount < mMaxCount) { 31 | mCircleCount++; 32 | } 33 | //print('mCircleCount: $mCircleCount'); 34 | } 35 | mFirstRadius = _doubleAnimation.value; 36 | if (mounted) { 37 | this.setState(() {}); 38 | } 39 | 40 | }; 41 | 42 | 43 | _doubleAnimation.addListener(_listener); 44 | 45 | } 46 | 47 | void startAnim() { 48 | mCircleCount = 0; 49 | mFirstRadius = mInitRadius; 50 | _controller.forward(from: 0.0); 51 | _controller.repeat(); // 循环动画,中途不会触发AnimationStatus变化。 52 | } 53 | 54 | void stopAnim() { 55 | _controller.stop(); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | /// 有个重要的注意 61 | /// 动画取消监听要放在super.dispose之前,不然就无效。 62 | _controller.stop(); 63 | _controller.dispose(); 64 | _doubleAnimation.removeListener(_listener); 65 | super.dispose(); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | if (widget.isRunning != this.isRunning) { 71 | this.isRunning = widget.isRunning; 72 | isRunning ? startAnim() : stopAnim(); 73 | } 74 | return CustomPaint( 75 | size: Size(80, 80),// 画布宽高,发现其实绘制范围可以超出 76 | painter: _WavePainter(isRunning:isRunning), 77 | ); 78 | } 79 | } 80 | 81 | Paint mPaint; 82 | final double mInitRadius = 30; // 第一个初始圆圈的半径 83 | final double mItemMargin = 20; // 每圈之间的间隔 84 | double mFirstRadius; // 第一个圆圈半径,其他扩散的依次增加。 85 | int mCircleCount = 0; // 当前圈的个数 86 | int mMaxCount; 87 | final double maxRadius = 120.0; 88 | 89 | class _WavePainter extends CustomPainter { 90 | int mColor = 0x888888; 91 | bool isRunning = false; 92 | 93 | _WavePainter({this.isRunning}); 94 | 95 | void _init() { 96 | print('_WavePainter init'); 97 | mPaint = Paint() 98 | ..isAntiAlias = true 99 | ..style = PaintingStyle.fill //填充 100 | ..color = Color(mColor); 101 | mMaxCount = (maxRadius - mInitRadius) ~/ mItemMargin; 102 | } 103 | 104 | @override 105 | void paint(Canvas canvas, Size size) { 106 | if (mPaint == null) { 107 | _init(); 108 | } 109 | 110 | if (!isRunning) { 111 | return; 112 | } 113 | 114 | //print('_WavePainter paint: $mFirstRadius , mCircleCount: $mCircleCount '); 115 | 116 | // 绘制扩散圆 117 | double radius = mFirstRadius; 118 | int i=0; 119 | while (i<=mCircleCount && radius <= maxRadius) { 120 | // 设置透明度 121 | int alpha = (255 * (1.0 - radius / maxRadius)).toInt(); 122 | mPaint.color = Color(mColor).withAlpha(alpha); 123 | canvas.drawCircle( 124 | Offset(size.width / 2, size.height / 2), radius, mPaint); 125 | 126 | radius += mItemMargin; 127 | i++; 128 | } 129 | } 130 | 131 | @override 132 | bool shouldRepaint(CustomPainter oldDelegate) => true; 133 | } 134 | -------------------------------------------------------------------------------- /lib/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_music_player/model/music_controller.dart'; 3 | import 'package:flutter_music_player/pages/favorite_page.dart'; 4 | import 'package:flutter_music_player/utils/network_util.dart'; 5 | import 'package:flutter_music_player/utils/shared_preference_util.dart'; 6 | import 'package:flutter_music_player/utils/toast_util.dart'; 7 | import 'package:flutter_music_player/widget/floating_player.dart'; 8 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 9 | import 'package:flutter_music_player/utils/screen_size.dart'; 10 | import 'package:provider/provider.dart'; 11 | import './tabs_bottom.dart'; 12 | import './play_list_page.dart'; 13 | import './recommend_page.dart'; 14 | import 'mv_page.dart'; 15 | 16 | class HomePage extends StatefulWidget { 17 | HomePage({Key key}) : super(key: key); 18 | 19 | _HomePageState createState() => _HomePageState(); 20 | } 21 | 22 | class _HomePageState extends State { 23 | int _currentIndex = 0; 24 | List pages = []; 25 | final NetworkUtil networkUtil = NetworkUtil(); 26 | DateTime lastBackTime; 27 | bool showFloatPlayer = true; 28 | 29 | final PageController _controller = PageController( 30 | initialPage: 0, 31 | ); 32 | 33 | void _tapCallback(int index) { 34 | print("HomePage: on page selected: $index"); 35 | _controller.jumpToPage(index); 36 | setState(() { 37 | _currentIndex = index; 38 | }); 39 | } 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | 45 | pages 46 | ..add(RecommendPage(tapCallback: this._tapCallback)) 47 | ..add(PlayListPage()) 48 | ..add(MVPage()) 49 | ..add(FavoritePage()); 50 | 51 | networkUtil.initNetworkListener(); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | super.dispose(); 57 | networkUtil.dispose(); 58 | print('HomePage dispose'); 59 | } 60 | 61 | @override 62 | void deactivate() { 63 | super.deactivate(); 64 | print('HomePage deactive'); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | print('HomePage build'); 70 | 71 | if (ScreenSize.width == null) { 72 | // 获取屏幕大小,用于界面适配(之前手动弄的,推荐下面的插件) 73 | ScreenSize.getScreenSize(context); 74 | // 屏幕适配,原理:设置设计稿尺寸,然后和设备的实际尺寸进行比较,进行缩放 75 | ScreenUtil.init( 76 | BoxConstraints( 77 | maxWidth: MediaQuery.of(context).size.width, 78 | maxHeight: MediaQuery.of(context).size.height), 79 | designSize: Size(360, 640), 80 | orientation: Orientation.portrait); 81 | } 82 | 83 | showFloatPlayer = 84 | SharedPreferenceUtil.getInstance().getBool('showFloatPlayer') ?? true; 85 | print('showFloatPlayer: $showFloatPlayer'); 86 | 87 | return WillPopScope( 88 | onWillPop: () => _beforePop(context), 89 | child: Scaffold( 90 | body: PageView( 91 | controller: _controller, 92 | children: pages, 93 | physics: NeverScrollableScrollPhysics(), // 设置为不能滚动 94 | ), 95 | bottomNavigationBar: BottomTabs( 96 | this._currentIndex, this._tapCallback, showFloatPlayer), 97 | floatingActionButton: showFloatPlayer ? FloatingPlayer() : null, 98 | floatingActionButtonLocation: 99 | FloatingActionButtonLocation.centerDocked, 100 | )); 101 | } 102 | 103 | Future _beforePop(BuildContext context) async { 104 | if (this._currentIndex != 0) { 105 | _tapCallback(0); 106 | return false; 107 | } 108 | 109 | if (lastBackTime == null || 110 | DateTime.now().difference(lastBackTime) > Duration(seconds: 2)) { 111 | ToastUtil.showToast(context, "再按一次退出"); 112 | lastBackTime = DateTime.now(); 113 | return false; // 不返回 114 | } 115 | 116 | Provider.of(context, listen: false).dispose(); 117 | return true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/pages/artist_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/dao/music_163.dart'; 4 | import 'package:flutter_music_player/utils/screen_size.dart'; 5 | import 'package:flutter_music_player/widget/song_item_tile.dart'; 6 | 7 | /// 歌单详情页 8 | /// 9 | class ArtistDetailPage extends StatefulWidget { 10 | final int id; 11 | ArtistDetailPage(this.id); 12 | 13 | _ArtistDetailPageState createState() => _ArtistDetailPageState(); 14 | } 15 | 16 | class _ArtistDetailPageState extends State { 17 | double _appBarHeight; 18 | Map _artist; 19 | ScrollController _controller; 20 | 21 | @override 22 | void initState() { 23 | // appBar和图片宽高比相同 24 | _appBarHeight = ScreenSize.width * 4 / 6; 25 | _controller = ScrollController(); 26 | 27 | _getAritistDetailSongs(); 28 | super.initState(); 29 | } 30 | 31 | @override 32 | void dispose() { 33 | super.dispose(); 34 | _controller.dispose(); 35 | } 36 | 37 | _getAritistDetailSongs() async { 38 | MusicDao.getArtistDetail(widget.id).then((re) { 39 | if (mounted) { 40 | setState(() { 41 | _artist = re; 42 | }); 43 | } 44 | }); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return WillPopScope( 50 | onWillPop: _beforePop, 51 | child: Scaffold( 52 | body: CustomScrollView( 53 | slivers: _buildSlivers(), 54 | controller: _controller, 55 | ), 56 | ), 57 | ); 58 | } 59 | 60 | List _buildSlivers() { 61 | List slivers = []; 62 | slivers.add(_buildAppBar()); 63 | if (_artist != null) { 64 | slivers.add(_buildDesc()); 65 | slivers.add(_buildList()); 66 | } 67 | return slivers; 68 | } 69 | 70 | Widget _buildAppBar() { 71 | return SliverAppBar( 72 | expandedHeight: _appBarHeight, 73 | pinned: true, 74 | floating: false, 75 | snap: false, 76 | flexibleSpace: FlexibleSpaceBar( 77 | title: Text( 78 | "${_artist == null ? '' : _artist['name']}", 79 | maxLines: 1, 80 | overflow: TextOverflow.ellipsis, 81 | style: TextStyle(fontSize: 16.0, color: Colors.white), 82 | ), 83 | centerTitle: false, 84 | titlePadding: EdgeInsetsDirectional.only(start: 46.0, bottom: 16.0), 85 | background: Stack( 86 | fit: StackFit.expand, 87 | children: [ 88 | _artist == null 89 | ? Image.asset( 90 | 'images/placeholder_play_list.jpg', 91 | fit: BoxFit.cover, 92 | ) 93 | : CachedNetworkImage( 94 | imageUrl: 95 | "${_artist == null ? '' : _artist['image']}?param=600y400", 96 | fit: BoxFit.cover, 97 | placeholder: (context, url) => Image.asset( 98 | 'images/placeholder_play_list.jpg', 99 | fit: BoxFit.cover, 100 | ), 101 | height: _appBarHeight), 102 | // This gradient ensures that the toolbar icons are distinct 103 | // against the background image. 104 | const DecoratedBox( 105 | decoration: BoxDecoration( 106 | gradient: LinearGradient( 107 | begin: Alignment(0.0, -1.0), 108 | end: Alignment(0.0, -0.4), 109 | colors: [Color(0x60000000), Color(0x00000000)], 110 | ), 111 | ), 112 | ), 113 | ], 114 | ), 115 | ), 116 | ); 117 | } 118 | 119 | Widget _buildDesc() { 120 | return SliverToBoxAdapter( 121 | child: Container( 122 | padding: EdgeInsets.all(16.0), 123 | child:Text(_artist['desc'], 124 | style: TextStyle(color: Colors.black87, fontSize: 13.0, height: 1.2)))); 125 | } 126 | 127 | Widget _buildList() { 128 | return SliverList( 129 | delegate: SliverChildBuilderDelegate( 130 | (BuildContext context, int index) { 131 | return SongItemTile(_artist['songs'], index); 132 | }, 133 | childCount: _artist['songs'].length, 134 | ), 135 | ); 136 | } 137 | 138 | Future _beforePop() async { 139 | // 在页面退出的时候回到顶部,不然Hero动画之前的图片会空白。 140 | _controller.jumpTo(0); 141 | return true; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/widget/floating_player.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_music_player/model/music_controller.dart'; 4 | import 'package:flutter_music_player/model/song_util.dart'; 5 | import 'package:flutter_music_player/pages/player_page.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | class FloatingPlayer extends StatefulWidget { 9 | FloatingPlayer({Key key}) : super(key: key); 10 | 11 | @override 12 | _FloatingPlayerState createState() => _FloatingPlayerState(); 13 | } 14 | 15 | class _FloatingPlayerState extends State 16 | with SingleTickerProviderStateMixin { 17 | AnimationController _animController; 18 | MusicController musicController; 19 | MusicListener musicListener; 20 | PlayerState playerState = PlayerState.playing; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | print('FloatingPlayer initState'); 26 | 27 | _animController = 28 | AnimationController(duration: const Duration(seconds: 16), vsync: this); 29 | } 30 | 31 | @override 32 | void didChangeDependencies() { 33 | super.didChangeDependencies(); 34 | 35 | print('FloatingPlayer didChangeDependencies'); 36 | 37 | /// listen: true,当Provider中notifyListeners时,自动触发更新。 38 | /// 默认为true,所以在不需要自动触发更新的地方要设为false。 39 | initMusicListener(); 40 | } 41 | 42 | void initMusicListener() { 43 | if (musicListener == null) { 44 | musicListener = MusicListener( 45 | getName: () => "FloatingPlayer", 46 | onLoading: () {}, 47 | onStart: (duration) {}, 48 | onPosition: (position) {}, 49 | onStateChanged: (state) { 50 | setState(() => this.playerState = state); 51 | }, 52 | onError: (msg) => {}); 53 | } 54 | 55 | musicController = Provider.of(context, listen: true); 56 | musicController.addMusicListener(musicListener); 57 | } 58 | 59 | @override 60 | void dispose() { 61 | /// 这儿有个问题,要在后面调用super.dispose 62 | /// at the time dispose() was called on the mixin, 63 | /// that Ticker was still active. 64 | /// The Ticker must be disposed before calling super.dispose(). 65 | print('FloatingPlayer dispose'); 66 | _animController.dispose(); 67 | musicController.removeMusicListener(musicListener); 68 | super.dispose(); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | print('FloatingPlayer build'); 74 | 75 | Map song = musicController.getCurrentSong(); 76 | 77 | _buildAnim(); 78 | 79 | return GestureDetector( 80 | // 加了双击事件之后,单击事件就变迟缓了。 81 | /* onDoubleTap: (){ 82 | print('onDoubleTap'); 83 | musicController.toggle(); 84 | }, */ 85 | onLongPress: () { 86 | print('onLongPress'); 87 | }, 88 | child: Container( 89 | width: 70.0, 90 | height: 70.0, 91 | child: FloatingActionButton( 92 | onPressed: () { 93 | if (song != null) { 94 | PlayerPage.gotoPlayer(context); 95 | } 96 | }, 97 | elevation: 2.0, 98 | backgroundColor: Colors.black26, 99 | heroTag: 'FloatingPlayer', 100 | child: Container( 101 | padding: EdgeInsets.all(2.0), 102 | child: RotationTransition( 103 | //设置动画的旋转中心 104 | alignment: Alignment.center, 105 | //动画控制器 106 | turns: _animController, 107 | child: ClipOval( 108 | child: song == null 109 | ? Image.asset('images/music_2.jpg', 110 | fit: BoxFit.cover) 111 | : CachedNetworkImage( 112 | imageUrl: SongUtil.getSongImage(song), 113 | fit: BoxFit.cover), 114 | ))), 115 | ))); 116 | } 117 | 118 | void _buildAnim() { 119 | playerState = musicController.getCurrentState(); 120 | if (playerState == PlayerState.playing) { 121 | if (!_animController.isAnimating) { 122 | print('开始动画'); 123 | _animController.forward(); 124 | _animController.repeat(); 125 | } 126 | } else { 127 | if (_animController.isAnimating) { 128 | print('结束动画'); 129 | _animController.stop(); 130 | } 131 | } 132 | } 133 | } 134 | --------------------------------------------------------------------------------