├── format.sh ├── android ├── settings.gradle ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ └── values │ │ │ └── styles.xml │ │ └── kotlin │ │ └── fl │ │ └── pip │ │ ├── FlPiPActivity.kt │ │ ├── FlPiPPlugin.kt │ │ └── PiPHelper.kt └── build.gradle ├── analysis_options.yaml ├── assets ├── audio.mp3 └── landscape.mp4 ├── example ├── 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 │ │ ├── SceneDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile.lock │ ├── .gitignore │ ├── Podfile │ └── Runner.xcodeproj │ │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj ├── assets │ ├── close.png │ ├── logo.png │ ├── android.mp4 │ └── landscape.mp4 ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── fl │ │ │ │ │ │ └── pip │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle.kts │ └── settings.gradle.kts ├── pubspec.yaml ├── README.md ├── .gitignore ├── analysis_options.yaml └── lib │ ├── main.dart │ └── src │ ├── pip_home_page.dart │ └── home_page.dart ├── ios ├── Classes │ ├── FlPiPPlugin.swift │ ├── FlFlutterAppDelegate.swift │ ├── BackgroundAudioPlayer.swift │ └── PiPHelper.swift ├── Resources │ └── PrivacyInfo.xcprivacy ├── .gitignore └── fl_pip.podspec ├── pubspec.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md └── lib └── fl_pip.dart /format.sh: -------------------------------------------------------------------------------- 1 | dart format lib 2 | dart format example/lib -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'fl_pip' 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml -------------------------------------------------------------------------------- /assets/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/assets/audio.mp3 -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/landscape.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/assets/landscape.mp4 -------------------------------------------------------------------------------- /example/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/assets/close.png -------------------------------------------------------------------------------- /example/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/assets/logo.png -------------------------------------------------------------------------------- /example/assets/android.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/assets/android.mp4 -------------------------------------------------------------------------------- /example/assets/landscape.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/assets/landscape.mp4 -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/fl/pip/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package fl.pip.example 2 | 3 | import fl.pip.FlPiPActivity 4 | 5 | class MainActivity : FlPiPActivity() 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wayaer/fl_pip/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Classes/FlPiPPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | 3 | public class FlPiPPlugin: NSObject, FlutterPlugin { 4 | public static func register(with registrar: FlutterPluginRegistrar) { 5 | PiPHelper.shared.setRegistrar(registrar) 6 | PiPHelper.shared.newFlutterMethodChannel(registrar.messenger()) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /.kotlin 7 | /local.properties 8 | GeneratedPluginRegistrant.java 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /example/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. -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTrackingDomains 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Demonstrates how to use the fl_pip plugin. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.6.0 <4.0.0' 8 | flutter: '>=3.27.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | fl_pip: 14 | path: ../ 15 | flutter_waya: ^10.0.1 16 | 17 | dev_dependencies: 18 | flutter_lints: ^5.0.0 19 | 20 | flutter: 21 | uses-material-design: true 22 | assets: 23 | - assets/ -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Classes/FlFlutterAppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | 3 | open class FlFlutterAppDelegate: FlutterAppDelegate { 4 | open func registerPlugin(_ registry: FlutterPluginRegistry) {} 5 | 6 | override open func applicationWillEnterForeground(_ application: UIApplication) { 7 | PiPHelper.shared.applicationWillEnterForeground(application) 8 | } 9 | 10 | override open func applicationDidEnterBackground(_ application: UIApplication) { 11 | PiPHelper.shared.applicationDidEnterBackground(application) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - fl_pip (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | 6 | DEPENDENCIES: 7 | - fl_pip (from `.symlinks/plugins/fl_pip/ios`) 8 | - Flutter (from `Flutter`) 9 | 10 | EXTERNAL SOURCES: 11 | fl_pip: 12 | :path: ".symlinks/plugins/fl_pip/ios" 13 | Flutter: 14 | :path: Flutter 15 | 16 | SPEC CHECKSUMS: 17 | fl_pip: 8161c3d6fb04a73d698d7a80c1bf83cd046d738a 18 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 19 | 20 | PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb 21 | 22 | COCOAPODS: 1.16.2 23 | -------------------------------------------------------------------------------- /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/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fl_pip 2 | description: A picture-in-picture plugin for android and ios that can display any flutter view 3 | version: 3.2.1 4 | repository: https://github.com/Wayaer/fl_pip.git 5 | 6 | environment: 7 | sdk: '>=3.5.0 <4.0.0' 8 | flutter: ">=3.24.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_lints: ^5.0.0 16 | 17 | flutter: 18 | plugin: 19 | platforms: 20 | android: 21 | package: fl.pip 22 | pluginClass: FlPiPPlugin 23 | ios: 24 | pluginClass: FlPiPPlugin 25 | assets: 26 | - assets/ 27 | -------------------------------------------------------------------------------- /example/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # fl_pip_example 2 | 3 | Demonstrates how to use the fl_pip plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import fl_pip 2 | import Flutter 3 | import UIKit 4 | 5 | @main 6 | @objc class AppDelegate: FlFlutterAppDelegate { 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | GeneratedPluginRegistrant.register(with: self) 12 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 13 | } 14 | 15 | override func registerPlugin(_ registry: FlutterPluginRegistry) { 16 | GeneratedPluginRegistrant.register(with: registry) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Runner/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | //import UIKit 2 | // 3 | //class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | // 5 | // var window: UIWindow? 6 | // 7 | // 8 | // func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 9 | // guard let _ = (scene as? UIWindowScene) else { return } 10 | // } 11 | // 12 | // func sceneDidDisconnect(_ scene: UIScene) { 13 | // 14 | // } 15 | // 16 | // func sceneDidBecomeActive(_ scene: UIScene) { 17 | // print("sceneDidBecomeActive") 18 | // BackgroundTaskManager.shared.stopPlay() 19 | // } 20 | // 21 | // func sceneDidEnterBackground(_ scene: UIScene) { 22 | // print("sceneDidEnterBackground") 23 | // BackgroundTaskManager.shared.startPlay() 24 | // } 25 | // 26 | //} 27 | // 28 | -------------------------------------------------------------------------------- /example/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "2.0.20" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/fl_pip.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint fl_pip.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'fl_pip' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter project.' 9 | s.description = <<-DESC 10 | A new Flutter 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, '11.0' 19 | # Flutter.framework does not contain a i386 slice. 20 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 21 | s.swift_version = '5.0' 22 | end 23 | -------------------------------------------------------------------------------- /example/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("dev.flutter.flutter-gradle-plugin") 5 | } 6 | 7 | android { 8 | namespace = "fl.pip.example" 9 | compileSdk = flutter.compileSdkVersion 10 | 11 | compileOptions { 12 | sourceCompatibility = JavaVersion.VERSION_21 13 | targetCompatibility = JavaVersion.VERSION_21 14 | } 15 | 16 | kotlinOptions { 17 | jvmTarget = JavaVersion.VERSION_21.toString() 18 | } 19 | 20 | defaultConfig { 21 | applicationId = "fl.pip.example" 22 | minSdk = flutter.minSdkVersion 23 | targetSdk = flutter.targetSdkVersion 24 | versionCode = flutter.versionCode 25 | versionName = flutter.versionName 26 | } 27 | 28 | buildTypes { 29 | release { 30 | signingConfig = signingConfigs.getByName("debug") 31 | } 32 | } 33 | } 34 | 35 | flutter { 36 | source = "../.." 37 | } 38 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'fl.pip' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:8.5.2' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | apply plugin: 'kotlin-android' 24 | 25 | android { 26 | compileSdk 34 27 | 28 | if (project.android.hasProperty("namespace")) { 29 | namespace 'fl.pip' 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_17.toString() 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | minSdk = 16 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /android/src/main/kotlin/fl/pip/FlPiPActivity.kt: -------------------------------------------------------------------------------- 1 | package fl.pip 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import io.flutter.embedding.android.FlutterActivity 6 | 7 | open class FlPiPActivity : FlutterActivity() { 8 | private val pipHelper: PiPHelper = PiPHelper.getInstance() 9 | 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | pipHelper.setActivity(this, this.applicationContext) 14 | } 15 | 16 | 17 | override fun onPictureInPictureModeChanged( 18 | isInPictureInPictureMode: Boolean, newConfig: Configuration? 19 | ) { 20 | super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) 21 | pipHelper.onPictureInPictureModeChanged(isInPictureInPictureMode) 22 | } 23 | 24 | override fun onPause() { 25 | super.onPause() 26 | pipHelper.onActivityPaused() 27 | } 28 | 29 | override fun onResume() { 30 | super.onResume() 31 | pipHelper.onActivityResume() 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.2.1 2 | 3 | * Update gradle version 4 | * Optimize background music playback on iOS backend 5 | 6 | ## 3.1.0 7 | 8 | * Breaking changes , Please refer to the example 9 | * Fix the error when calling plug-in methods in Pip mode 10 | * Modify some configuration parameters of Android and iOS 11 | * Modified examples and some documents 12 | * When the app enters the background, the picture in picture still cannot work properly on iOS 13 | 14 | ## 2.0.0 15 | 16 | * Add the `PiPStatusInfo` class and add the `isCreateNewEngine` and `isEnabledWhenBackground` for 17 | the current pip 18 | * Fixed `disable()` not working in android when `createNewEngine=true` 19 | * Change the `isActive()` return parameter to `PiPStatusInfo` 20 | 21 | ## 1.0.0 22 | 23 | * Removed `FlPiP().enableWithEngine` 24 | * Add `createNewEngine`、`enabledWhenBackground` to `FlPiPConfig()` 25 | 26 | ## 0.1.1 27 | 28 | * Fixed ios gesture conflicts 29 | * Added the method for creating an engine 30 | * Added a system-level window for android 31 | 32 | ## 0.0.1 33 | 34 | * TODO: Describe initial release. 35 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Wayaer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/src/main/kotlin/fl/pip/FlPiPPlugin.kt: -------------------------------------------------------------------------------- 1 | package fl.pip 2 | 3 | import io.flutter.embedding.engine.plugins.FlutterPlugin 4 | import io.flutter.plugin.common.MethodCall 5 | import io.flutter.plugin.common.MethodChannel 6 | 7 | 8 | /** FlPiPPlugin */ 9 | class FlPiPPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { 10 | private lateinit var channel: MethodChannel 11 | private val pipHelper: PiPHelper = PiPHelper.getInstance() 12 | private lateinit var pluginBinding: FlutterPlugin.FlutterPluginBinding 13 | override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { 14 | pluginBinding = binding 15 | channel = MethodChannel(binding.binaryMessenger, "fl_pip") 16 | channel.setMethodCallHandler(this) 17 | pipHelper.channels.add(channel) 18 | } 19 | 20 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 21 | pipHelper.onMethodCall(call, result, pluginBinding) 22 | } 23 | 24 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 25 | channel.setMethodCallHandler(null) 26 | pipHelper.channels.remove(channel) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '15.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/src/home_page.dart'; 2 | import 'package:example/src/pip_home_page.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_waya/flutter_waya.dart'; 5 | 6 | void main() { 7 | runApp(App(home: HomePage())); 8 | } 9 | 10 | /// mainName must be the same as the method name 11 | @pragma('vm:entry-point') 12 | void pipMain() { 13 | runApp(ClipRRect( 14 | borderRadius: BorderRadius.circular(12), 15 | child: App(home: PiPHomePage()))); 16 | } 17 | 18 | class App extends StatelessWidget { 19 | const App({super.key, required this.home}); 20 | 21 | final Widget home; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return MaterialApp( 26 | debugShowCheckedModeBanner: false, 27 | theme: ThemeData.light(), 28 | darkTheme: ThemeData.dark(), 29 | home: home); 30 | } 31 | } 32 | 33 | class Timer extends StatelessWidget { 34 | const Timer({super.key}); 35 | 36 | @override 37 | Widget build(BuildContext context) => Counter.down( 38 | value: const Duration(seconds: 500), 39 | builder: (Duration duration, bool isRunning, VoidCallback startTiming, 40 | VoidCallback stopTiming) { 41 | return Padding( 42 | padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 12), 43 | child: Text('timer:${duration.inSeconds.toString()}')); 44 | }); 45 | } 46 | 47 | class Filled extends StatelessWidget { 48 | const Filled({super.key, required this.text, this.onPressed}); 49 | 50 | final String text; 51 | final VoidCallback? onPressed; 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return FilledButton( 56 | onPressed: onPressed, child: Text(text, textAlign: TextAlign.center)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/lib/src/pip_home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/main.dart'; 2 | import 'package:fl_pip/fl_pip.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_waya/flutter_waya.dart'; 5 | 6 | class PiPHomePage extends StatelessWidget { 7 | const PiPHomePage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) => Scaffold( 11 | backgroundColor: Colors.white70, 12 | body: Center( 13 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 14 | Timer(), 15 | const Text('The current pip is created using a new engine'), 16 | Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 17 | Filled(text: 'disable', onPressed: FlPiP().disable), 18 | Filled( 19 | text: 'PiPStatus isAvailable', 20 | onPressed: () async { 21 | final state = await FlPiP().isAvailable; 22 | if (context.mounted) { 23 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 24 | content: state 25 | ? const Text('PiP available') 26 | : const Text('PiP unavailable'))); 27 | } 28 | }), 29 | ]), 30 | Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 31 | Filled( 32 | text: 'foreground', 33 | onPressed: () { 34 | FlPiP().toggle(AppState.foreground); 35 | }), 36 | Filled( 37 | text: 'background', 38 | onPressed: () { 39 | FlPiP().toggle(AppState.background); 40 | }), 41 | ]), 42 | const SizedBox( 43 | height: 20, 44 | width: double.infinity, 45 | child: FlAnimationWave( 46 | value: 0.5, color: Colors.red, direction: Axis.vertical)), 47 | ]))); 48 | } 49 | -------------------------------------------------------------------------------- /example/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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Classes/BackgroundAudioPlayer.swift: -------------------------------------------------------------------------------- 1 | import AVFAudio 2 | import AVKit 3 | import Foundation 4 | 5 | class BackgroundAudioPlayer: NSObject { 6 | static let shared = BackgroundAudioPlayer() 7 | 8 | var audioPlayer: AVAudioPlayer? 9 | var audioSession = AVAudioSession.sharedInstance() 10 | var backgroundTaskIdentifier: UIBackgroundTaskIdentifier? 11 | 12 | func startPlay(_ path: String) -> Bool { 13 | do { 14 | stopPlay() 15 | if !FileManager.default.fileExists(atPath: path) { 16 | return false 17 | } 18 | let audio = Bundle.main.path(forResource: path, ofType: nil) 19 | if audio != nil { 20 | // 设置后台模式和锁屏模式下依旧能够播放 21 | try audioSession.setCategory(.playback, options: .mixWithOthers) 22 | try audioSession.setActive(true) 23 | backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "FlPiPBackgroundAudio") { 24 | // 后台任务结束时的清理工作 25 | print("Background task ended.") 26 | } 27 | if audioPlayer == nil { 28 | audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: audio!)) 29 | } 30 | audioPlayer!.volume = 0 31 | audioPlayer!.numberOfLoops = -1 32 | return true 33 | } 34 | 35 | } catch { 36 | print("FlPiP BackgroundAudioPlayer error") 37 | } 38 | return false 39 | } 40 | 41 | func stopPlay() { 42 | audioPlayer?.stop() 43 | audioPlayer = nil 44 | do { 45 | try audioSession.setActive(false) 46 | } catch { 47 | print("FlPiP AVAudioSession setActive error") 48 | } 49 | if backgroundTaskIdentifier != nil { 50 | UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier!) 51 | backgroundTaskIdentifier = nil 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | FlPiP 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | FlPiP 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UIBackgroundModes 32 | 33 | fetch 34 | audio 35 | processing 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fl_pip 2 | 3 | ## 基于原生ios和android的画中画模式,实现显示flutter的view,可以通过修改flutter 栈顶的view来显示任意UI 4 | 5 | ## The picture-in-picture mode is implemented in native ios and android to display flutter's view 6 | 7 | ### 目前在ios上遇到了一个问题,当app在后台的时候,FlutterUi停止运行或者画中画直接黑屏,猜测可能是由于ios冻结app导致,本人目前没有好的解决办法,如果你有想法,请提交pr 8 | 9 | ### At present, there is a problem in ios, when the app is in the background, FlutterUi will stop running or black screen directly, which may be caused by ios freezing the app, I have no good solution at present, if you have ideas, please submit PR 10 | 11 | ## Use configuration 12 | 13 | - ios 配置 : `Signing & Capabilities` -> `Capability` 添加 `BackgroundModes` 14 | 勾选 `Audio,AirPlay,And Picture in Picture` 15 | - ios configuration : `Signing & Capabilities` -> `Capability` Add `BackgroundModes` 16 | check `Audio,AirPlay,And Picture in Picture` 17 | - 修改`/ios/Runner/AppDelegate.swift` 的内容 18 | - Modify the content of `/ios/Runner/AppDelegate.swift` 19 | 20 | ### swift 21 | 22 | ```swift 23 | import fl_pip 24 | import Flutter 25 | import UIKit 26 | 27 | @main 28 | @objc class AppDelegate: FlFlutterAppDelegate { 29 | override func application( 30 | _ application: UIApplication, 31 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 32 | ) -> Bool { 33 | GeneratedPluginRegistrant.register(with: self) 34 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 35 | } 36 | 37 | override func registerPlugin(_ registry: FlutterPluginRegistry) { 38 | GeneratedPluginRegistrant.register(with: registry) 39 | } 40 | } 41 | 42 | 43 | ``` 44 | 45 | - android 配置 : `android/app/src/main/${your package name}/MainActivity` 修改 MainActivity 继承, 46 | - android configuration : `android/app/src/main/${your package name}/MainActivity`, 47 | 48 | ### kotlin 49 | 50 | ```kotlin 51 | 52 | class MainActivity : FlPiPActivity() 53 | 54 | ``` 55 | 56 | ### java 57 | 58 | ```java 59 | 60 | class MainActivity extends FlPiPActivity { 61 | 62 | } 63 | 64 | ``` 65 | 66 | android AndroidManifest file `android/app/src/main/AndroidManifest.xml`, 67 | add ` android:supportsPictureInPicture="true"` 68 | 69 | ```xml 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ## Methods available 77 | 78 | ```dart 79 | /// 开启画中画 80 | /// Open picture-in-picture 81 | void enable() { 82 | FlPiP().enable( 83 | iosConfig: FlPiPiOSConfig(), 84 | androidConfig: FlPiPAndroidConfig( 85 | aspectRatio: const Rational.maxLandscape())); 86 | } 87 | 88 | /// 是否支持画中画 89 | /// Whether to support picture in picture 90 | void isAvailable() { 91 | FlPiP().isAvailable; 92 | } 93 | 94 | /// 画中画状态 95 | /// Picture-in-picture window state 96 | void isActive() { 97 | FlPiP().isActive; 98 | } 99 | 100 | /// 切换前后台 101 | /// Toggle front and back 102 | /// ios仅支持切换后台 103 | /// ios supports background switching only 104 | void toggle() { 105 | FlPiP().toggle(); 106 | } 107 | 108 | /// 退出画中画 109 | /// Quit painting in picture 110 | void disable() { 111 | FlPiP().disable(); 112 | } 113 | ``` 114 | 115 | - 如果使用enableWithEngine方法必须在main文件中添加这个main方法 116 | - The main method must be added to the main file if the enableWithEngine method is used 117 | 118 | ```dart 119 | /// mainName must be the same as the method name 120 | @pragma('vm:entry-point') 121 | void pipMain() { 122 | runApp(YourApp()); 123 | } 124 | 125 | ``` 126 | 127 | - Android 128 | 129 | https://github.com/user-attachments/assets/1ba2238e-e556-4f87-8ccb-1b25440a6649 130 | 131 | - IOS 132 | 133 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/lib/src/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/main.dart'; 2 | import 'package:fl_pip/fl_pip.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | const videoPath = 'assets/landscape.mp4'; 6 | const closeIconPath = 'assets/close.png'; 7 | 8 | class HomePage extends StatelessWidget { 9 | const HomePage({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) => Scaffold( 13 | body: SafeArea( 14 | child: SingleChildScrollView( 15 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 16 | SizedBox(width: double.infinity, height: 20), 17 | Timer(), 18 | Container( 19 | margin: EdgeInsets.all(20), 20 | padding: EdgeInsets.all(12), 21 | decoration: BoxDecoration( 22 | borderRadius: BorderRadius.circular(10), 23 | border: Border.all(color: Theme.of(context).dividerColor)), 24 | child: PiPBuilder(builder: (PiPStatusInfo? statusInfo) { 25 | switch (statusInfo?.status) { 26 | case PiPStatus.enabled: 27 | return builderEnabled(statusInfo); 28 | case PiPStatus.disabled: 29 | return builderDisabled; 30 | case PiPStatus.unavailable: 31 | return buildUnavailable(context); 32 | case null: 33 | return builderDisabled; 34 | } 35 | })), 36 | Filled( 37 | text: 'PiPStatus isAvailable', 38 | onPressed: () async { 39 | final state = await FlPiP().isAvailable; 40 | if (context.mounted) { 41 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 42 | content: state 43 | ? const Text('PiP available') 44 | : const Text('PiP unavailable'))); 45 | } 46 | }), 47 | Filled( 48 | text: 'toggle', 49 | onPressed: () { 50 | FlPiP().toggle(AppState.background); 51 | }), 52 | ])), 53 | )); 54 | 55 | Widget builderEnabled(PiPStatusInfo? statusInfo) => Column(children: [ 56 | const Text('PiPStatus enabled'), 57 | Text('isCreateNewEngine: ${statusInfo!.isCreateNewEngine}', 58 | style: const TextStyle(fontSize: 10)), 59 | Text('isEnabledWhenBackground: ${statusInfo.isEnabledWhenBackground}', 60 | style: const TextStyle(fontSize: 10)), 61 | Filled(text: 'disable', onPressed: FlPiP().disable), 62 | ]); 63 | 64 | Widget get builderDisabled => 65 | Column(mainAxisSize: MainAxisSize.min, children: [ 66 | Text('Currently using picture in picture mode'), 67 | Filled( 68 | onPressed: () async { 69 | await FlPiP().enable( 70 | ios: const FlPiPiOSConfig( 71 | videoPath: videoPath, packageName: null), 72 | android: const FlPiPAndroidConfig( 73 | aspectRatio: Rational.maxLandscape())); 74 | Future.delayed(const Duration(seconds: 10), () { 75 | FlPiP().disable(); 76 | }); 77 | }, 78 | text: 'Enable PiP'), 79 | Text( 80 | 'The picture in picture mode will only be activated when the app enters the background'), 81 | Filled( 82 | onPressed: () { 83 | FlPiP().enable( 84 | ios: const FlPiPiOSConfig( 85 | enabledWhenBackground: true, 86 | videoPath: videoPath, 87 | packageName: null), 88 | android: const FlPiPAndroidConfig( 89 | enabledWhenBackground: true, 90 | aspectRatio: Rational.maxLandscape())); 91 | }, 92 | text: 'Enabled when background'), 93 | Divider(), 94 | Text( 95 | 'This still uses picture in picture mode in iOS and has created a new FlutterEngine that cannot be shared with the current main,But in Android, the picture in picture mode is not used, and WindowManager is used, similar to a system pop-up window'), 96 | Filled( 97 | onPressed: () { 98 | FlPiP().enable( 99 | android: const FlPiPAndroidConfig( 100 | createNewEngine: true, closeIconPath: closeIconPath), 101 | ios: const FlPiPiOSConfig( 102 | createNewEngine: true, 103 | videoPath: videoPath, 104 | packageName: null)); 105 | }, 106 | text: 'Create new engine'), 107 | Text('Start when the app enters the background'), 108 | Filled( 109 | onPressed: () { 110 | FlPiP().enable( 111 | android: const FlPiPAndroidConfig( 112 | closeIconPath: closeIconPath, 113 | enabledWhenBackground: true, 114 | createNewEngine: true), 115 | ios: const FlPiPiOSConfig( 116 | enabledWhenBackground: true, 117 | createNewEngine: true, 118 | videoPath: videoPath, 119 | packageName: null)); 120 | }, 121 | text: 'Create new engine and enabled when background'), 122 | ]); 123 | 124 | Widget buildUnavailable(BuildContext context) => Filled( 125 | text: 'PiP unavailable', 126 | onPressed: () async { 127 | final state = await FlPiP().isAvailable; 128 | if (!context.mounted) return; 129 | if (!state) { 130 | ScaffoldMessenger.of(context) 131 | .showSnackBar(const SnackBar(content: Text('PiP unavailable'))); 132 | } 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /lib/fl_pip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | typedef PiPBuilderCallback = Widget Function(PiPStatusInfo? status); 8 | 9 | class PiPBuilder extends StatefulWidget { 10 | const PiPBuilder({ 11 | super.key, 12 | required this.builder, 13 | }); 14 | 15 | final PiPBuilderCallback builder; 16 | 17 | @override 18 | State createState() => _PiPBuilderState(); 19 | } 20 | 21 | class _PiPBuilderState extends State { 22 | @override 23 | void initState() { 24 | super.initState(); 25 | WidgetsBinding.instance.addPostFrameCallback((_) async { 26 | final value = await FlPiP().isAvailable; 27 | if (value) { 28 | FlPiP().status.addListener(listener); 29 | await FlPiP().isActive; 30 | } else { 31 | FlPiP().status.value = PiPStatusInfo(status: PiPStatus.unavailable); 32 | } 33 | }); 34 | } 35 | 36 | void listener() { 37 | if (mounted) setState(() {}); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) => widget.builder(FlPiP().status.value); 42 | 43 | @override 44 | void dispose() { 45 | FlPiP().status.removeListener(listener); 46 | super.dispose(); 47 | } 48 | } 49 | 50 | enum PiPStatus { 51 | /// Show picture in picture 52 | enabled, 53 | 54 | /// Does not display picture-in-picture 55 | disabled, 56 | 57 | /// Picture-in-picture is not supported 58 | unavailable 59 | } 60 | 61 | class PiPStatusInfo { 62 | PiPStatusInfo( 63 | {required this.status, 64 | this.isCreateNewEngine = false, 65 | this.isEnabledWhenBackground = false}); 66 | 67 | PiPStatusInfo.fromMap(Map map) 68 | : status = PiPStatus.values[map['status'] as int], 69 | isCreateNewEngine = map['createNewEngine'] as bool, 70 | isEnabledWhenBackground = map['enabledWhenBackground'] as bool; 71 | 72 | /// pip status 73 | final PiPStatus status; 74 | 75 | /// is create new engine 76 | final bool isCreateNewEngine; 77 | 78 | /// is enabled when background 79 | final bool isEnabledWhenBackground; 80 | } 81 | 82 | const _channel = MethodChannel('fl_pip'); 83 | 84 | class FlPiP { 85 | factory FlPiP() => _singleton ??= FlPiP._(); 86 | 87 | static FlPiP? _singleton; 88 | 89 | FlPiP._() { 90 | _channel.setMethodCallHandler((call) async { 91 | switch (call.method) { 92 | case 'onPiPStatus': 93 | status.value = PiPStatusInfo.fromMap(call.arguments); 94 | break; 95 | } 96 | }); 97 | } 98 | 99 | final ValueNotifier status = ValueNotifier(null); 100 | 101 | /// 开启画中画 102 | /// enable picture-in-picture 103 | Future enable({ 104 | FlPiPAndroidConfig android = const FlPiPAndroidConfig(), 105 | FlPiPiOSConfig ios = const FlPiPiOSConfig(), 106 | }) async { 107 | if (!(_isAndroid || _isIOS)) { 108 | return false; 109 | } 110 | 111 | if (_isAndroid && 112 | !(android.aspectRatio.fitsInAndroidRequirements) && 113 | !android.createNewEngine) { 114 | throw RationalNotMatchingAndroidRequirementsException( 115 | android.aspectRatio); 116 | } 117 | final state = await _channel.invokeMethod( 118 | 'enable', _isAndroid ? android.toMap() : ios.toMap()); 119 | return state ?? false; 120 | } 121 | 122 | /// 关闭画中画 123 | /// disable picture-in-picture 124 | Future disable() async { 125 | final state = await _channel.invokeMethod('disable'); 126 | return state ?? false; 127 | } 128 | 129 | /// 画中画状态 130 | /// Picture-in-picture window state 131 | Future get isActive async { 132 | final map = await _channel.invokeMethod>('isActive'); 133 | if (map != null) { 134 | status.value = PiPStatusInfo.fromMap(map); 135 | } 136 | return status.value; 137 | } 138 | 139 | /// 是否支持画中画 140 | /// Whether to support picture in picture 141 | Future get isAvailable async { 142 | final bool? state = await _channel.invokeMethod('available'); 143 | return state ?? false; 144 | } 145 | 146 | /// 切换前后台 147 | /// Toggle front and back 148 | /// ios仅支持切换后台 149 | /// ios supports background switching only 150 | Future toggle(AppState state) => 151 | _channel.invokeMethod('toggle', state == AppState.foreground); 152 | } 153 | 154 | enum AppState { 155 | /// 前台 156 | foreground, 157 | 158 | /// 后台 159 | background, 160 | } 161 | 162 | class FlPiPConfig { 163 | const FlPiPConfig( 164 | {this.enabledWhenBackground = false, 165 | this.createNewEngine = false, 166 | this.packageName, 167 | this.rect}); 168 | 169 | /// ios 画中画弹出前视频的初始大小和位置,默认 [left:width/2,top:height/2,width:0.1,height:0.1] 170 | /// ios The initial size and position of the video before the picture-in-picture pops up,default [left:width/2,top:height/2,width:0.1,height:0.1] 171 | /// android 系统弹窗的大小和位置 ,默认 [left:width/2,top:height/2,width:300,height:300] 172 | /// android The size and position of the system popup,default [left:width/2,top:height/2,width:300,height:300] 173 | final Rect? rect; 174 | 175 | /// Enabled when the app is in the background 176 | /// 当app处于后台时启用 177 | final bool enabledWhenBackground; 178 | 179 | /// 创建新的 engine 180 | /// create new engine 181 | final bool createNewEngine; 182 | 183 | /// 资源地址的 packageName 184 | /// Set packageName to the asset address 185 | /// 如果使用你自己项目的资源文件 请设置[packageName]为null 186 | /// If using your own project's resource files, set [packageName] to null 187 | final String? packageName; 188 | 189 | Map toMap() => { 190 | 'left': rect?.left, 191 | 'top': rect?.top, 192 | 'width': rect?.width, 193 | 'height': rect?.height, 194 | 'packageName': packageName, 195 | 'enabledWhenBackground': enabledWhenBackground, 196 | 'createNewEngine': createNewEngine, 197 | }; 198 | } 199 | 200 | /// android 画中画配置 201 | /// android picture-in-picture configuration 202 | class FlPiPAndroidConfig extends FlPiPConfig { 203 | const FlPiPAndroidConfig( 204 | { 205 | /// Android 悬浮框右上角的关闭按钮的图片地址 206 | /// Android The image address of the Close button in the upper right corner of the floating 207 | this.closeIconPath, 208 | this.aspectRatio = const Rational.square(), 209 | super.packageName, 210 | super.enabledWhenBackground = false, 211 | super.createNewEngine = false, 212 | super.rect}); 213 | 214 | /// android 画中画窗口宽高比例 215 | /// android picture in picture window width-height ratio 216 | final Rational aspectRatio; 217 | 218 | /// 当createNewEngine 等于true时,在android上实际上是创建了一个windowManager,而不是PIP,这个是右上角关闭按钮的图片路径 219 | /// When [createNewEngine]= true, a windowManager is actually created on Android, not PIP. This is the image path of the close button in the upper right corner 220 | final String? closeIconPath; 221 | 222 | @override 223 | Map toMap() => { 224 | ...aspectRatio.toMap(), 225 | ...super.toMap(), 226 | 'closeIconPath': closeIconPath, 227 | }; 228 | } 229 | 230 | /// ios 画中画配置 231 | /// ios picture-in-picture configuration 232 | class FlPiPiOSConfig extends FlPiPConfig { 233 | const FlPiPiOSConfig( 234 | { 235 | /// 视频路径 用于修修改画中画尺寸 236 | /// The video [path] is used to modify the size of the picture in picture 237 | this.videoPath = 'assets/landscape.mp4', 238 | this.audioPath = 'assets/audio.mp3', 239 | super.packageName = 'fl_pip', 240 | this.enableControls = false, 241 | this.enablePlayback = false, 242 | super.enabledWhenBackground = false, 243 | super.createNewEngine = false, 244 | super.rect}); 245 | 246 | /// 显示播放控制 247 | /// Display play control 248 | final bool enableControls; 249 | 250 | /// 开启播放速度 251 | /// Turn on playback speed 252 | final bool enablePlayback; 253 | 254 | /// 视频路径 用于修修改画中画尺寸 255 | /// The video [path] is used to modify the size of the picture in picture 256 | /// Video address used to control pip width and height 257 | final String videoPath; 258 | 259 | /// Audio address 260 | final String audioPath; 261 | 262 | @override 263 | Map toMap() => { 264 | ...super.toMap(), 265 | 'videoPath': videoPath, 266 | 'audioPath': audioPath, 267 | 'packageName': packageName, 268 | 'enableControls': enableControls, 269 | 'enablePlayback': enablePlayback, 270 | }; 271 | } 272 | 273 | /// android 画中画宽高比 274 | /// android picture in picture aspect ratio 275 | class Rational { 276 | final int numerator; 277 | final int denominator; 278 | 279 | double get aspectRatio => numerator / denominator; 280 | 281 | const Rational(this.numerator, this.denominator); 282 | 283 | const Rational.square() 284 | : numerator = 1, 285 | denominator = 1; 286 | 287 | const Rational.landscape() 288 | : numerator = 16, 289 | denominator = 9; 290 | 291 | const Rational.maxLandscape() 292 | : numerator = 239, 293 | denominator = 100; 294 | 295 | const Rational.maxVertical() 296 | : numerator = 100, 297 | denominator = 239; 298 | 299 | const Rational.vertical() 300 | : numerator = 9, 301 | denominator = 16; 302 | 303 | @override 304 | String toString() => 305 | 'Rational(numerator: $numerator, denominator: $denominator)'; 306 | 307 | Map toMap() => 308 | {'numerator': numerator, 'denominator': denominator}; 309 | } 310 | 311 | extension on Rational { 312 | bool get fitsInAndroidRequirements { 313 | final aspectRatio = numerator / denominator; 314 | const min = 1 / 2.39; 315 | const max = 2.39; 316 | return (min <= aspectRatio) && (aspectRatio <= max); 317 | } 318 | } 319 | 320 | class RationalNotMatchingAndroidRequirementsException implements Exception { 321 | final Rational rational; 322 | 323 | RationalNotMatchingAndroidRequirementsException(this.rational); 324 | 325 | @override 326 | String toString() => 'RationalNotMatchingAndroidRequirementsException(' 327 | '${rational.numerator}/${rational.denominator} does not fit into ' 328 | 'Android-supported aspect ratios. Boundaries: ' 329 | 'min: 1/2.39, max: 2.39/1. ' 330 | ')'; 331 | } 332 | 333 | bool get _isAndroid => defaultTargetPlatform == TargetPlatform.android; 334 | 335 | bool get _isIOS => defaultTargetPlatform == TargetPlatform.iOS; 336 | -------------------------------------------------------------------------------- /ios/Classes/PiPHelper.swift: -------------------------------------------------------------------------------- 1 | import AVKit 2 | import Flutter 3 | import Foundation 4 | import UIKit 5 | 6 | class PiPHelper: NSObject, AVPictureInPictureControllerDelegate { 7 | static let shared = PiPHelper() 8 | 9 | private var playerLayer: AVPlayerLayer? 10 | private var player: AVPlayer? 11 | private var pipController: AVPictureInPictureController? 12 | 13 | private var engineGroup = FlutterEngineGroup(name: "pip.flutter", project: nil) 14 | private var flPiPEngine: FlutterEngine? 15 | private var flutterController: FlutterViewController? 16 | 17 | private var createNewEngine: Bool = false 18 | private var isEnable: Bool = false 19 | private var enabledWhenBackground: Bool = false 20 | private var rootWindow: UIWindow? 21 | 22 | private var registrar: FlutterPluginRegistrar? 23 | 24 | public func setRegistrar(_ registrar: FlutterPluginRegistrar) { 25 | if self.registrar == nil { 26 | self.registrar = registrar 27 | } 28 | } 29 | 30 | var channels: [Int: FlutterMethodChannel] = [:] 31 | 32 | public func newFlutterMethodChannel(_ messenger: FlutterBinaryMessenger) { 33 | let channel = FlutterMethodChannel(name: "fl_pip", binaryMessenger: messenger) 34 | channel.setMethodCallHandler { call, result in 35 | self.handle(call, result: result) 36 | } 37 | channels[messenger.hash] = channel 38 | } 39 | 40 | private var enableArgs: [String: Any?] = [:] 41 | 42 | private var isCallDisable: Bool = false 43 | 44 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 45 | switch call.method { 46 | case "enable": 47 | if isAvailable(), !isEnable { 48 | enableArgs = call.arguments as! [String: Any?] 49 | createNewEngine = enableArgs["createNewEngine"] as! Bool 50 | enabledWhenBackground = enableArgs["enabledWhenBackground"] as! Bool 51 | rootWindow = windows()?.filter { window in 52 | window.isKeyWindow 53 | }.first 54 | isEnable = enable() 55 | result(isEnable) 56 | return 57 | } 58 | result(false) 59 | case "disable": 60 | isCallDisable = true 61 | dispose() 62 | enableArgs = [:] 63 | setPiPStatus(1) 64 | result(true) 65 | case "isActive": 66 | var map = ["createNewEngine": createNewEngine, "enabledWhenBackground": enabledWhenBackground] as [String: Any] 67 | if isAvailable() { 68 | map["status"] = (pipController?.isPictureInPictureActive ?? false) ? 0 : 1 69 | } else { 70 | map["status"] = 2 71 | } 72 | result(map) 73 | case "toggle": 74 | let value = call.arguments as! Bool 75 | if value { 76 | /// 切换前台 77 | } else { 78 | /// 切换后台 79 | background() 80 | } 81 | result(nil) 82 | case "available": 83 | result(isAvailable()) 84 | default: 85 | result(nil) 86 | } 87 | } 88 | 89 | var audioPath: String? 90 | 91 | func enable() -> Bool { 92 | do { 93 | try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) 94 | try AVAudioSession.sharedInstance().setActive(true, options: []) 95 | } catch { 96 | print("FlPiP error : AVAudioSession.sharedInstance()") 97 | return false 98 | } 99 | var videoPath = enableArgs["videoPath"] as! String 100 | audioPath = (enableArgs["audioPath"] as! String) 101 | let packageName = enableArgs["packageName"] as? String 102 | if registrar != nil { 103 | if packageName != nil { 104 | videoPath = registrar!.lookupKey(forAsset: videoPath, fromPackage: packageName!) 105 | audioPath = registrar!.lookupKey(forAsset: audioPath!, fromPackage: packageName!) 106 | } else { 107 | videoPath = registrar!.lookupKey(forAsset: videoPath) 108 | audioPath = registrar!.lookupKey(forAsset: audioPath!) 109 | } 110 | } 111 | let bundleVideoPath = Bundle.main.path(forResource: videoPath, ofType: nil) 112 | if bundleVideoPath == nil { 113 | print("FlPiP error : Unable to load video resources, \(videoPath) in \(packageName ?? "current")") 114 | return false 115 | } 116 | if isAvailable() { 117 | if rootWindow == nil { 118 | print("FlPiP error : rootWindow is null") 119 | return false 120 | } 121 | getCreateNewEngine() 122 | playerLayer = AVPlayerLayer() 123 | let x = enableArgs["left"] as? CGFloat ?? UIScreen.main.bounds.size.width/2 124 | let y = enableArgs["top"] as? CGFloat ?? UIScreen.main.bounds.size.height/2 125 | let width = enableArgs["width"] as? CGFloat ?? 10 126 | let height = enableArgs["height"] as? CGFloat ?? 10 127 | 128 | playerLayer!.frame = .init(x: x, y: y, width: width, height: height) 129 | player = AVPlayer(playerItem: AVPlayerItem(asset: AVURLAsset(url: URL(fileURLWithPath: bundleVideoPath!)))) 130 | playerLayer!.player = player 131 | player!.isMuted = true 132 | player!.allowsExternalPlayback = true 133 | player!.accessibilityElementsHidden = true 134 | pipController = AVPictureInPictureController(playerLayer: playerLayer!) 135 | pipController!.delegate = self 136 | 137 | let enableControls = enableArgs["enableControls"] as! Bool 138 | pipController!.setValue(enableControls ? 0 : 1, forKey: "controlsStyle") 139 | 140 | let enablePlayback = enableArgs["enablePlayback"] as! Bool 141 | pipController!.setValue(enablePlayback ? 0 : 1, forKey: "requiresLinearPlayback") 142 | 143 | if #available(iOS 14.2, *) { 144 | pipController!.canStartPictureInPictureAutomaticallyFromInline = true 145 | } 146 | player!.play() 147 | rootWindow!.rootViewController?.view?.layer.addSublayer(playerLayer!) 148 | if !enabledWhenBackground { 149 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4) { 150 | self.pipController!.startPictureInPicture() 151 | } 152 | } 153 | return true 154 | } 155 | return false 156 | } 157 | 158 | func getCreateNewEngine() { 159 | if createNewEngine { 160 | let rootController = (rootWindow?.rootViewController as! FlutterViewController) 161 | flPiPEngine = engineGroup.makeEngine(withEntrypoint: "pipMain", libraryURI: nil) 162 | flPiPEngine!.run(withEntrypoint: "pipMain") 163 | flutterController = FlutterViewController( 164 | engine: flPiPEngine!, 165 | nibName: rootController.nibName, 166 | bundle: rootController.nibBundle) 167 | let delegate = UIApplication.shared.delegate as! FlFlutterAppDelegate 168 | delegate.registerPlugin(flutterController!.pluginRegistry()) 169 | } 170 | } 171 | 172 | public func background() { 173 | /// 切换后台 174 | let targetSelect = #selector(NSXPCConnection.suspend) 175 | if UIApplication.shared.responds(to: targetSelect) { 176 | UIApplication.shared.perform(targetSelect) 177 | } 178 | } 179 | 180 | public func isAvailable() -> Bool { 181 | AVPictureInPictureController.isPictureInPictureSupported() 182 | } 183 | 184 | public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 185 | if let firstWindow = UIApplication.shared.windows.first, rootWindow != nil { 186 | let rect = firstWindow.rootViewController?.view.frame ?? CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height) 187 | setPiPStatus(0) 188 | if createNewEngine { 189 | flutterController?.view.frame = rect 190 | firstWindow.rootViewController = flutterController 191 | } else { 192 | let rootController = rootWindow!.rootViewController 193 | let flController = (rootController as! FlutterViewController) 194 | let engine = flController.engine 195 | engine.viewController = nil 196 | let newController = FlutterViewController(engine: engine, nibName: flController.nibName, bundle: flController.nibBundle) 197 | flController.dismiss(animated: true) 198 | newController.view.frame = rect 199 | firstWindow.rootViewController = newController 200 | } 201 | } 202 | } 203 | 204 | func setPiPStatus(_ int: Int) { 205 | channels.forEach { channel in 206 | channel.value.invokeMethod("onPiPStatus", arguments: ["createNewEngine": createNewEngine, "enabledWhenBackground": enabledWhenBackground, "status": int]) 207 | } 208 | } 209 | 210 | public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 211 | if !isCallDisable { 212 | dispose() 213 | } 214 | } 215 | 216 | public func dispose() { 217 | pipController?.stopPictureInPicture() 218 | if createNewEngine { 219 | flutterController?.removeFromParent() 220 | flPiPEngine?.viewController?.dismiss(animated: false) 221 | flutterController = nil 222 | if flPiPEngine != nil { 223 | channels.removeValue(forKey: flPiPEngine!.binaryMessenger.hash) 224 | } 225 | flPiPEngine = nil 226 | } else if rootWindow != nil { 227 | let rect = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height) 228 | let firstWindow = UIApplication.shared.windows.first 229 | if firstWindow!.rootViewController is FlutterViewController { 230 | let flController = (firstWindow!.rootViewController as! FlutterViewController) 231 | let engine = flController.engine 232 | engine.viewController = nil 233 | let newController = FlutterViewController(engine: flController.engine, nibName: flController.nibName, bundle: flController.nibBundle) 234 | flController.dismiss(animated: true) 235 | firstWindow!.rootViewController = nil 236 | newController.view.frame = rect 237 | rootWindow?.rootViewController = newController 238 | } 239 | } 240 | isCallDisable = false 241 | pipController = nil 242 | playerLayer?.removeFromSuperlayer() 243 | playerLayer = nil 244 | player?.replaceCurrentItem(with: nil) 245 | player = nil 246 | setPiPStatus(1) 247 | isEnable = false 248 | createNewEngine = false 249 | enabledWhenBackground = false 250 | } 251 | 252 | public func applicationWillEnterForeground(_ application: UIApplication) { 253 | BackgroundAudioPlayer.shared.stopPlay() 254 | if enabledWhenBackground { 255 | // print("app will enter foreground") 256 | } 257 | } 258 | 259 | public func applicationDidEnterBackground(_ application: UIApplication) { 260 | if let audioPath=audioPath,!audioPath.isEmpty{ 261 | BackgroundAudioPlayer.shared.startPlay(audioPath) 262 | } 263 | 264 | if enabledWhenBackground { 265 | if createNewEngine { 266 | getCreateNewEngine() 267 | } 268 | pipController?.startPictureInPicture() 269 | } 270 | } 271 | 272 | public func windows() -> [UIWindow]? { 273 | return UIApplication.shared.windows 274 | // if #available(iOS 13.0, *) { 275 | // let windowScene = (UIApplication.shared.connectedScenes.first as? UIWindowScene) 276 | // return windowScene?.windows 277 | // } else { 278 | // return UIApplication.shared.windows 279 | // } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /android/src/main/kotlin/fl/pip/PiPHelper.kt: -------------------------------------------------------------------------------- 1 | package fl.pip 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ActivityManager 5 | import android.app.PictureInPictureParams 6 | import android.app.Service 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.content.pm.PackageManager 10 | import android.graphics.BitmapFactory 11 | import android.graphics.PixelFormat 12 | import android.graphics.Rect 13 | import android.net.Uri 14 | import android.os.Build 15 | import android.provider.Settings 16 | import android.util.Log 17 | import android.util.Rational 18 | import android.view.Gravity 19 | import android.view.MotionEvent 20 | import android.view.View 21 | import android.view.WindowManager 22 | import android.widget.FrameLayout 23 | import android.widget.ImageView 24 | import io.flutter.FlutterInjector 25 | import io.flutter.embedding.android.FlutterActivity 26 | import io.flutter.embedding.android.FlutterSurfaceView 27 | import io.flutter.embedding.android.FlutterView 28 | import io.flutter.embedding.engine.FlutterEngine 29 | import io.flutter.embedding.engine.FlutterEngineCache 30 | import io.flutter.embedding.engine.FlutterEngineGroup 31 | import io.flutter.embedding.engine.dart.DartExecutor 32 | import io.flutter.embedding.engine.plugins.FlutterPlugin 33 | import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister 34 | import io.flutter.plugin.common.MethodCall 35 | import io.flutter.plugin.common.MethodChannel 36 | 37 | class PiPHelper private constructor() { 38 | companion object { 39 | @SuppressLint("StaticFieldLeak") 40 | private var instance: PiPHelper? = null 41 | 42 | fun getInstance(): PiPHelper { 43 | return instance ?: synchronized(this) { 44 | instance ?: PiPHelper().also { instance = it } 45 | } 46 | } 47 | } 48 | 49 | private var createNewEngine = false 50 | private var enabledWhenBackground = false 51 | private var context: Context? = null 52 | private var activity: FlutterActivity? = null 53 | private var pluginBinding: FlutterPlugin.FlutterPluginBinding? = null 54 | private var enableArgs: Map<*, *> = mutableMapOf() 55 | private var engineId = "pip.flutter" 56 | private var engine: FlutterEngine? = null 57 | private var flutterView: FlutterView? = null 58 | private var windowManager: WindowManager? = null 59 | private var rootView: FrameLayout? = null 60 | 61 | var channels = mutableListOf() 62 | 63 | // 是否开启PIP 64 | private var isEnabledPIP = false 65 | 66 | // 是否开启WM 67 | private var isEnabledWM = false 68 | 69 | fun setActivity(activity: FlutterActivity, context: Context) { 70 | this.activity = activity 71 | this.context = context 72 | } 73 | 74 | private fun setPiPStatus(int: Int) { 75 | channels.forEach { 76 | it.invokeMethod( 77 | "onPiPStatus", mapOf( 78 | "createNewEngine" to createNewEngine, 79 | "enabledWhenBackground" to enabledWhenBackground, 80 | "status" to int, 81 | ) 82 | ) 83 | } 84 | } 85 | 86 | fun onMethodCall( 87 | call: MethodCall, result: MethodChannel.Result, binding: FlutterPlugin.FlutterPluginBinding 88 | ) { 89 | if (context != null && activity != null) { 90 | pluginBinding = binding 91 | when (call.method) { 92 | "enable" -> { 93 | enableArgs = call.arguments as Map<*, *> 94 | enabledWhenBackground = enableArgs["enabledWhenBackground"] as Boolean 95 | if (!enabledWhenBackground) { 96 | result.success(enable()) 97 | return 98 | } 99 | result.success(false) 100 | } 101 | 102 | "disable" -> { 103 | createNewEngine = false 104 | enabledWhenBackground = false 105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity!!.isInPictureInPictureMode) { 106 | launchApp() 107 | } 108 | disposeEngine() 109 | setPiPStatus(1) 110 | result.success(true) 111 | } 112 | 113 | "isActive" -> { 114 | val map = mutableMapOf( 115 | "createNewEngine" to createNewEngine, 116 | "enabledWhenBackground" to enabledWhenBackground 117 | ) 118 | val isAvailable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 119 | activity!!.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 120 | } else { 121 | false 122 | } 123 | if (isAvailable) { 124 | map["status"] = 125 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity!!.isInPictureInPictureMode) 0 else 1 126 | } else { 127 | map["status"] = 2 128 | } 129 | result.success(map) 130 | } 131 | 132 | "available" -> result.success( 133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 134 | activity!!.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 135 | } else false 136 | ) 137 | 138 | "toggle" -> { 139 | if (call.arguments as Boolean) { 140 | launchApp() 141 | } else { 142 | background() 143 | } 144 | result.success(null) 145 | } 146 | 147 | "launchApp" -> { 148 | val intent = Intent(context, activity!!.javaClass) 149 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 150 | activity!!.startActivity(intent) 151 | result.success(true) 152 | } 153 | 154 | else -> result.notImplemented() 155 | } 156 | } else { 157 | result.error("0", "MainActivity must extends FliPActivity", null) 158 | } 159 | } 160 | 161 | 162 | private fun background() { 163 | if (!isForeground) return 164 | /// 切换后台 165 | val intent = Intent(Intent.ACTION_MAIN) 166 | intent.addCategory(Intent.CATEGORY_HOME) 167 | activity!!.startActivity(intent) 168 | } 169 | 170 | 171 | private fun launchApp() { 172 | if (isForeground) return 173 | /// 启动app 174 | val intent = 175 | activity!!.packageManager.getLaunchIntentForPackage(activity!!.applicationContext.packageName) 176 | intent?.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP 177 | activity!!.startActivity(intent) 178 | isEnabledWM = rootView == null 179 | isEnabledPIP = false 180 | } 181 | 182 | 183 | private fun enable(): Boolean { 184 | if (isEnabledPIP || isEnabledWM) return false 185 | createNewEngine = enableArgs["createNewEngine"] as Boolean 186 | return if (createNewEngine) { 187 | enableWM() 188 | } else { 189 | enablePiP() 190 | } 191 | } 192 | 193 | 194 | private fun enablePiP(): Boolean { 195 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 196 | return false 197 | } 198 | val pipBuilder = PictureInPictureParams.Builder().apply { 199 | setAspectRatio( 200 | Rational( 201 | enableArgs["numerator"] as Int, enableArgs["denominator"] as Int 202 | ) 203 | ) 204 | setSourceRectHint(Rect(0, 0, 0, 0)) 205 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 206 | setSeamlessResizeEnabled(false) 207 | } 208 | } 209 | return activity!!.enterPictureInPictureMode(pipBuilder.build()) 210 | } 211 | 212 | private fun enableWM(): Boolean { 213 | if (isEnabledWM) { 214 | return false 215 | } 216 | if (!checkPermission()) { 217 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 218 | activity!!.startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply { 219 | data = Uri.parse("package:${context!!.packageName}") 220 | }) 221 | } 222 | setPiPStatus(1) 223 | return false 224 | } 225 | isEnabledWM = true 226 | getCreateNewEngine() 227 | val displayMetrics = context!!.resources.displayMetrics 228 | val w = (enableArgs["width"] as Double?)?.toInt() ?: (displayMetrics.widthPixels - 100) 229 | val h = (enableArgs["height"] as Double?)?.toInt() ?: 600 230 | val layoutParams = WindowManager.LayoutParams().apply { 231 | type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 232 | WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 233 | } else { 234 | @Suppress("Deprecation") WindowManager.LayoutParams.TYPE_TOAST 235 | } 236 | format = PixelFormat.TRANSLUCENT 237 | flags = 238 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 239 | width = w 240 | height = h 241 | gravity = Gravity.START or Gravity.TOP 242 | x = (enableArgs["left"] as Double?)?.toInt() ?: 50 243 | y = (enableArgs["top"] as Double?)?.toInt() ?: (displayMetrics.heightPixels / 2) 244 | } 245 | rootView = FrameLayout(context!!) 246 | rootView!!.addView(flutterView, FrameLayout.LayoutParams(w, h)) 247 | pluginBinding?.let { 248 | val packageName = enableArgs["packageName"] as String? 249 | val closeIconPath = enableArgs["closeIconPath"] as String? 250 | closeIconPath?.let { 251 | addCloseIconView(closeIconPath, packageName) 252 | } 253 | } 254 | windowManager = context!!.getSystemService(Service.WINDOW_SERVICE) as WindowManager 255 | @Suppress("ClickableViewAccessibility") flutterView!!.setOnTouchListener(object : 256 | View.OnTouchListener { 257 | private var initialX: Int = 0 258 | private var initialY: Int = 0 259 | private var initialTouchX: Float = 0f 260 | private var initialTouchY: Float = 0f 261 | 262 | @SuppressLint("ClickableViewAccessibility") 263 | override fun onTouch(view: View, event: MotionEvent): Boolean { 264 | when (event.action) { 265 | MotionEvent.ACTION_DOWN -> { 266 | initialX = layoutParams.x 267 | initialY = layoutParams.y 268 | initialTouchX = event.rawX 269 | initialTouchY = event.rawY 270 | } 271 | 272 | MotionEvent.ACTION_MOVE -> { 273 | val dx = event.rawX - initialTouchX 274 | val dy = event.rawY - initialTouchY 275 | layoutParams.x = (initialX + dx).toInt() 276 | layoutParams.y = (initialY + dy).toInt() 277 | windowManager!!.updateViewLayout(rootView, layoutParams) 278 | } 279 | } 280 | return false 281 | } 282 | }) 283 | windowManager!!.addView(rootView, layoutParams) 284 | setPiPStatus(0) 285 | return true 286 | } 287 | 288 | 289 | private fun addCloseIconView(closeIconPath: String, packageName: String?) { 290 | val closeIcon: String = if (packageName == null) { 291 | pluginBinding!!.flutterAssets.getAssetFilePathByName(closeIconPath) 292 | } else { 293 | pluginBinding!!.flutterAssets.getAssetFilePathByName( 294 | enableArgs["closeIconPath"] as String, packageName 295 | ) 296 | } 297 | val bitmap = BitmapFactory.decodeStream(context!!.assets.open(closeIcon)) 298 | val close = ImageView(context) 299 | close.setOnClickListener { 300 | setPiPStatus(1) 301 | /// 切换前台 302 | (context!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).moveTaskToFront( 303 | activity!!.taskId, ActivityManager.MOVE_TASK_WITH_HOME 304 | ) 305 | disposeEngine() 306 | } 307 | close.setImageBitmap(bitmap) 308 | val closeLayoutParams = FrameLayout.LayoutParams( 309 | dp2px(22), dp2px(22) 310 | ) 311 | closeLayoutParams.gravity = Gravity.END 312 | closeLayoutParams.setMargins(0, dp2px(4), dp2px(4), 0) 313 | rootView!!.addView(close, closeLayoutParams) 314 | } 315 | 316 | private fun getCreateNewEngine() { 317 | if (engine == null) { 318 | flutterView = FlutterView(context!!, FlutterSurfaceView(context!!, true)) 319 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 320 | flutterView!!.elevation = 0F 321 | } 322 | val dartEntrypoint = DartExecutor.DartEntrypoint( 323 | FlutterInjector.instance().flutterLoader().findAppBundlePath(), "pipMain" 324 | ) 325 | val engineGroup = pluginBinding?.engineGroup ?: FlutterEngineGroup(context!!) 326 | engine = engineGroup.createAndRunEngine(context!!, dartEntrypoint) 327 | GeneratedPluginRegister.registerGeneratedPlugins(engine!!) 328 | FlutterEngineCache.getInstance().put(engineId, engine) 329 | flutterView!!.attachToFlutterEngine(engine!!) 330 | 331 | engine!!.platformViewsController.attach( 332 | context, engine!!.renderer, engine!!.dartExecutor 333 | ) 334 | engine!!.lifecycleChannel.appIsResumed() 335 | } 336 | } 337 | 338 | private fun checkPermission(): Boolean { 339 | var result = true 340 | if (Build.VERSION.SDK_INT >= 23) { 341 | try { 342 | val clazz: Class<*> = Settings::class.java 343 | val canDrawOverlays = 344 | clazz.getDeclaredMethod("canDrawOverlays", Context::class.java) 345 | result = canDrawOverlays.invoke(null, context) as Boolean 346 | } catch (e: Exception) { 347 | println("FlPiP checkPermission error : ${Log.getStackTraceString(e)}") 348 | } 349 | } 350 | return result 351 | } 352 | 353 | private fun disposeEngine() { 354 | if (flutterView != null) { 355 | flutterView?.detachFromFlutterEngine() 356 | windowManager?.removeView(rootView) 357 | } 358 | flutterView = null 359 | rootView = null 360 | engine?.let { 361 | it.destroy() 362 | FlutterEngineCache.getInstance().remove(engineId) 363 | } 364 | engine = null 365 | isEnabledWM = false 366 | } 367 | 368 | private fun dp2px(value: Int): Int { 369 | val scale: Float = context!!.resources.displayMetrics.density 370 | return (value * scale + 0.5f).toInt() 371 | } 372 | 373 | 374 | private var isForeground = true 375 | 376 | fun onActivityPaused() { 377 | isForeground = false 378 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && enabledWhenBackground) { 379 | enable() 380 | } 381 | } 382 | 383 | fun onActivityResume() { 384 | isForeground = true 385 | } 386 | 387 | fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { 388 | isEnabledPIP = isInPictureInPictureMode 389 | setPiPStatus(if (isInPictureInPictureMode) 0 else 1) 390 | } 391 | 392 | } 393 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 12F9469B8F2B6E2C6A32410D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6A3FA0AADEF0488A50600E1 /* Pods_Runner.framework */; }; 11 | 143DBB0F2CA6A5AA00D9293C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143DBB0E2CA6A5AA00D9293C /* SceneDelegate.swift */; }; 12 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 13 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 14 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 15 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 16 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 17 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 14039C637FE979E8636D3968 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 35 | 143DBB0E2CA6A5AA00D9293C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 36 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 37 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 40 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 42 | 877F101B80249A1B7DA26A73 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | C6A3FA0AADEF0488A50600E1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | F0BDAF417CC5219A6D45FF6B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 12F9469B8F2B6E2C6A32410D /* Pods_Runner.framework in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 016B4EB03C32695D4798EDC5 /* Frameworks */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | C6A3FA0AADEF0488A50600E1 /* Pods_Runner.framework */, 70 | ); 71 | name = Frameworks; 72 | sourceTree = ""; 73 | }; 74 | 9740EEB11CF90186004384FC /* Flutter */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 78 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 79 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 80 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 81 | ); 82 | name = Flutter; 83 | sourceTree = ""; 84 | }; 85 | 97C146E51CF9000F007C117D = { 86 | isa = PBXGroup; 87 | children = ( 88 | 9740EEB11CF90186004384FC /* Flutter */, 89 | 97C146F01CF9000F007C117D /* Runner */, 90 | 97C146EF1CF9000F007C117D /* Products */, 91 | AFA0B8D296E081E0BF11773E /* Pods */, 92 | 016B4EB03C32695D4798EDC5 /* Frameworks */, 93 | ); 94 | sourceTree = ""; 95 | }; 96 | 97C146EF1CF9000F007C117D /* Products */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 97C146EE1CF9000F007C117D /* Runner.app */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 97C146F01CF9000F007C117D /* Runner */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 108 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 109 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 110 | 97C147021CF9000F007C117D /* Info.plist */, 111 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 112 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 113 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 114 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 115 | 143DBB0E2CA6A5AA00D9293C /* SceneDelegate.swift */, 116 | ); 117 | path = Runner; 118 | sourceTree = ""; 119 | }; 120 | AFA0B8D296E081E0BF11773E /* Pods */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 877F101B80249A1B7DA26A73 /* Pods-Runner.debug.xcconfig */, 124 | 14039C637FE979E8636D3968 /* Pods-Runner.release.xcconfig */, 125 | F0BDAF417CC5219A6D45FF6B /* Pods-Runner.profile.xcconfig */, 126 | ); 127 | path = Pods; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | 97C146ED1CF9000F007C117D /* Runner */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 136 | buildPhases = ( 137 | 11A0EFE73ADB99F5D8EF4BFA /* [CP] Check Pods Manifest.lock */, 138 | 9740EEB61CF901F6004384FC /* Run Script */, 139 | 97C146EA1CF9000F007C117D /* Sources */, 140 | 97C146EB1CF9000F007C117D /* Frameworks */, 141 | 97C146EC1CF9000F007C117D /* Resources */, 142 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 143 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 144 | E64EE36A5CC50A938C696097 /* [CP] Embed Pods Frameworks */, 145 | ); 146 | buildRules = ( 147 | ); 148 | dependencies = ( 149 | ); 150 | name = Runner; 151 | productName = Runner; 152 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 153 | productType = "com.apple.product-type.application"; 154 | }; 155 | /* End PBXNativeTarget section */ 156 | 157 | /* Begin PBXProject section */ 158 | 97C146E61CF9000F007C117D /* Project object */ = { 159 | isa = PBXProject; 160 | attributes = { 161 | LastUpgradeCheck = 1510; 162 | ORGANIZATIONNAME = ""; 163 | TargetAttributes = { 164 | 97C146ED1CF9000F007C117D = { 165 | CreatedOnToolsVersion = 7.3.1; 166 | LastSwiftMigration = 1100; 167 | }; 168 | }; 169 | }; 170 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 171 | compatibilityVersion = "Xcode 9.3"; 172 | developmentRegion = en; 173 | hasScannedForEncodings = 0; 174 | knownRegions = ( 175 | en, 176 | Base, 177 | ); 178 | mainGroup = 97C146E51CF9000F007C117D; 179 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 180 | projectDirPath = ""; 181 | projectRoot = ""; 182 | targets = ( 183 | 97C146ED1CF9000F007C117D /* Runner */, 184 | ); 185 | }; 186 | /* End PBXProject section */ 187 | 188 | /* Begin PBXResourcesBuildPhase section */ 189 | 97C146EC1CF9000F007C117D /* Resources */ = { 190 | isa = PBXResourcesBuildPhase; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 194 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 195 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 196 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXResourcesBuildPhase section */ 201 | 202 | /* Begin PBXShellScriptBuildPhase section */ 203 | 11A0EFE73ADB99F5D8EF4BFA /* [CP] Check Pods Manifest.lock */ = { 204 | isa = PBXShellScriptBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | ); 208 | inputFileListPaths = ( 209 | ); 210 | inputPaths = ( 211 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 212 | "${PODS_ROOT}/Manifest.lock", 213 | ); 214 | name = "[CP] Check Pods Manifest.lock"; 215 | outputFileListPaths = ( 216 | ); 217 | outputPaths = ( 218 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | shellPath = /bin/sh; 222 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 223 | showEnvVarsInLog = 0; 224 | }; 225 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 226 | isa = PBXShellScriptBuildPhase; 227 | alwaysOutOfDate = 1; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | inputPaths = ( 232 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 233 | ); 234 | name = "Thin Binary"; 235 | outputPaths = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 240 | }; 241 | 9740EEB61CF901F6004384FC /* Run Script */ = { 242 | isa = PBXShellScriptBuildPhase; 243 | alwaysOutOfDate = 1; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | inputPaths = ( 248 | ); 249 | name = "Run Script"; 250 | outputPaths = ( 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | shellPath = /bin/sh; 254 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 255 | }; 256 | E64EE36A5CC50A938C696097 /* [CP] Embed Pods Frameworks */ = { 257 | isa = PBXShellScriptBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | inputFileListPaths = ( 262 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 263 | ); 264 | name = "[CP] Embed Pods Frameworks"; 265 | outputFileListPaths = ( 266 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | shellPath = /bin/sh; 270 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 271 | showEnvVarsInLog = 0; 272 | }; 273 | /* End PBXShellScriptBuildPhase section */ 274 | 275 | /* Begin PBXSourcesBuildPhase section */ 276 | 97C146EA1CF9000F007C117D /* Sources */ = { 277 | isa = PBXSourcesBuildPhase; 278 | buildActionMask = 2147483647; 279 | files = ( 280 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 281 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 282 | 143DBB0F2CA6A5AA00D9293C /* SceneDelegate.swift in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | /* End PBXSourcesBuildPhase section */ 287 | 288 | /* Begin PBXVariantGroup section */ 289 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 290 | isa = PBXVariantGroup; 291 | children = ( 292 | 97C146FB1CF9000F007C117D /* Base */, 293 | ); 294 | name = Main.storyboard; 295 | sourceTree = ""; 296 | }; 297 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 298 | isa = PBXVariantGroup; 299 | children = ( 300 | 97C147001CF9000F007C117D /* Base */, 301 | ); 302 | name = LaunchScreen.storyboard; 303 | sourceTree = ""; 304 | }; 305 | /* End PBXVariantGroup section */ 306 | 307 | /* Begin XCBuildConfiguration section */ 308 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_ANALYZER_NONNULL = YES; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNREACHABLE_CODE = YES; 335 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 336 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 339 | ENABLE_NS_ASSERTIONS = NO; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu99; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 350 | MTL_ENABLE_DEBUG_INFO = NO; 351 | SDKROOT = iphoneos; 352 | SUPPORTED_PLATFORMS = iphoneos; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | VALIDATE_PRODUCT = YES; 355 | }; 356 | name = Profile; 357 | }; 358 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 359 | isa = XCBuildConfiguration; 360 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 361 | buildSettings = { 362 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 363 | CLANG_ENABLE_MODULES = YES; 364 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 365 | DEVELOPMENT_TEAM = ""; 366 | ENABLE_BITCODE = NO; 367 | INFOPLIST_FILE = Runner/Info.plist; 368 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 369 | LD_RUNPATH_SEARCH_PATHS = ( 370 | "$(inherited)", 371 | "@executable_path/Frameworks", 372 | ); 373 | PRODUCT_BUNDLE_IDENTIFIER = fl.pip.example; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 376 | SWIFT_VERSION = 5.0; 377 | VERSIONING_SYSTEM = "apple-generic"; 378 | }; 379 | name = Profile; 380 | }; 381 | 97C147031CF9000F007C117D /* Debug */ = { 382 | isa = XCBuildConfiguration; 383 | buildSettings = { 384 | ALWAYS_SEARCH_USER_PATHS = NO; 385 | CLANG_ANALYZER_NONNULL = YES; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_COMMA = YES; 393 | CLANG_WARN_CONSTANT_CONVERSION = YES; 394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 396 | CLANG_WARN_EMPTY_BODY = YES; 397 | CLANG_WARN_ENUM_CONVERSION = YES; 398 | CLANG_WARN_INFINITE_RECURSION = YES; 399 | CLANG_WARN_INT_CONVERSION = YES; 400 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 402 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 410 | COPY_PHASE_STRIP = NO; 411 | DEBUG_INFORMATION_FORMAT = dwarf; 412 | ENABLE_STRICT_OBJC_MSGSEND = YES; 413 | ENABLE_TESTABILITY = YES; 414 | GCC_C_LANGUAGE_STANDARD = gnu99; 415 | GCC_DYNAMIC_NO_PIC = NO; 416 | GCC_NO_COMMON_BLOCKS = YES; 417 | GCC_OPTIMIZATION_LEVEL = 0; 418 | GCC_PREPROCESSOR_DEFINITIONS = ( 419 | "DEBUG=1", 420 | "$(inherited)", 421 | ); 422 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 423 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 424 | GCC_WARN_UNDECLARED_SELECTOR = YES; 425 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 426 | GCC_WARN_UNUSED_FUNCTION = YES; 427 | GCC_WARN_UNUSED_VARIABLE = YES; 428 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 429 | MTL_ENABLE_DEBUG_INFO = YES; 430 | ONLY_ACTIVE_ARCH = YES; 431 | SDKROOT = iphoneos; 432 | TARGETED_DEVICE_FAMILY = "1,2"; 433 | }; 434 | name = Debug; 435 | }; 436 | 97C147041CF9000F007C117D /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | ALWAYS_SEARCH_USER_PATHS = NO; 440 | CLANG_ANALYZER_NONNULL = YES; 441 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 442 | CLANG_CXX_LIBRARY = "libc++"; 443 | CLANG_ENABLE_MODULES = YES; 444 | CLANG_ENABLE_OBJC_ARC = YES; 445 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 446 | CLANG_WARN_BOOL_CONVERSION = YES; 447 | CLANG_WARN_COMMA = YES; 448 | CLANG_WARN_CONSTANT_CONVERSION = YES; 449 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 450 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 451 | CLANG_WARN_EMPTY_BODY = YES; 452 | CLANG_WARN_ENUM_CONVERSION = YES; 453 | CLANG_WARN_INFINITE_RECURSION = YES; 454 | CLANG_WARN_INT_CONVERSION = YES; 455 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 456 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 457 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 459 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 460 | CLANG_WARN_STRICT_PROTOTYPES = YES; 461 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 462 | CLANG_WARN_UNREACHABLE_CODE = YES; 463 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 464 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 465 | COPY_PHASE_STRIP = NO; 466 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 467 | ENABLE_NS_ASSERTIONS = NO; 468 | ENABLE_STRICT_OBJC_MSGSEND = YES; 469 | GCC_C_LANGUAGE_STANDARD = gnu99; 470 | GCC_NO_COMMON_BLOCKS = YES; 471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 473 | GCC_WARN_UNDECLARED_SELECTOR = YES; 474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 475 | GCC_WARN_UNUSED_FUNCTION = YES; 476 | GCC_WARN_UNUSED_VARIABLE = YES; 477 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 478 | MTL_ENABLE_DEBUG_INFO = NO; 479 | SDKROOT = iphoneos; 480 | SUPPORTED_PLATFORMS = iphoneos; 481 | SWIFT_COMPILATION_MODE = wholemodule; 482 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 483 | TARGETED_DEVICE_FAMILY = "1,2"; 484 | VALIDATE_PRODUCT = YES; 485 | }; 486 | name = Release; 487 | }; 488 | 97C147061CF9000F007C117D /* Debug */ = { 489 | isa = XCBuildConfiguration; 490 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 491 | buildSettings = { 492 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 493 | CLANG_ENABLE_MODULES = YES; 494 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 495 | DEVELOPMENT_TEAM = ""; 496 | ENABLE_BITCODE = NO; 497 | INFOPLIST_FILE = Runner/Info.plist; 498 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 499 | LD_RUNPATH_SEARCH_PATHS = ( 500 | "$(inherited)", 501 | "@executable_path/Frameworks", 502 | ); 503 | PRODUCT_BUNDLE_IDENTIFIER = fl.pip.example; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 506 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 507 | SWIFT_VERSION = 5.0; 508 | VERSIONING_SYSTEM = "apple-generic"; 509 | }; 510 | name = Debug; 511 | }; 512 | 97C147071CF9000F007C117D /* Release */ = { 513 | isa = XCBuildConfiguration; 514 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 515 | buildSettings = { 516 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 517 | CLANG_ENABLE_MODULES = YES; 518 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 519 | DEVELOPMENT_TEAM = ""; 520 | ENABLE_BITCODE = NO; 521 | INFOPLIST_FILE = Runner/Info.plist; 522 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 523 | LD_RUNPATH_SEARCH_PATHS = ( 524 | "$(inherited)", 525 | "@executable_path/Frameworks", 526 | ); 527 | PRODUCT_BUNDLE_IDENTIFIER = fl.pip.example; 528 | PRODUCT_NAME = "$(TARGET_NAME)"; 529 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 530 | SWIFT_VERSION = 5.0; 531 | VERSIONING_SYSTEM = "apple-generic"; 532 | }; 533 | name = Release; 534 | }; 535 | /* End XCBuildConfiguration section */ 536 | 537 | /* Begin XCConfigurationList section */ 538 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 539 | isa = XCConfigurationList; 540 | buildConfigurations = ( 541 | 97C147031CF9000F007C117D /* Debug */, 542 | 97C147041CF9000F007C117D /* Release */, 543 | 249021D3217E4FDB00AE95B9 /* Profile */, 544 | ); 545 | defaultConfigurationIsVisible = 0; 546 | defaultConfigurationName = Release; 547 | }; 548 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 549 | isa = XCConfigurationList; 550 | buildConfigurations = ( 551 | 97C147061CF9000F007C117D /* Debug */, 552 | 97C147071CF9000F007C117D /* Release */, 553 | 249021D4217E4FDB00AE95B9 /* Profile */, 554 | ); 555 | defaultConfigurationIsVisible = 0; 556 | defaultConfigurationName = Release; 557 | }; 558 | /* End XCConfigurationList section */ 559 | }; 560 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 561 | } 562 | --------------------------------------------------------------------------------