Pārlūkot izejas kodu

feat(fullscreen videos): Support fullscreen video on Android (#325)

* Extract WebChromeClient from an anonymous class

* Support fullscreen videos on Android

Forces landscape mode while playing.

* Use sticky immersive mode for fullscreen videos

No longer forces landscape mode as that is a problem for portrait videos - allow
the user to rotate as necessary.

Only supports KitKat or greater, and falls back to leaving the status and navigation
bars visible for lower than KitKat. This is the easiest way to prevent issues with
resizing the video during playback.

Also implement a lifecyle event listener which means if a user backgrounds the app
or locks the screen with the video fullscreen, the UI visibility is re-applied.

* Add allowsFullscreenVideo prop to control whether videos can be fullscreen on Android

Luckily, we're able to change the WebChromeClient on demand in response to prop changes
without seeming to do any harm. If you switch to disallow fullscreen, it will attempt
to close the currently fullscreened video (if there is one) so users aren't stuck.

I did notice a bug that if you go from fullscreen allowed, to fullscreen disallowed,
the fullscreen button will remain on the video. Tapping the button will have no effect.
Daniel Vicory 5 gadus atpakaļ
vecāks
revīzija
d72c2ae144

+ 169
- 54
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java Parādīt failu

@@ -6,12 +6,17 @@ import android.app.DownloadManager;
6 6
 import android.content.Context;
7 7
 import android.content.Intent;
8 8
 import android.graphics.Bitmap;
9
+import android.graphics.Color;
9 10
 import android.net.Uri;
10 11
 import android.os.Build;
11 12
 import android.os.Environment;
13
+import android.support.annotation.RequiresApi;
12 14
 import android.text.TextUtils;
15
+import android.view.Gravity;
13 16
 import android.view.View;
17
+import android.view.ViewGroup;
14 18
 import android.view.ViewGroup.LayoutParams;
19
+import android.view.WindowManager;
15 20
 import android.webkit.ConsoleMessage;
16 21
 import android.webkit.CookieManager;
17 22
 import android.webkit.DownloadListener;
@@ -24,6 +29,7 @@ import android.webkit.WebResourceRequest;
24 29
 import android.webkit.WebSettings;
25 30
 import android.webkit.WebView;
26 31
 import android.webkit.WebViewClient;
32
+import android.widget.FrameLayout;
27 33
 
28 34
 import com.facebook.react.bridge.Arguments;
29 35
 import com.facebook.react.bridge.LifecycleEventListener;
@@ -106,6 +112,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
106 112
   protected static final String BLANK_URL = "about:blank";
107 113
   protected WebViewConfig mWebViewConfig;
108 114
 
115
+  protected RNCWebChromeClient mWebChromeClient = null;
116
+  protected boolean mAllowsFullscreenVideo = false;
117
+
109 118
   public RNCWebViewManager() {
110 119
     mWebViewConfig = new WebViewConfig() {
111 120
       public void configWebView(WebView webView) {
@@ -137,59 +146,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
137 146
   @TargetApi(Build.VERSION_CODES.LOLLIPOP)
138 147
   protected WebView createViewInstance(ThemedReactContext reactContext) {
139 148
     RNCWebView webView = createRNCWebViewInstance(reactContext);
140
-    webView.setWebChromeClient(new WebChromeClient() {
141
-      @Override
142
-      public boolean onConsoleMessage(ConsoleMessage message) {
143
-        if (ReactBuildConfig.DEBUG) {
144
-          return super.onConsoleMessage(message);
145
-        }
146
-        // Ignore console logs in non debug builds.
147
-        return true;
148
-      }
149
-
150
-
151
-      @Override
152
-      public void onProgressChanged(WebView webView, int newProgress) {
153
-        super.onProgressChanged(webView, newProgress);
154
-        WritableMap event = Arguments.createMap();
155
-        event.putDouble("target", webView.getId());
156
-        event.putString("title", webView.getTitle());
157
-        event.putBoolean("canGoBack", webView.canGoBack());
158
-        event.putBoolean("canGoForward", webView.canGoForward());
159
-        event.putDouble("progress", (float) newProgress / 100);
160
-        dispatchEvent(
161
-          webView,
162
-          new TopLoadingProgressEvent(
163
-            webView.getId(),
164
-            event));
165
-      }
166
-
167
-      @Override
168
-      public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
169
-        callback.invoke(origin, true, false);
170
-      }
171
-
172
-      protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
173
-        getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType);
174
-      }
175
-
176
-      protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
177
-        getModule(reactContext).startPhotoPickerIntent(filePathCallback, "");
178
-      }
179
-
180
-      protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
181
-        getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType);
182
-      }
183
-
184
-      @TargetApi(Build.VERSION_CODES.LOLLIPOP)
185
-      @Override
186
-      public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
187
-        String[] acceptTypes = fileChooserParams.getAcceptTypes();
188
-        boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
189
-        Intent intent = fileChooserParams.createIntent();
190
-        return getModule(reactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
191
-      }
192
-    });
149
+    setupWebChromeClient(reactContext, webView);
193 150
     reactContext.addLifecycleEventListener(webView);
194 151
     mWebViewConfig.configWebView(webView);
195 152
     WebSettings settings = webView.getSettings();
@@ -454,6 +411,14 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
454 411
     }
455 412
   }
456 413
 
414
+  @ReactProp(name = "allowsFullscreenVideo")
415
+  public void setAllowsFullscreenVideo(
416
+    WebView view,
417
+    @Nullable Boolean allowsFullscreenVideo) {
418
+    mAllowsFullscreenVideo = allowsFullscreenVideo != null && allowsFullscreenVideo;
419
+    setupWebChromeClient((ReactContext)view.getContext(), view);
420
+  }
421
+
457 422
   @ReactProp(name = "allowFileAccess")
458 423
   public void setAllowFileAccess(
459 424
     WebView view,
@@ -554,10 +519,67 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
554 519
     ((RNCWebView) webView).cleanupCallbacksAndDestroy();
555 520
   }
556 521
 
557
-  public RNCWebViewModule getModule(ReactContext reactContext) {
522
+  public static RNCWebViewModule getModule(ReactContext reactContext) {
558 523
     return reactContext.getNativeModule(RNCWebViewModule.class);
559 524
   }
560 525
 
526
+  protected void setupWebChromeClient(ReactContext reactContext, WebView webView) {
527
+    if (mAllowsFullscreenVideo) {
528
+      mWebChromeClient = new RNCWebChromeClient(reactContext, webView) {
529
+        @Override
530
+        public void onShowCustomView(View view, CustomViewCallback callback) {
531
+          if (mVideoView != null) {
532
+            callback.onCustomViewHidden();
533
+            return;
534
+          }
535
+
536
+          mVideoView = view;
537
+          mCustomViewCallback = callback;
538
+
539
+          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
540
+            mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY);
541
+            mReactContext.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
542
+          }
543
+
544
+          mVideoView.setBackgroundColor(Color.BLACK);
545
+          getRootView().addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS);
546
+          mWebView.setVisibility(View.GONE);
547
+
548
+          mReactContext.addLifecycleEventListener(this);
549
+        }
550
+
551
+        @Override
552
+        public void onHideCustomView() {
553
+          if (mVideoView == null) {
554
+            return;
555
+          }
556
+
557
+          mVideoView.setVisibility(View.GONE);
558
+          getRootView().removeView(mVideoView);
559
+          mCustomViewCallback.onCustomViewHidden();
560
+
561
+          mVideoView = null;
562
+          mCustomViewCallback = null;
563
+
564
+          mWebView.setVisibility(View.VISIBLE);
565
+
566
+          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
567
+            mReactContext.getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
568
+          }
569
+
570
+          mReactContext.removeLifecycleEventListener(this);
571
+        }
572
+      };
573
+      webView.setWebChromeClient(mWebChromeClient);
574
+    } else {
575
+      if (mWebChromeClient != null) {
576
+        mWebChromeClient.onHideCustomView();
577
+      }
578
+      mWebChromeClient = new RNCWebChromeClient(reactContext, webView);
579
+      webView.setWebChromeClient(mWebChromeClient);
580
+    }
581
+  }
582
+
561 583
   protected static class RNCWebViewClient extends WebViewClient {
562 584
 
563 585
     protected boolean mLastLoadFailed = false;
@@ -655,6 +677,99 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
655 677
     }
656 678
   }
657 679
 
680
+  protected static class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener {
681
+    protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams(
682
+      LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER);
683
+
684
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
685
+    protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
686
+      View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
687
+      View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
688
+      View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
689
+      View.SYSTEM_UI_FLAG_FULLSCREEN |
690
+      View.SYSTEM_UI_FLAG_IMMERSIVE |
691
+      View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
692
+
693
+    protected ReactContext mReactContext;
694
+    protected View mWebView;
695
+
696
+    protected View mVideoView;
697
+    protected WebChromeClient.CustomViewCallback mCustomViewCallback;
698
+
699
+    public RNCWebChromeClient(ReactContext reactContext, WebView webView) {
700
+      this.mReactContext = reactContext;
701
+      this.mWebView = webView;
702
+    }
703
+
704
+    @Override
705
+    public boolean onConsoleMessage(ConsoleMessage message) {
706
+      if (ReactBuildConfig.DEBUG) {
707
+        return super.onConsoleMessage(message);
708
+      }
709
+      // Ignore console logs in non debug builds.
710
+      return true;
711
+    }
712
+
713
+    @Override
714
+    public void onProgressChanged(WebView webView, int newProgress) {
715
+      super.onProgressChanged(webView, newProgress);
716
+      WritableMap event = Arguments.createMap();
717
+      event.putDouble("target", webView.getId());
718
+      event.putString("title", webView.getTitle());
719
+      event.putBoolean("canGoBack", webView.canGoBack());
720
+      event.putBoolean("canGoForward", webView.canGoForward());
721
+      event.putDouble("progress", (float) newProgress / 100);
722
+      dispatchEvent(
723
+        webView,
724
+        new TopLoadingProgressEvent(
725
+          webView.getId(),
726
+          event));
727
+    }
728
+
729
+    @Override
730
+    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
731
+      callback.invoke(origin, true, false);
732
+    }
733
+
734
+    protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
735
+      getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType);
736
+    }
737
+
738
+    protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
739
+      getModule(mReactContext).startPhotoPickerIntent(filePathCallback, "");
740
+    }
741
+
742
+    protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
743
+      getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType);
744
+    }
745
+
746
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
747
+    @Override
748
+    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
749
+      String[] acceptTypes = fileChooserParams.getAcceptTypes();
750
+      boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
751
+      Intent intent = fileChooserParams.createIntent();
752
+      return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
753
+    }
754
+
755
+    @Override
756
+    public void onHostResume() {
757
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mVideoView != null && mVideoView.getSystemUiVisibility() != FULLSCREEN_SYSTEM_UI_VISIBILITY) {
758
+        mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY);
759
+      }
760
+    }
761
+
762
+    @Override
763
+    public void onHostPause() { }
764
+
765
+    @Override
766
+    public void onHostDestroy() { }
767
+
768
+    protected ViewGroup getRootView() {
769
+      return (ViewGroup) mReactContext.getCurrentActivity().findViewById(android.R.id.content);
770
+    }
771
+  }
772
+
658 773
   /**
659 774
    * Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
660 775
    * to call {@link WebView#destroy} on activity destroy event and also to clear the client

+ 11
- 0
docs/Reference.md Parādīt failu

@@ -30,6 +30,7 @@ This document lays out the current public properties and methods for the React N
30 30
 - [`mixedContentMode`](Reference.md#mixedcontentmode)
31 31
 - [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled)
32 32
 - [`userAgent`](Reference.md#useragent)
33
+- [`allowsFullscreenVideo`](Reference.md#allowsfullscreenvideo)
33 34
 - [`allowsInlineMediaPlayback`](Reference.md#allowsinlinemediaplayback)
34 35
 - [`bounces`](Reference.md#bounces)
35 36
 - [`overScrollMode`](Reference.md#overscrollmode)
@@ -591,6 +592,16 @@ Sets the user-agent for the `WebView`. This will only work for iOS if you are us
591 592
 
592 593
 ---
593 594
 
595
+### `allowsFullscreenVideo`
596
+
597
+Boolean that determines whether videos are allowed to be played in fullscreen. The default value is `false`.
598
+
599
+| Type | Required | Platform |
600
+| ---- | -------- | -------- |
601
+| bool | No       | Android  |
602
+
603
+---
604
+
594 605
 ### `allowsInlineMediaPlayback`
595 606
 
596 607
 Boolean that determines whether HTML5 videos play inline or use the native full-screen controller. The default value is `false`.

+ 1
- 0
src/WebView.android.tsx Parādīt failu

@@ -48,6 +48,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
48 48
     javaScriptEnabled: true,
49 49
     thirdPartyCookiesEnabled: true,
50 50
     scalesPageToFit: true,
51
+    allowsFullscreenVideo: false,
51 52
     allowFileAccess: false,
52 53
     saveFormDataDisabled: false,
53 54
     cacheEnabled: true,