diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..906bbb3 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.24.3" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9be145f..aab2f03 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. @@ -27,3 +27,6 @@ .dart_tool/ .packages build/ + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fe35ea3..62cba3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ +## 1.1.0 + +* Fix namespace issue and add a new media action preset by [kmartins](https://github.com/kmartins): [PR #21](https://github.com/PuntitOwO/simple_pip_mode_flutter/pull/21) +* Add deprecation warning to `PipWidget.builder` and `PipWidget.pipBuilder` parameters. They will be removed in v2.0.0. +* Refactor native code method dispatcher to separate methods for each method call. +* Update README.md to include new features and deprecation warning. + +## 1.0.0 + +* Android 14 support by [song011794](https://github.com/song011794): [PR #12](https://github.com/PuntitOwO/simple_pip_mode_flutter/pull/12) +* Add auto enter parameter to `setAutoPipMode` by [af-ffr](https://github.com/af-ffr): [PR #8](https://github.com/PuntitOwO/simple_pip_mode_flutter/pull/8) +* Add `AspectRatio` record type. +* Update README.md to inlcude updated videos of new features. +* Actions code refactor to pass flutter static analysis. +* Dependencies updated. +* Refactor example app. + ## 0.8.0 -* Pip Actions implemented by [Erick Daros](https://github.com/erickdaros): [Issue #5](https://github.com/PuntitOwO/simple_pip_mode_flutter/issues/5). +* Pip Actions implemented by [Erick Daros](https://github.com/erickdaros): [PR #6](https://github.com/PuntitOwO/simple_pip_mode_flutter/pull/6). ## 0.7.1 diff --git a/README.md b/README.md index eaf845a..4c1f5e5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ Big shout out to the original author of this library [@puntitOwO](https://www.gi Provides methods to check feature availability, enter PIP mode, callbacks for mode change and PIP Actions support. -![pip_example](https://user-images.githubusercontent.com/69210614/154329387-bd90ce0b-d563-4173-b2d0-2cbcc62b670c.gif) +[main.webm](https://github.com/user-attachments/assets/8d9dd33d-a008-41af-a663-579c3a9cae7d) + +[auto_enter.webm](https://github.com/user-attachments/assets/9704e36b-351d-440e-b66b-cf6ad1523d92) + +[actions.webm](https://github.com/user-attachments/assets/d171aaf5-2b21-4c49-9269-bd58cb3858b7) # Reasoning behind forking this library @@ -30,16 +34,31 @@ In the `dependencies:` section of your `pubspec.yaml`, add the following line: simple_pip_mode: ``` -# Usage +# Table of contents + +- [Features](#features) +- [Installation](#installation) +- [Table of contents](#table-of-contents) +- [Usage](#usage) + - [Update manifest](#update-manifest) + - [Verify pip support](#verify-pip-support) + - [Entering pip mode](#entering-pip-mode) + - [Setting automatic pip mode](#setting-automatic-pip-mode) + - [Enabling callbacks](#enabling-callbacks) + - [Activity wrapper](#activity-wrapper) + - [Kotlin](#kotlin) + - [Java](#java) + - [Callback helper](#callback-helper) + - [Kotlin](#kotlin-1) + - [Java](#java-1) + - [Using callbacks](#using-callbacks) + - [Using the PIP widget](#using-the-pip-widget) + - [Using PIP Actions](#using-pip-actions) +- [Notes](#notes) + - [Multi-platform apps](#multi-platform-apps) +- [Contribute](#contribute) -This section has example code for the following tasks: -* [Update manifest](#update-manifest) -* [Verify PIP support](#verify-pip-support) -* [Entering PIP mode](#entering-pip-mode) -* [Enabling callbacks](#enabling-callbacks) -* [Using callbacks](#using-callbacks) -* [Using the PIP Widget](#using-the-pip-widget) -* [Using PIP Actions](#using-pip-actions) +# Usage ## Update manifest @@ -90,7 +109,7 @@ This way, when user presses home (or uses home gesture), the app enters PIP mode There's two ways of enabling callbacks: * [Activity wrapper](#activity-wrapper) (Recommended!) -* [Callback helper](#callback-helper) +* [Callback helper](#callback-helper) (The old, manual way) ### Activity wrapper @@ -178,16 +197,18 @@ SimplePip _pip = SimplePip( To use the widget, you need to [enable callbacks](#enabling-callbacks) first. Import `pip_widget.dart` file. -Add a `PipWidget` widget to your tree and give it a `builder` or a `child`, and a `pipBuilder` or a `pipChild`. +Add a `PipWidget` widget to your tree and give it a `child` and a `pipChild`. + +> [!Note] +> `builder` and `pipBuilder` are deprecated. Use a `Builder` as the `child` or `pipChild` instead. + ```dart import 'package:simple_pip_mode/pip_widget.dart'; class MyWidget extends StatelessWidget { Widget build(BuildContext context) { return PipWidget( - builder: (context) => Text('This is built when PIP mode is not active'), - child: Text('This widget is not used because builder is not null'), - //pipBuilder: (context) => Text('This is built when PIP mode is active'), - pipChild: Text('This widget is used because pipBuilder is null'), + child: Text('This is built when PIP mode is not active'), + pipChild: Text('This is built when PIP mode is active'), ); } } @@ -197,7 +218,7 @@ You can also pass callbacks directly to `PipWidget`. ## Using PIP Actions To use PIP actions, you need to specify a `pipLayout` preset on your `PipWidget`. -The current available action layout presets are focused on giving support to media reproduction controls. They are `media`, `media_only_pause` and `media_live`. Those are defined on the `[PipActionsLayout]` enum. +The current available action layout presets are focused on giving support to media reproduction controls. They are `media`, `media_only_pause`, `media_live` and `mediaWithSeek10`. Those are defined on the `[PipActionsLayout]` enum. You can also add a `onPipAction` listener to handle actions callbacks from `PipWidget`. This can be defined on `SimplePip(onPipAction: ...)` too. ```dart @@ -213,14 +234,16 @@ class MyWidget extends StatelessWidget { switch (action) { case PipAction.play: // example: videoPlayerController.play(); - break; case PipAction.pause: // example: videoPlayerController.pause(); - break; case PipAction.next: // example: videoPlayerController.next(); case PipAction.previous: // example: videoPlayerController.previous(); + case PipAction.rewind: + // example: videoPlayerController.seek(-10); + case PipAction.forward: + // example: videoPlayerController.seek(10); default: break; } @@ -248,6 +271,10 @@ Calling `SimplePip` methods on a non-Android device will raise a `MissingPluginE # Contribute -Huge thanks to [Erick Daros](https://github.com/erickdaros) for PIP Actions feature. +Huge thanks to: +* [Erick Daros](https://github.com/erickdaros) for PIP Actions feature. +* [song011794](https://github.com/song011794) for updating the plugin to Android 14. +* [af-ffr](https://github.com/af-ffr) for updating the plugin to add auto enter parameter. +* [kmartins](https://github.com/kmartins) for updating the plugin to add more actions. -I'm currently working on more features, so issues and pull requests are appreciated! +Issues and pull requests are appreciated! diff --git a/android/build.gradle b/android/build.gradle index 1d01687..ac6741e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'cl.puntito.simple_pip_mode' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:8.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,15 +25,16 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdk 34 + namespace = "cl.puntito.simple_pip_mode" compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = JavaVersion.VERSION_17 } sourceSets { @@ -41,10 +42,12 @@ android { } defaultConfig { - minSdkVersion 16 + minSdkVersion 21 } } dependencies { + implementation "androidx.core:core:1.10.1" + implementation 'androidx.annotation:annotation:1.7.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 7fca53d..a2f47b6 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + diff --git a/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelper.kt b/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelper.kt index d9a99ac..d56538b 100644 --- a/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelper.kt +++ b/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelper.kt @@ -7,26 +7,26 @@ import io.flutter.embedding.engine.FlutterEngine open class PipCallbackHelper { - private val CHANNEL = "puntito.simple_pip_mode" - private lateinit var channel: MethodChannel + private val CHANNEL = "puntito.simple_pip_mode" + private lateinit var channel: MethodChannel - fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) - } + fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + } - fun setChannel(channel: MethodChannel) { - this.channel = channel - } + fun setChannel(channel: MethodChannel) { + this.channel = channel + } - fun onPictureInPictureModeChanged(active: Boolean) { - if (active) { - channel.invokeMethod("onPipEntered", null) - } else { - channel.invokeMethod("onPipExited", null) + fun onPictureInPictureModeChanged(active: Boolean) { + if (active) { + channel.invokeMethod("onPipEntered", null) + } else { + channel.invokeMethod("onPipExited", null) + } } - } - fun onPipAction(action: PipAction) { - channel.invokeMethod("onPipAction", action.name.lowercase()) - } + fun onPipAction(action: PipAction) { + channel.invokeMethod("onPipAction", action.name.lowercase()) + } } \ No newline at end of file diff --git a/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelperActivityWrapper.kt b/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelperActivityWrapper.kt index 4bf7feb..6b5ec84 100644 --- a/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelperActivityWrapper.kt +++ b/android/src/main/kotlin/cl/puntito/simple_pip_mode/PipCallbackHelperActivityWrapper.kt @@ -7,16 +7,16 @@ import io.flutter.embedding.engine.FlutterEngine import cl.puntito.simple_pip_mode.PipCallbackHelper -open class PipCallbackHelperActivityWrapper: FlutterActivity() { - private var callbackHelper = PipCallbackHelper() +open class PipCallbackHelperActivityWrapper : FlutterActivity() { + private var callbackHelper = PipCallbackHelper() - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - callbackHelper.configureFlutterEngine(flutterEngine) - } + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + callbackHelper.configureFlutterEngine(flutterEngine) + } - override fun onPictureInPictureModeChanged(active: Boolean, newConfig: Configuration?) { - super.onPictureInPictureModeChanged(active, newConfig) - callbackHelper.onPictureInPictureModeChanged(active) - } + override fun onPictureInPictureModeChanged(active: Boolean, newConfig: Configuration?) { + super.onPictureInPictureModeChanged(active, newConfig) + callbackHelper.onPictureInPictureModeChanged(active) + } } \ No newline at end of file diff --git a/android/src/main/kotlin/cl/puntito/simple_pip_mode/SimplePipModePlugin.kt b/android/src/main/kotlin/cl/puntito/simple_pip_mode/SimplePipModePlugin.kt index b0ee439..b93331b 100644 --- a/android/src/main/kotlin/cl/puntito/simple_pip_mode/SimplePipModePlugin.kt +++ b/android/src/main/kotlin/cl/puntito/simple_pip_mode/SimplePipModePlugin.kt @@ -12,10 +12,13 @@ import android.os.Build import android.util.Rational import androidx.annotation.NonNull import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.RECEIVER_EXPORTED import cl.puntito.simple_pip_mode.Constants.EXTRA_ACTION_TYPE import cl.puntito.simple_pip_mode.Constants.SIMPLE_PIP_ACTION import cl.puntito.simple_pip_mode.actions.PipAction import cl.puntito.simple_pip_mode.actions.PipActionsLayout +import io.flutter.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -24,160 +27,217 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +/** PIP_METHODS enum */ +enum class PIP_METHODS(val methodName: String) { + GET_PLATFORM_VERSION("getPlatformVersion"), + IS_PIP_AVAILABLE("isPipAvailable"), + IS_PIP_ACTIVATED("isPipActivated"), + IS_AUTO_PIP_AVAILABLE("isAutoPipAvailable"), + ENTER_PIP_MODE("enterPipMode"), + SET_PIP_LAYOUT("setPipLayout"), + SET_IS_PLAYING("setIsPlaying"), + SET_AUTO_PIP_MODE("setAutoPipMode"), +} + /** SimplePipModePlugin */ -class SimplePipModePlugin: FlutterPlugin, MethodCallHandler, ActivityAware { - - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private val CHANNEL = "puntito.simple_pip_mode" - private lateinit var channel: MethodChannel - private lateinit var context: Context - private lateinit var activity: Activity - private var actions: MutableList = mutableListOf() - private var actionsLayout: PipActionsLayout = PipActionsLayout.NONE - - private var callbackHelper = PipCallbackHelper() - private var params: PictureInPictureParams.Builder? = null - private lateinit var broadcastReceiver: BroadcastReceiver - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL) - callbackHelper.setChannel(channel) - channel.setMethodCallHandler(this) - context = flutterPluginBinding.applicationContext - broadcastReceiver = object : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.O) - override fun onReceive(context: Context, intent: Intent) { - if (SIMPLE_PIP_ACTION !== intent.action) { - return - } - intent.getStringExtra(EXTRA_ACTION_TYPE)?.let { - val action = PipAction.valueOf(it) - action.afterAction()?.let { - toggleAction(action) - } - callbackHelper.onPipAction(action) - } - } - }.also { broadcastReceiver = it } - context.registerReceiver(broadcastReceiver, IntentFilter(SIMPLE_PIP_ACTION)) - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - context.unregisterReceiver(broadcastReceiver) - } - - @RequiresApi(Build.VERSION_CODES.O) - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${Build.VERSION.RELEASE}") - } else if (call.method == "isPipAvailable") { - result.success( - activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) - ) - } else if (call.method == "isPipActivated") { - result.success(activity.isInPictureInPictureMode) - } else if (call.method == "isAutoPipAvailable") { - result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - } else if (call.method == "enterPipMode") { - val aspectRatio = call.argument>("aspectRatio") - val autoEnter = call.argument("autoEnter") - val seamlessResize = call.argument("seamlessResize") - var params = PictureInPictureParams.Builder() - .setAspectRatio(Rational(aspectRatio!![0], aspectRatio[1])) - .setActions(actions) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - params = params.setAutoEnterEnabled(autoEnter!!) - .setSeamlessResizeEnabled(seamlessResize!!) - } - - this.params = params - - result.success( - activity.enterPictureInPictureMode(params.build()) - ) - } else if (call.method == "setPipLayout") { - val success = call.argument("layout")?.let { - try { - actionsLayout = PipActionsLayout.valueOf(it.uppercase()) - actions = actionsLayout.remoteActions(context) - true - } catch(e: Exception) { - false - } - } ?: false - result.success(success) - } else if (call.method == "setIsPlaying") { - call.argument("isPlaying")?.let { isPlaying -> - if (actionsLayout.actions.contains(PipAction.PLAY) || - actionsLayout.actions.contains(PipAction.PAUSE)) { - var i = actionsLayout.actions.indexOf(PipAction.PLAY) - if (i == -1) { - i = actionsLayout.actions.indexOf(PipAction.PAUSE) - } - if( i >= 0) { - actionsLayout.actions[i] = if(isPlaying) PipAction.PAUSE else PipAction.PLAY - renderPipActions() - result.success(true) - } - } else { - result.success(false) +class SimplePipModePlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private val CHANNEL = "puntito.simple_pip_mode" + private lateinit var channel: MethodChannel + private lateinit var context: Context + private lateinit var activity: Activity + private var actions: MutableList = mutableListOf() + private var actionsLayout: PipActionsLayout = PipActionsLayout.NONE + + private var callbackHelper = PipCallbackHelper() + private var params: PictureInPictureParams.Builder? = null + private lateinit var broadcastReceiver: BroadcastReceiver + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL) + callbackHelper.setChannel(channel) + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + broadcastReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.O) + override fun onReceive(context: Context, intent: Intent) { + if (SIMPLE_PIP_ACTION !== intent.action) { + return + } + intent.getStringExtra(EXTRA_ACTION_TYPE)?.let { + val action = PipAction.valueOf(it) + action.afterAction()?.let { + toggleAction(action) + } + callbackHelper.onPipAction(action) + } + } + }.also { broadcastReceiver = it } + + ContextCompat.registerReceiver( + context, + broadcastReceiver, + IntentFilter(SIMPLE_PIP_ACTION), + RECEIVER_EXPORTED + ) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + context.unregisterReceiver(broadcastReceiver) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + PIP_METHODS.GET_PLATFORM_VERSION.methodName -> getPlatformVersion(result) + PIP_METHODS.IS_PIP_AVAILABLE.methodName -> isPipAvailable(result) + PIP_METHODS.IS_PIP_ACTIVATED.methodName -> isPipActivated(result) + PIP_METHODS.IS_AUTO_PIP_AVAILABLE.methodName -> isAutoPipAvailable(result) + PIP_METHODS.ENTER_PIP_MODE.methodName -> enterPipMode(call, result) + PIP_METHODS.SET_PIP_LAYOUT.methodName -> setPipLayout(call, result) + PIP_METHODS.SET_IS_PLAYING.methodName -> setIsPlaying(call, result) + PIP_METHODS.SET_AUTO_PIP_MODE.methodName -> setAutoPipMode(call, result) + else -> result.notImplemented() } - } ?: result.success(false) - } else if (call.method == "setAutoPipMode") { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivity() { + } + + /* METHOD IMPLEMENTATION */ + + private fun getPlatformVersion(result: MethodChannel.Result) { + result.success("Android ${Build.VERSION.RELEASE}") + } + + private fun isPipAvailable(result: MethodChannel.Result) { + result.success( + activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + ) + } + + private fun isPipActivated(result: MethodChannel.Result) { + result.success(activity.isInPictureInPictureMode) + } + + private fun isAutoPipAvailable(result: MethodChannel.Result) { + result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + } + + private fun enterPipMode(call: MethodCall, result: MethodChannel.Result) { val aspectRatio = call.argument>("aspectRatio") val autoEnter = call.argument("autoEnter") val seamlessResize = call.argument("seamlessResize") - val params = PictureInPictureParams.Builder() - .setAspectRatio(Rational(aspectRatio!![0], aspectRatio[1])) - .setAutoEnterEnabled(autoEnter!!) - .setSeamlessResizeEnabled(seamlessResize!!) - .setActions(actions) + var params = PictureInPictureParams.Builder() + .setAspectRatio(Rational(aspectRatio!![0], aspectRatio[1])) + .setActions(actions) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + params = params.setAutoEnterEnabled(autoEnter!!) + .setSeamlessResizeEnabled(seamlessResize!!) + } this.params = params - activity.setPictureInPictureParams(params.build()) + result.success( + activity.enterPictureInPictureMode(params.build()) + ) + } + + private fun setPipLayout(call: MethodCall, result: MethodChannel.Result) { + val success = call.argument("layout")?.let { + try { + Log.i("PIP", "layout = ${convertAction(it)}") + actionsLayout = PipActionsLayout.valueOf(convertAction(it)) + actions = actionsLayout.remoteActions(context) + true + } catch (e: Exception) { + Log.e("PIP", e.message?: "Error setting layout") + false + } + } ?: false + result.success(success) + } - result.success(true) - } else { - result.error("NotImplemented", "System Version less than Android S found", "Expected Android S or newer.") - } - } else { - result.notImplemented() + private fun setIsPlaying(call: MethodCall, result: MethodChannel.Result) { + call.argument("isPlaying")?.let { isPlaying -> + if (actionsLayout.actions.contains(PipAction.PLAY) || + actionsLayout.actions.contains(PipAction.PAUSE) + ) { + var i = actionsLayout.actions.indexOf(PipAction.PLAY) + if (i == -1) { + i = actionsLayout.actions.indexOf(PipAction.PAUSE) + } + if (i >= 0) { + actionsLayout.actions[i] = + if (isPlaying) PipAction.PAUSE else PipAction.PLAY + renderPipActions() + result.success(true) + } + } else { + result.success(false) + } + } ?: result.success(false) } - } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - } + private fun setAutoPipMode(call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val aspectRatio = call.argument>("aspectRatio") + val autoEnter = call.argument("autoEnter") + val seamlessResize = call.argument("seamlessResize") + val params = PictureInPictureParams.Builder() + .setAspectRatio(Rational(aspectRatio!![0], aspectRatio[1])) + .setAutoEnterEnabled(autoEnter!!) + .setSeamlessResizeEnabled(seamlessResize!!) + .setActions(actions) - override fun onDetachedFromActivityForConfigChanges() { - } + this.params = params - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - } + activity.setPictureInPictureParams(params.build()) - override fun onDetachedFromActivity() { - } + result.success(true) + } else { + result.error( + "NotImplemented", + "System Version less than Android S found", + "Expected Android S or newer." + ) + } + } - @RequiresApi(Build.VERSION_CODES.O) - private fun toggleAction(action: PipAction) { - actionsLayout.toggleToAfterAction(action) - renderPipActions() - } + @RequiresApi(Build.VERSION_CODES.O) + private fun toggleAction(action: PipAction) { + actionsLayout.toggleToAfterAction(action) + renderPipActions() + } - @RequiresApi(Build.VERSION_CODES.O) - private fun renderPipActions() { - actions = PipActionsLayout.remoteActions(context, actionsLayout.actions) - params?.let { - it.setActions(actions).build() - activity.setPictureInPictureParams(it.build()) + @RequiresApi(Build.VERSION_CODES.O) + private fun renderPipActions() { + actions = PipActionsLayout.remoteActions(context, actionsLayout.actions) + params?.let { + it.setActions(actions).build() + activity.setPictureInPictureParams(it.build()) + } } - } + + private fun convertAction(action: String) = action + .replace(Regex("([a-z])([A-Z])"), "$1_$2") + .replace(Regex("([a-z])([0-9])"), "$1_$2") + .uppercase() } diff --git a/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipAction.kt b/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipAction.kt index 3b9cf77..0104e50 100644 --- a/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipAction.kt +++ b/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipAction.kt @@ -21,7 +21,9 @@ enum class PipAction( PAUSE(R.drawable.ic_baseline_pause_24, R.string.pip_action_pause, R.string.pip_action_pause_description, "PLAY"), NEXT(R.drawable.ic_baseline_skip_next_24, R.string.pip_action_next, R.string.pip_action_next_description), PREVIOUS(R.drawable.ic_baseline_skip_previous_24, R.string.pip_action_previous, R.string.pip_action_previous_description), - LIVE(R.drawable.ic_surround_sound_24, R.string.pip_action_live, R.string.pip_action_live_description,); + LIVE(R.drawable.ic_surround_sound_24, R.string.pip_action_live, R.string.pip_action_live_description,), + REWIND(R.drawable.ic_baseline_replay_10_24, R.string.pip_action_rewind_10, R.string.pip_action_rewind_10_description), + FORWARD(R.drawable.ic_baseline_forward_10_24, R.string.pip_action_forward_10, R.string.pip_action_forward_10_description); @RequiresApi(Build.VERSION_CODES.O) fun toRemoteAction(context: Context) : RemoteAction = RemoteAction( diff --git a/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipActionsLayout.kt b/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipActionsLayout.kt index 10e6c39..c59b4be 100644 --- a/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipActionsLayout.kt +++ b/android/src/main/kotlin/cl/puntito/simple_pip_mode/actions/PipActionsLayout.kt @@ -6,12 +6,13 @@ import android.os.Build import androidx.annotation.RequiresApi enum class PipActionsLayout( - var actions: MutableList + var actions: MutableList, ) { NONE(mutableListOf()), MEDIA(mutableListOf(PipAction.PREVIOUS, PipAction.PAUSE, PipAction.NEXT)), MEDIA_ONLY_PAUSE(mutableListOf(PipAction.PAUSE)), - MEDIA_LIVE(mutableListOf(PipAction.LIVE, PipAction.PAUSE)); + MEDIA_LIVE(mutableListOf(PipAction.LIVE, PipAction.PAUSE)), + MEDIA_WITH_SEEK_10(mutableListOf(PipAction.REWIND,PipAction.PAUSE, PipAction.FORWARD)); @RequiresApi(Build.VERSION_CODES.O) fun remoteActions(context: Context): MutableList = diff --git a/android/src/main/res/drawable/ic_baseline_forward_10_24.xml b/android/src/main/res/drawable/ic_baseline_forward_10_24.xml new file mode 100644 index 0000000..9170369 --- /dev/null +++ b/android/src/main/res/drawable/ic_baseline_forward_10_24.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/res/drawable/ic_baseline_replay_10_24.xml b/android/src/main/res/drawable/ic_baseline_replay_10_24.xml new file mode 100644 index 0000000..18d01da --- /dev/null +++ b/android/src/main/res/drawable/ic_baseline_replay_10_24.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 6a55567..467ddd3 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -11,5 +11,9 @@ Back to previous media Live Go to live view + Rewind + Rewind 10 + Forward 10 + Rewind 10 \ No newline at end of file diff --git a/example/README.md b/example/README.md index 24d5770..242d7ee 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,3 @@ # simple_pip_mode_example Demonstrates how to use the simple_pip_mode plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index a9ebb50..c106f76 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,12 +22,9 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion flutter.compileSdkVersion + namespace 'cl.puntito.simple_pip_mode_example' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -42,8 +40,8 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "cl.puntito.simple_pip_mode_example" + namespace 'cl.puntito.simple_pip_mode_example' minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -62,7 +60,3 @@ android { flutter { source '../..' } - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index dd8d92c..f32ea6e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + properties.load(reader) } +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.3.2" apply false + id "org.jetbrains.kotlin.android" version "1.8.0" apply false +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ':app' diff --git a/example/lib/main.dart b/example/lib/main.dart index 3095df0..83f2cb7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,18 +1,20 @@ -import 'package:flutter/material.dart'; +// ignore_for_file: sort_child_properties_last + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide AspectRatio; import 'package:simple_pip_mode/actions/pip_action.dart'; import 'package:simple_pip_mode/actions/pip_actions_layout.dart'; -import 'dart:async'; - -import 'package:simple_pip_mode/simple_pip.dart'; // To enter pip mode and receive callbacks +import 'package:simple_pip_mode/aspect_ratio.dart'; import 'package:simple_pip_mode/pip_widget.dart'; // To build pip mode dependent layouts +import 'package:simple_pip_mode/simple_pip.dart'; // To enter pip mode and receive callbacks /// Some aspect ratio presets to choose const aspectRatios = [ - [1, 1], - [2, 3], - [3, 2], - [16, 9], - [9, 16], + (1, 1), + (2, 3), + (3, 2), + (16, 9), + (9, 16), ]; void main() { @@ -23,7 +25,7 @@ void main() { /// Example App to show usage of PIP mode class ExampleApp extends StatefulWidget { - const ExampleApp({Key? key}) : super(key: key); + const ExampleApp({super.key}); @override State createState() => _ExampleAppState(); @@ -31,7 +33,7 @@ class ExampleApp extends StatefulWidget { class _ExampleAppState extends State { bool pipAvailable = false; - List aspectRatio = aspectRatios.first; + AspectRatio aspectRatio = aspectRatios.first; bool autoPipAvailable = false; bool autoPipSwitch = false; late SimplePip pip; @@ -59,12 +61,25 @@ class _ExampleAppState extends State { }); } - List> layoutList() { + /// List of available layouts + List> get layoutList { return PipActionsLayout.values .map>( - (PipActionsLayout value) => DropdownMenuItem( - child: Text(value.name), - value: value, + (PipActionsLayout layout) => DropdownMenuItem( + value: layout, + child: Text(layout.name), + ), + ) + .toList(); + } + + /// List of available aspect ratio presets + List> get aspectRatioList { + return aspectRatios + .map>( + (AspectRatio ratio) => DropdownMenuItem( + value: ratio, + child: Text(ratio.name), ), ) .toList(); @@ -77,78 +92,37 @@ class _ExampleAppState extends State { home: PipWidget( // builder is null so child is used when not in pip mode pipLayout: pipActionsLayout, - onPipAction: (action) { - print("PIP ACTION TAP: " + action.name); - switch (action) { - case PipAction.play: - // example: videoPlayerController.play(); - setState(() { - isPlaying = true; - actionResponse = "Playing"; - }); - break; - case PipAction.pause: - // example: videoPlayerController.pause(); - setState(() { - isPlaying = false; - actionResponse = "Paused"; - }); - break; - case PipAction.live: - // example: videoPlayerController.forceLive(); - setState(() { - actionResponse = "Go to live view"; - }); - break; - case PipAction.next: - // example: videoPlayerController.next(); - setState(() { - actionResponse = "Next"; - }); - break; - case PipAction.previous: - // example: videoPlayerController.previous(); - setState(() { - actionResponse = "Previous"; - }); - break; - default: - break; - } - }, + onPipAction: _handlePipAction, child: Scaffold( - appBar: AppBar( - title: const Text('Pip Plugin example app'), - ), + appBar: AppBar(title: const Text('PiP Plugin example app')), body: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(width: double.infinity), - Text('Pip is ${pipAvailable ? '' : 'not '}Available'), - const Text('Pip is not activated'), - DropdownButton>( - value: aspectRatio, - onChanged: (List? newValue) { - if (newValue == null) return; - if (autoPipSwitch) { - pip.setAutoPipMode( - aspectRatio: newValue, - seamlessResize: true, - ); - } - setState(() { - aspectRatio = newValue; - }); - }, - items: aspectRatios - .map>>( - (List value) => DropdownMenuItem>( - child: Text('${value.first} : ${value.last}'), - value: value, - ), - ) - .toList(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('PiP Available: '), + Icon(pipAvailable ? Icons.check : Icons.close), + ], + ), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('PiP Activated: '), + Icon(Icons.close), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Aspect ratio: '), + DropdownButton( + value: aspectRatio, + onChanged: _handleAspectRatioSelection, + items: aspectRatioList, + ), + ], ), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -156,29 +130,17 @@ class _ExampleAppState extends State { const Text('Auto Enter (Android S): '), Switch( value: autoPipSwitch, - onChanged: autoPipAvailable - ? (newValue) { - setState(() { - autoPipSwitch = newValue; - }); - } - : null, + onChanged: autoPipAvailable ? _handleAutoSwitch : null, ), ], ), IconButton( - onPressed: pipAvailable - ? () => pip.enterPipMode( - aspectRatio: aspectRatio, - ) - : null, + onPressed: pipAvailable ? _handleEnterPip : null, icon: const Icon(Icons.picture_in_picture), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), - child: Divider( - thickness: 1, - ), + child: Divider(thickness: 1), ), const Text("PIP Actions:"), Padding( @@ -189,82 +151,136 @@ class _ExampleAppState extends State { const Text("Current actions layout: "), DropdownButton( value: pipActionsLayout, - onChanged: (PipActionsLayout? newValue) { - if (newValue == null) return; - pip.setPipActionsLayout(newValue); - pip.setIsPlaying(true); - setState(() { - isPlaying = true; - pipActionsLayout = newValue; - }); - }, - items: layoutList(), + onChanged: _handlePipActionsLayoutSelection, + items: layoutList, ), ], ), ), - pipActionsLayout != PipActionsLayout.none - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const Text("Simulated player: "), - IconButton( - onPressed: () { - bool newValue = !isPlaying; - pip.setIsPlaying(newValue); - setState(() { - isPlaying = newValue; - actionResponse = ""; - }); - }, - icon: Icon( - isPlaying ? Icons.pause : Icons.play_arrow)) - ], + if (pipActionsLayout != PipActionsLayout.none) + Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const Text("Simulated player: "), + IconButton( + onPressed: () { + bool newValue = !isPlaying; + pip.setIsPlaying(newValue); + setState(() { + isPlaying = newValue; + actionResponse = ""; + }); + }, + isSelected: isPlaying, + icon: const Icon(Icons.play_arrow), + selectedIcon: const Icon(Icons.pause), ), + ], + ), + ), + const Padding( + padding: EdgeInsets.all(16), + child: Text( + "Obs.: " + "Tap the simulated player button to see the PIP " + "actions be updated on PIP mode, when you tap PIP " + "actions on PIP mode it will reflect here too", ), - const Padding( - padding: EdgeInsets.all(16), - child: Text("Obs.: Tap the simulated player button to see the PIP actions be updated on PIP mode, when you tap PIP actions on PIP mode it will reflect here too"), - ) - ], - ) - : Container(), + ), + ], + ), ], ), ), // pip builder is null so pip child is used when in pip mode pipChild: Scaffold( - appBar: AppBar( - title: const Text('Pip Mode'), - ), + appBar: AppBar(title: const Text('Pip Mode')), body: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(width: double.infinity), - const Text('Pip activated'), - pipActionsLayout != PipActionsLayout.none - ? IconButton( - onPressed: () { - bool newValue = !isPlaying; - pip.setIsPlaying(newValue); - setState(() { - isPlaying = newValue; - }); - }, - icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow)) - : Container(), - pipActionsLayout != PipActionsLayout.none - ? Text(actionResponse) - : Container(), + const SizedBox(width: double.maxFinite), + const Text('PiP activated'), + if (pipActionsLayout != PipActionsLayout.none) ...[ + Icon(isPlaying ? Icons.pause : Icons.play_arrow), + Text(actionResponse), + ] ], ), ), ), ); } + + void _handlePipActionsLayoutSelection(PipActionsLayout? newValue) { + if (newValue == null) return; + pip.setPipActionsLayout(newValue); + pip.setIsPlaying(true); + setState(() { + isPlaying = true; + pipActionsLayout = newValue; + }); + } + + void _handleEnterPip() => pip.enterPipMode( + aspectRatio: aspectRatio, + autoEnter: autoPipSwitch, + seamlessResize: autoPipSwitch, + ); + + void _handleAutoSwitch(newValue) { + pip.setAutoPipMode( + aspectRatio: aspectRatio, + autoEnter: newValue, + seamlessResize: newValue, + ); + setState(() => autoPipSwitch = newValue); + } + + void _handleAspectRatioSelection(AspectRatio? newValue) { + if (newValue == null) return; + pip.setAutoPipMode( + aspectRatio: newValue, + autoEnter: autoPipSwitch, + seamlessResize: autoPipSwitch, + ); + setState(() => aspectRatio = newValue); + } + + void _handlePipAction(PipAction action) { + if (kDebugMode) print("PIP ACTION TAP: ${action.name}"); + switch (action) { + case PipAction.play: + // example: videoPlayerController.play(); + setState(() { + isPlaying = true; + actionResponse = "Playing"; + }); + case PipAction.pause: + // example: videoPlayerController.pause(); + setState(() { + isPlaying = false; + actionResponse = "Paused"; + }); + case PipAction.live: + // example: videoPlayerController.forceLive(); + setState(() => actionResponse = "Go to live view"); + case PipAction.next: + // example: videoPlayerController.next(); + setState(() => actionResponse = "Next"); + case PipAction.previous: + // example: videoPlayerController.previous(); + setState(() => actionResponse = "Previous"); + case PipAction.rewind: + // example: videoPlayerController.seek(-10); + setState(() => actionResponse = "Rewind"); + case PipAction.forward: + // example: videoPlayerController.seek(10); + setState(() => actionResponse = "Forward"); + } + } } diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..a3fc743 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,220 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + simple_pip_mode: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 985fc4d..e36d9cc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,19 +1,11 @@ name: simple_pip_mode_example description: Demonstrates how to use the simple_pip_mode plugin. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.18.0 <3.0.0" + sdk: "^3.0.0" -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -34,51 +26,8 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^1.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec + flutter_lints: ^4.0.0 # The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/actions/pip_action.dart b/lib/actions/pip_action.dart index 51e1f4f..53cb852 100644 --- a/lib/actions/pip_action.dart +++ b/lib/actions/pip_action.dart @@ -1,24 +1,24 @@ /// PIP Action preset -/// -/// This actions are defined on a ENUM inside Android src, where is +/// +/// This actions are defined on a ENUM inside Android src, where is /// specified each action drawable, name, description and a [afterAction] /// that shows after the action is tap. Ex.: play's after action is pause /// and vice-versa. -/// -/// TODO: Create implement generic actions on runtime, so plugin users can -/// create theirs own actions without needing to update this preset -/// -/// +/// +/// /// [play] Play action represented by triangle play icon /// [pause] Pause action represented by double vertical bars pause icon /// [previous] Previous action represented by previous icon /// [next] Next action represented by next icon /// [live] Live action (force player seeker to show latest content) represented by sorround icon -/// enum PipAction { play, pause, previous, next, - live -} \ No newline at end of file + live, + rewind, + forward, +} + +// TODO(PuntitOwO): Create implement generic actions on runtime, so plugin users can create theirs own actions without needing to update this preset diff --git a/lib/actions/pip_actions_layout.dart b/lib/actions/pip_actions_layout.dart index f4f0810..ea0b26d 100644 --- a/lib/actions/pip_actions_layout.dart +++ b/lib/actions/pip_actions_layout.dart @@ -1,20 +1,20 @@ /// PIP Actions Layout preset -/// -/// This layouts are defined on a ENUM inside Android src, where is +/// +/// This layouts are defined on a ENUM inside Android src, where is /// specified the actions each layout should show. -/// -/// TODO: Implement generic layouts on runtime, so plugin users can -/// create theirs own layouts without needing to update this preset -/// -/// +/// +/// /// [none] do not show any actions on PIP mode /// [media] shows `previous`, `pause/play`, `next` actions (on this specific order) -/// [media_only_pause] shows only `pause/play` action -/// [media_live] shows `live` and `pause/play` actions (on this specific order) -/// +/// [mediaOnlyPause] shows only `pause/play` action +/// [mediaLive] shows `live` and `pause/play` actions (on this specific order) +/// [mediaWithSeek10] shows `previous`, `pause/play`, `next`, `seek10` actions (on this specific order) enum PipActionsLayout { none, media, - media_only_pause, - media_live -} \ No newline at end of file + mediaOnlyPause, + mediaLive, + mediaWithSeek10, +} + +// TODO(PuntitOwO): Implement generic layouts on runtime, so plugin users can create theirs own layouts without needing to update this preset diff --git a/lib/aspect_ratio.dart b/lib/aspect_ratio.dart new file mode 100644 index 0000000..7be6844 --- /dev/null +++ b/lib/aspect_ratio.dart @@ -0,0 +1,17 @@ +/// AspectRatio type to represent the width to height ratio. +typedef AspectRatio = (int, int); + +/// Extension to get the width, height, name, and list form of an AspectRatio. +extension AspectRatioExtension on AspectRatio { + /// Width of the aspect ratio. + int get width => this.$1; + + /// Height of the aspect ratio. + int get height => this.$2; + + /// Aspect ratio in a human readable form. + String get name => '$width:$height'; + + /// Aspect ratio as a list. + List get asList => [width, height]; +} diff --git a/lib/pip_widget.dart b/lib/pip_widget.dart index a822117..7a77c9b 100644 --- a/lib/pip_widget.dart +++ b/lib/pip_widget.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:simple_pip_mode/actions/pip_action.dart'; import 'package:simple_pip_mode/actions/pip_actions_layout.dart'; @@ -22,13 +24,21 @@ class PipWidget extends StatefulWidget { final VoidCallback? onPipEntered; final VoidCallback? onPipExited; final Function(PipAction)? onPipAction; + @Deprecated( + 'Use a Builder widget as the child instead. ' + 'This field will be removed in v2.0.0.', + ) final Widget Function(BuildContext)? builder; final Widget? child; + @Deprecated( + 'Use a Builder widget as the pipChild instead. ' + 'This field will be removed in v2.0.0.', + ) final Widget Function(BuildContext)? pipBuilder; final Widget? pipChild; final PipActionsLayout pipLayout; const PipWidget({ - Key? key, + super.key, this.onPipEntered, this.onPipExited, this.onPipAction, @@ -36,10 +46,9 @@ class PipWidget extends StatefulWidget { this.child, this.pipBuilder, this.pipChild, - this.pipLayout = PipActionsLayout.none + this.pipLayout = PipActionsLayout.none, }) : assert(child != null || builder != null), - assert(pipChild != null || pipBuilder != null), - super(key: key); + assert(pipChild != null || pipBuilder != null); @override PipWidgetState createState() => PipWidgetState(); @@ -52,42 +61,41 @@ class PipWidgetState extends State { /// Whether the app is currently in PIP mode bool _pipMode = false; + Widget? get builder => + widget.builder != null ? Builder(builder: widget.builder!) : null; + Widget? get pipBuilder => + widget.pipBuilder != null ? Builder(builder: widget.pipBuilder!) : null; + @override void initState() { super.initState(); pip = SimplePip( onPipEntered: onPipEntered, onPipExited: onPipExited, - onPipAction: onPipAction + onPipAction: onPipAction, ); pip.setPipActionsLayout(widget.pipLayout); } /// The app entered PIP mode void onPipEntered() { - setState(() { - _pipMode = true; - }); + setState(() => _pipMode = true); widget.onPipEntered?.call(); } /// The app exited PIP mode void onPipExited() { - setState(() { - _pipMode = false; - }); + setState(() => _pipMode = false); widget.onPipExited?.call(); } - /// The user taps one PIP action - void onPipAction(PipAction action) { - widget.onPipAction?.call(action); - } + /// The user taps one PIP action + void onPipAction(PipAction action) => widget.onPipAction?.call(action); @override Widget build(BuildContext context) { return _pipMode - ? (widget.pipBuilder?.call(context) ?? widget.pipChild!) - : (widget.builder?.call(context) ?? widget.child!); + ? (pipBuilder ?? widget.pipChild!) + : (builder ?? widget.child!); } } diff --git a/lib/simple_pip.dart b/lib/simple_pip.dart index 79ce672..89e604a 100644 --- a/lib/simple_pip.dart +++ b/lib/simple_pip.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:simple_pip_mode/actions/pip_action.dart'; import 'package:simple_pip_mode/actions/pip_actions_layout.dart'; +import 'package:simple_pip_mode/aspect_ratio.dart'; /// Main controller class. /// It can verify whether the system supports PIP, @@ -10,8 +11,7 @@ import 'package:simple_pip_mode/actions/pip_actions_layout.dart'; /// request entering PIP mode, /// and call some callbacks when the app changes its mode. class SimplePip { - static const MethodChannel _channel = - MethodChannel('puntito.simple_pip_mode'); + static const _channel = MethodChannel('puntito.simple_pip_mode'); /// Whether this device supports PIP mode. static Future get isPipAvailable async { @@ -38,16 +38,16 @@ class SimplePip { VoidCallback? onPipExited; /// Called when the user taps on a PIP action - Function(PipAction)? onPipAction; + void Function(PipAction)? onPipAction; /// Request entering PIP mode Future enterPipMode({ - aspectRatio = const [16, 9], - autoEnter = false, - seamlessResize = false, + AspectRatio aspectRatio = const (16, 9), + bool autoEnter = false, + bool seamlessResize = false, }) async { Map params = { - 'aspectRatio': aspectRatio, + 'aspectRatio': aspectRatio.asList, 'autoEnter': autoEnter, 'seamlessResize': seamlessResize, }; @@ -59,12 +59,12 @@ class SimplePip { /// Request setting automatic PIP mode. /// Android 12 (Android S, API level 31) or newer required. Future setAutoPipMode({ - aspectRatio = const [16, 9], - seamlessResize = false, - autoEnter = true, + AspectRatio aspectRatio = const (16, 9), + bool seamlessResize = false, + bool autoEnter = true, }) async { Map params = { - 'aspectRatio': aspectRatio, + 'aspectRatio': aspectRatio.asList, 'autoEnter': autoEnter, 'seamlessResize': seamlessResize, }; @@ -108,16 +108,13 @@ class SimplePip { switch (call.method) { case 'onPipEntered': onPipEntered?.call(); - break; case 'onPipExited': onPipExited?.call(); - break; case 'onPipAction': String arg = call.arguments; PipAction action = PipAction.values.firstWhere((e) => e.name == arg); onPipAction?.call(action); - break; } }, ); diff --git a/pubspec.yaml b/pubspec.yaml index 0157b98..cf0f05a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: simple_pip_mode description: A complete Picture-In-Picutre mode plugin (Android support only) -version: 0.8.0 +version: 1.1.0 repository: https://github.com/PuntitOwO/simple_pip_mode_flutter environment: - sdk: ">=2.16.0 <3.0.0" - flutter: ">=2.5.0" + sdk: "^3.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -14,7 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/pip_widget_test.dart b/test/pip_widget_test.dart index ab4a4fc..6c96306 100644 --- a/test/pip_widget_test.dart +++ b/test/pip_widget_test.dart @@ -16,16 +16,16 @@ void main() { MaterialApp( home: PipWidget( builder: (_) => const Text('builder'), - child: const Text('child'), pipBuilder: (_) => const Text('pipbuilder'), pipChild: const Text('pipchild'), + child: const Text('child'), ), ), ); final PipWidgetState state = tester.state(find.byType(PipWidget)); SimplePip pip = state.pip; - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { switch (methodCall.method) { case 'enterPipMode': @@ -68,15 +68,15 @@ void main() { await tester.pumpWidget( const MaterialApp( home: PipWidget( - child: Text('child'), pipChild: Text('pipchild'), + child: Text('child'), ), ), ); final PipWidgetState state = tester.state(find.byType(PipWidget)); SimplePip pip = state.pip; - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { switch (methodCall.method) { case 'enterPipMode': diff --git a/test/simple_pip_test.dart b/test/simple_pip_test.dart index 17ce4aa..1749f44 100644 --- a/test/simple_pip_test.dart +++ b/test/simple_pip_test.dart @@ -18,7 +18,7 @@ void main() { setUp(() { enterCallbackCalled = false; exitCallbackCalled = false; - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, (MethodCall methodCall) async { switch (methodCall.method) { case 'isPipAvailable': @@ -46,7 +46,7 @@ void main() { }); tearDown(() { - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); });