Explorar o código

控制器UI初版完成
增加了横向和纵向拖动的手势

Caijinglong %!s(int64=6) %!d(string=hai) anos
pai
achega
f235119d51

+ 3 - 0
android/src/main/java/top/kikt/ijkplayer/Ijk.kt

@@ -109,6 +109,9 @@ class Ijk(private val registry: PluginRegistry.Registrar) : MethodChannel.Method
                 val volume = call.argument<Int>("volume")
                 setVolume(volume)
                 result?.success(true)
+            }
+            "getVolume"->{
+
             }
             else -> {
                 result?.notImplemented()

+ 12 - 2
android/src/main/java/top/kikt/ijkplayer/IjkplayerPlugin.kt

@@ -47,13 +47,20 @@ class IjkplayerPlugin(private val registrar: Registrar) : MethodCallHandler {
                 }
                 result.success(true)
             }
+            "getSystemVolume" -> {
+                val volume = getSystemVolume()
+                result.success(volume)
+            }
             else -> result.notImplemented()
         }
     }
 
+    private fun getSystemVolume(): Int {
+        return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+    }
+
     private fun setVolume(volume: Int) {
-        val am = registrar.activity().getSystemService(Context.AUDIO_SERVICE) as AudioManager?
-        am?.apply {
+        audioManager.apply {
             val max = getStreamMaxVolume(AudioManager.STREAM_MUSIC)
             val min = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                 getStreamMinVolume(AudioManager.STREAM_MUSIC)
@@ -66,6 +73,9 @@ class IjkplayerPlugin(private val registrar: Registrar) : MethodCallHandler {
         }
     }
 
+    private val audioManager: AudioManager
+        get() = registrar.activity().getSystemService(Context.AUDIO_SERVICE) as AudioManager
+
     fun MethodCall.getLongArg(key: String): Long {
         return this.argument<Int>(key)!!.toLong()
     }

+ 1 - 1
example/lib/main.dart

@@ -7,7 +7,7 @@ import 'package:photo/photo.dart';
 import 'package:photo_manager/photo_manager.dart';
 
 void main() {
-  initIJKPlayer();
+  IjkManager.initIJKPlayer();
   runApp(MyApp());
 }
 

+ 32 - 8
ios/Classes/IjkplayerPlugin.m

@@ -1,3 +1,4 @@
+#import <AVKit/AVKit.h>
 #import "IjkplayerPlugin.h"
 #import "CoolFlutterIjkManager.h"
 #import "CoolFlutterIJK.h"
@@ -62,23 +63,46 @@ static IjkplayerPlugin *__sharedInstance;
             int id = [params[@"id"] intValue];
             [self->manager disposeWithId:id];
             result(@(YES));
-        } else if ([@"setSystemVolume" isEqualToString:call.method]) {
-            NSDictionary *params = [call arguments];
-            int volume = [params[@"setSystemVolume"] intValue];
-            [self setVolume:volume];
-            result(@YES);
         } else if ([@"init" isEqualToString:call.method]) {
             [self->manager disposeAll];
             result(@YES);
+        } else if ([@"setSystemVolume" isEqualToString:call.method]) {
+            NSDictionary *params = [call arguments];
+            int volume = [params[@"volume"] intValue];
+            [self setSystemVolume:volume];
+            result(@YES);
+        } else if ([@"getSystemVolume" isEqualToString:call.method]) {
+            AVAudioSession *audioSession = [AVAudioSession sharedInstance];
+            CGFloat currentVol = audioSession.outputVolume * 100;
+            result(@((int) currentVol));
         } else {
             result(FlutterMethodNotImplemented);
         }
     });
 }
 
-- (void)setVolume:(int)volume {
-    MPMusicPlayerController *mpc = [MPMusicPlayerController applicationMusicPlayer];
-    mpc.volume = volume / 100;
+- (void)setSystemVolume:(int)volume {
+    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
+    UISlider *volumeViewSlider = nil;
+    for (UIView *view in [volumeView subviews]) {
+        if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
+            volumeViewSlider = (UISlider *) view;
+            break;
+        }
+    }
+
+    float targetVolume = ((float) volume) / 100;
+
+    volumeView.frame = CGRectMake(-1000, -1000, 100, 100);
+
+    UIWindow *window = UIApplication.sharedApplication.keyWindow;
+    [window addSubview:volumeView];
+
+    // change system volume, the value is between 0.0f and 1.0f
+    [volumeViewSlider setValue:targetVolume animated:NO];
+
+    // send UI control event to make the change effect right now. 立即生效
+    [volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
 }
 
 @end

+ 1 - 1
lib/flutter_ijkplayer.dart

@@ -1,4 +1,4 @@
 export 'src/error.dart';
 export 'src/ijkplayer.dart';
 export 'src/video_info.dart';
-export 'src/widget/controller.dart' show DefaultControllerWidget;
+export 'src/widget/controller_builder.dart' show DefaultControllerWidget;

+ 14 - 11
lib/src/controller.dart

@@ -52,8 +52,13 @@ class IjkMediaController {
   int _volume = 100;
 
   set volume(int value) {
+    if (value > 100) {
+      value = 100;
+    } else if (value < 0) {
+      value = 0;
+    }
     this._volume = value;
-    _volumeController.add(volume);
+    _volumeController.add(value);
     _setVolume(value);
   }
 
@@ -188,6 +193,14 @@ class IjkMediaController {
     await _plugin?.seekTo(0);
     refreshVideoInfo();
   }
+
+  Future<int> getSystemVolume() async {
+    return IjkManager.getSystemVolume();
+  }
+
+  Future<void> setSystemVolume(int volume) async {
+    await IjkManager.setSystemVolume(volume);
+  }
 }
 
 /// about channel
@@ -198,16 +211,6 @@ Future<int> _createIjk() async {
   return id;
 }
 
-/// For the hot reload/ hot restart to release last texture resource. Release version does not have hot reload, so you can not call it.
-///
-/// release版本可不调用, 主要是为了释放hot restart/hot reload的资源,因为原生资源不参与热重载
-///
-///
-/// If this method is not invoked in the debug version, the sound before the hot reload will continue to play.
-Future<void> initIJKPlayer() async {
-  _globalChannel.invokeMethod("init");
-}
-
 class _IjkPlugin {
   MethodChannel get channel => MethodChannel("top.kikt/ijkplayer/$textureId");
 

+ 1 - 1
lib/src/controller_builder.dart

@@ -1,6 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
-import 'package:flutter_ijkplayer/src/widget/controller.dart';
+import 'package:flutter_ijkplayer/src/widget/controller_builder.dart';
 
 Widget defaultBuildIjkControllerWidget(IjkMediaController controller) {
   return DefaultControllerWidget(controller: controller);

+ 2 - 1
lib/src/ijkplayer.dart

@@ -70,12 +70,13 @@ class IjkPlayerState extends State<IjkPlayer> {
       },
     );
     var controllerWidget = widget.controllerWidgetBuilder?.call(controller);
-    return Stack(
+    Widget stack = Stack(
       children: <Widget>[
         IgnorePointer(child: video),
         controllerWidget,
       ],
     );
+    return stack;
   }
 
   Widget _buildTexture(int id, VideoInfo info) {

+ 14 - 0
lib/src/manager.dart

@@ -3,9 +3,23 @@ part of './ijkplayer.dart';
 /// create 2019/3/18 by cai
 
 class IjkManager {
+  /// For the hot reload/ hot restart to release last texture resource. Release version does not have hot reload, so you can not call it.
+  ///
+  /// release版本可不调用, 主要是为了释放hot restart/hot reload的资源,因为原生资源不参与热重载
+  ///
+  ///
+  /// If this method is not invoked in the debug version, the sound before the hot reload will continue to play.
+  static Future<void> initIJKPlayer() async {
+    _globalChannel.invokeMethod("init");
+  }
+
   static Future<void> setSystemVolume(int volume) async {
     await _globalChannel.invokeMethod("setSystemVolume", {
       "volume": volume,
     });
   }
+
+  static Future<int> getSystemVolume() async {
+    return _globalChannel.invokeMethod("getSystemVolume");
+  }
 }

+ 0 - 202
lib/src/widget/controller.dart

@@ -1,202 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
-import 'package:flutter_ijkplayer/src/widget/progress_bar.dart';
-
-class DefaultControllerWidget extends StatefulWidget {
-  final IjkMediaController controller;
-  final bool doubleTapPlay;
-
-  const DefaultControllerWidget({
-    this.controller,
-    this.doubleTapPlay = false,
-  });
-
-  @override
-  _DefaultControllerWidgetState createState() =>
-      _DefaultControllerWidgetState();
-}
-
-class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
-  IjkMediaController get controller => widget.controller;
-
-  bool _isShow = true;
-
-  set isShow(bool value) {
-    _isShow = value;
-    print("call show");
-    setState(() {});
-    if (value == true) {
-      controller.refreshVideoInfo();
-    }
-  }
-
-  bool get isShow => _isShow;
-
-  Timer progressTimer;
-
-  StreamSubscription controllerSubscription;
-
-  @override
-  void initState() {
-    super.initState();
-    startTimer();
-    controllerSubscription = controller.textureIdStream.listen(_onTextIdChange);
-  }
-
-  void _onTextIdChange(int textId) {
-    print("onTextChange");
-    if (textId != null) {
-      startTimer();
-    } else {
-      stopTimer();
-    }
-  }
-
-  @override
-  void deactivate() {
-    super.deactivate();
-  }
-
-  @override
-  void dispose() {
-    controllerSubscription.cancel();
-    stopTimer();
-    super.dispose();
-  }
-
-  void startTimer() {
-    if (controller.textureId == null) {
-      return;
-    }
-
-    progressTimer?.cancel();
-    progressTimer = Timer.periodic(Duration(milliseconds: 400), (timer) {
-      print("will refresh info");
-      controller.refreshVideoInfo();
-    });
-  }
-
-  void stopTimer() {
-    progressTimer?.cancel();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return GestureDetector(
-      behavior: HitTestBehavior.opaque,
-      child: buildContent(),
-      onDoubleTap: widget.doubleTapPlay
-          ? () {
-              print("ondouble tap");
-              controller.playOrPause();
-            }
-          : null,
-      onTap: () => isShow = !isShow,
-    );
-  }
-
-  Widget buildContent() {
-    if (!isShow) {
-      return Container();
-    }
-    return StreamBuilder<VideoInfo>(
-      stream: controller.videoInfoStream,
-      builder: (context, snapshot) {
-        var info = snapshot.data;
-        if (info == null || !info.hasData) {
-          return Container();
-        }
-        return buildPortrait(info);
-      },
-    );
-  }
-
-  Widget buildPortrait(VideoInfo info) {
-    return PortraitController(
-      controller: controller,
-      info: info,
-    );
-  }
-}
-
-String _getTimeText(double durationSecond) {
-  var duration = Duration(milliseconds: ((durationSecond ?? 0) * 1000).toInt());
-  var minute = (duration.inMinutes % 60).toString().padLeft(2, "0");
-  var second = (duration.inSeconds % 60).toString().padLeft(2, "0");
-  var text = "$minute:$second";
-//  print("$durationSecond = $text");
-  return text;
-}
-
-class PortraitController extends StatelessWidget {
-  final IjkMediaController controller;
-  final VideoInfo info;
-
-  const PortraitController({
-    Key key,
-    this.controller,
-    this.info,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    if (!info.hasData) {
-      return Container();
-    }
-    Widget bottomBar = buildBottomBar();
-    return Column(
-      children: <Widget>[
-        Expanded(
-          child: Container(),
-        ),
-        bottomBar,
-      ],
-    );
-  }
-
-  Widget buildBottomBar() {
-    var currentTime = Text(
-      _getTimeText(info.currentPosition),
-    );
-    var maxTime = Text(
-      _getTimeText(info.duration),
-    );
-    var progress = buildProgress(info);
-    Widget widget = Row(
-      children: <Widget>[
-        Padding(
-          padding: const EdgeInsets.all(8.0),
-          child: currentTime,
-        ),
-        Expanded(child: progress),
-        Padding(
-          padding: const EdgeInsets.all(8.0),
-          child: maxTime,
-        ),
-      ],
-    );
-    widget = DefaultTextStyle(
-      style: const TextStyle(
-        color: Colors.white,
-      ),
-      child: widget,
-    );
-    widget = Container(
-      color: Colors.black.withOpacity(0.12),
-      child: widget,
-    );
-    return widget;
-  }
-
-  Widget buildProgress(VideoInfo info) {
-    return Container(
-      height: 5,
-      child: ProgressBar(
-        current: info.currentPosition,
-        max: info.duration,
-      ),
-    );
-  }
-}

+ 377 - 0
lib/src/widget/controller_builder.dart

@@ -0,0 +1,377 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
+import 'package:flutter_ijkplayer/src/widget/progress_bar.dart';
+
+class DefaultControllerWidget extends StatefulWidget {
+  final IjkMediaController controller;
+  final bool doubleTapPlay;
+
+  const DefaultControllerWidget({
+    this.controller,
+    this.doubleTapPlay = false,
+  });
+
+  @override
+  _DefaultControllerWidgetState createState() =>
+      _DefaultControllerWidgetState();
+}
+
+class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
+  IjkMediaController get controller => widget.controller;
+
+  bool _isShow = true;
+
+  set isShow(bool value) {
+    _isShow = value;
+    setState(() {});
+    if (value == true) {
+      controller.refreshVideoInfo();
+    }
+  }
+
+  bool get isShow => _isShow;
+
+  Timer progressTimer;
+
+  StreamSubscription controllerSubscription;
+
+  @override
+  void initState() {
+    super.initState();
+    startTimer();
+    controllerSubscription = controller.textureIdStream.listen(_onTextIdChange);
+  }
+
+  void _onTextIdChange(int textId) {
+    print("onTextChange");
+    if (textId != null) {
+      startTimer();
+    } else {
+      stopTimer();
+    }
+  }
+
+  @override
+  void deactivate() {
+    super.deactivate();
+  }
+
+  @override
+  void dispose() {
+    controllerSubscription.cancel();
+    stopTimer();
+    super.dispose();
+  }
+
+  void startTimer() {
+    if (controller.textureId == null) {
+      return;
+    }
+
+    progressTimer?.cancel();
+    progressTimer = Timer.periodic(Duration(milliseconds: 400), (timer) {
+      print("will refresh info");
+      controller.refreshVideoInfo();
+    });
+  }
+
+  void stopTimer() {
+    progressTimer?.cancel();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      child: buildContent(),
+      onDoubleTap: onDoubleTap(),
+      onHorizontalDragStart: _onHorizontalDragStart,
+      onHorizontalDragUpdate: _onHorizontalDragUpdate,
+      onHorizontalDragEnd: _onHorizontalDragEnd,
+      onVerticalDragStart: _onVerticalDragStart,
+      onVerticalDragUpdate: _onVerticalDragUpdate,
+      onVerticalDragEnd: _onVerticalDragEnd,
+      onTap: onTap,
+    );
+  }
+
+  onTap() => isShow = !isShow;
+
+  Function onDoubleTap() {
+    return widget.doubleTapPlay
+        ? () {
+            print("ondouble tap");
+            controller.playOrPause();
+          }
+        : null;
+  }
+
+  Widget buildContent() {
+    if (!isShow) {
+      return Container();
+    }
+    return StreamBuilder<VideoInfo>(
+      stream: controller.videoInfoStream,
+      builder: (context, snapshot) {
+        var info = snapshot.data;
+        if (info == null || !info.hasData) {
+          return Container();
+        }
+        return buildPortrait(info);
+      },
+    );
+  }
+
+  Widget buildPortrait(VideoInfo info) {
+    return PortraitController(
+      controller: controller,
+      info: info,
+    );
+  }
+
+  OverlayEntry _tipOverlay;
+
+  Widget createTipWidgetWrapper(Widget widget) {
+    var typography = Typography(platform: TargetPlatform.android);
+    var theme = typography.white;
+    const style = const TextStyle(
+      fontSize: 15.0,
+      color: Colors.white,
+      fontWeight: FontWeight.normal,
+    );
+    var mergedTextStyle = theme.body2.merge(style);
+    return Container(
+      decoration: BoxDecoration(
+        color: Colors.black.withOpacity(0.5),
+        borderRadius: BorderRadius.circular(20.0),
+      ),
+      height: 100.0,
+      width: 100.0,
+      child: DefaultTextStyle(
+        child: widget,
+        style: mergedTextStyle,
+      ),
+    );
+  }
+
+  void showTipWidget(Widget widget) {
+    hideTipWidget();
+    _tipOverlay = OverlayEntry(
+      builder: (BuildContext context) {
+        return IgnorePointer(
+          child: Center(
+            child: widget,
+          ),
+        );
+      },
+    );
+    Overlay.of(context).insert(_tipOverlay);
+  }
+
+  void hideTipWidget() {
+    _tipOverlay?.remove();
+    _tipOverlay = null;
+  }
+
+  _ProgressCalculator _calculator;
+
+  void _onHorizontalDragStart(DragStartDetails details) async {
+    var videoInfo = await controller.getVideoInfo();
+    _calculator = _ProgressCalculator(details, videoInfo);
+  }
+
+  void _onHorizontalDragUpdate(DragUpdateDetails details) {
+    if (_calculator == null || details == null) {
+      return;
+    }
+    var updateText = _calculator.calcUpdate(details);
+
+    var offsetPosition = _calculator.getOffsetPosition();
+
+    IconData iconData =
+        offsetPosition > 0 ? Icons.fast_forward : Icons.fast_rewind;
+    var w = Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: <Widget>[
+        Icon(
+          iconData,
+          color: Colors.white,
+          size: 40.0,
+        ),
+        Text(
+          updateText,
+          textAlign: TextAlign.center,
+        ),
+      ],
+    );
+
+    showTipWidget(createTipWidgetWrapper(w));
+  }
+
+  void _onHorizontalDragEnd(DragEndDetails details) async {
+    hideTipWidget();
+    var targetSeek = _calculator.getTargetSeek(details);
+    _calculator = null;
+    await controller.seekTo(targetSeek);
+    var videoInfo = await controller.getVideoInfo();
+    if (targetSeek < videoInfo.duration) await controller.play();
+  }
+
+  void _onVerticalDragStart(DragStartDetails details) {}
+
+  void _onVerticalDragUpdate(DragUpdateDetails details) async {
+    if (details.delta.dy > 0) {
+      controller.volume--;
+    } else if (details.delta.dy < 0) {
+      controller.volume++;
+    }
+
+    var column = Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: <Widget>[
+        Icon(
+          Icons.volume_up,
+          color: Colors.white,
+          size: 25.0,
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 10.0),
+          child: Text(controller.volume.toString()),
+        ),
+      ],
+    );
+
+    showTipWidget(createTipWidgetWrapper(column));
+  }
+
+  void _onVerticalDragEnd(DragEndDetails details) {
+    hideTipWidget();
+  }
+}
+
+class _ProgressCalculator {
+  DragStartDetails startDetails;
+  VideoInfo info;
+
+  double dx;
+
+  _ProgressCalculator(this.startDetails, this.info);
+
+  String calcUpdate(DragUpdateDetails details) {
+    dx = details.globalPosition.dx - startDetails.globalPosition.dx;
+    var f = dx > 0 ? "+" : "-";
+    var offset = getOffsetPosition().round().abs();
+    return "$f${offset}s";
+  }
+
+  double getTargetSeek(DragEndDetails details) {
+    var target = info.currentPosition + getOffsetPosition();
+    if (target < 0) {
+      target = 0;
+    } else if (target > info.duration) {
+      target = info.duration;
+    }
+    return target;
+  }
+
+  double getOffsetPosition() {
+    return dx / 10;
+  }
+}
+
+String _getTimeText(double durationSecond) {
+  var duration = Duration(milliseconds: ((durationSecond ?? 0) * 1000).toInt());
+  var minute = (duration.inMinutes % 60).toString().padLeft(2, "0");
+  var second = (duration.inSeconds % 60).toString().padLeft(2, "0");
+  var text = "$minute:$second";
+//  print("$durationSecond = $text");
+  return text;
+}
+
+class PortraitController extends StatelessWidget {
+  final IjkMediaController controller;
+  final VideoInfo info;
+
+  const PortraitController({
+    Key key,
+    this.controller,
+    this.info,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    if (!info.hasData) {
+      return Container();
+    }
+    Widget bottomBar = buildBottomBar();
+    return Column(
+      children: <Widget>[
+        Expanded(
+          child: Container(),
+        ),
+        bottomBar,
+      ],
+    );
+  }
+
+  Widget buildBottomBar() {
+    var currentTime = Text(
+      _getTimeText(info.currentPosition),
+    );
+    var maxTime = Text(
+      _getTimeText(info.duration),
+    );
+    var progress = buildProgress(info);
+
+    var playButton = buildPlayButton();
+
+    Widget widget = Row(
+      children: <Widget>[
+        playButton,
+        Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: currentTime,
+        ),
+        Expanded(child: progress),
+        Padding(
+          padding: const EdgeInsets.all(8.0),
+          child: maxTime,
+        ),
+      ],
+    );
+    widget = DefaultTextStyle(
+      style: const TextStyle(
+        color: Colors.white,
+      ),
+      child: widget,
+    );
+    widget = Container(
+      color: Colors.black.withOpacity(0.12),
+      child: widget,
+    );
+    return widget;
+  }
+
+  Widget buildProgress(VideoInfo info) {
+    return Container(
+      height: 5,
+      child: ProgressBar(
+        current: info.currentPosition,
+        max: info.duration,
+      ),
+    );
+  }
+
+  buildPlayButton() {
+    return IconButton(
+      onPressed: () {
+        controller.playOrPause();
+      },
+      color: Colors.white,
+      icon: Icon(info.isPlaying ? Icons.pause : Icons.play_arrow),
+      iconSize: 25.0,
+    );
+  }
+}

+ 4 - 2
lib/src/widget/ijkplayer_builder.dart

@@ -12,11 +12,13 @@ Widget buildDefaultIjkPlayer(
   IjkMediaController controller,
   VideoInfo info,
 ) {
-  print("buildPlayer");
-
   int degree = info?.degree ?? 0;
   double ratio = info?.ratio ?? 1280 / 720;
 
+  if (ratio == 0) {
+    ratio = 1280 / 720;
+  }
+
   var id = controller.textureId;
 
   if (id == null) {