expo-video-subtitle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/FORK.md +149 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +87 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +457 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +25 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
- package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
- package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
- package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
- package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
- package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
- package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
- package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
- package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
- package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
- package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +20 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
- package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
- package/build/NativeVideoAirPlayButtonView.js +3 -0
- package/build/NativeVideoAirPlayButtonView.js.map +1 -0
- package/build/NativeVideoModule.d.ts +13 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +5 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +5 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoAirPlayButton.d.ts +10 -0
- package/build/VideoAirPlayButton.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.d.ts +3 -0
- package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.js +6 -0
- package/build/VideoAirPlayButton.ios.js.map +1 -0
- package/build/VideoAirPlayButton.js +13 -0
- package/build/VideoAirPlayButton.js.map +1 -0
- package/build/VideoAirPlayButton.types.d.ts +35 -0
- package/build/VideoAirPlayButton.types.d.ts.map +1 -0
- package/build/VideoAirPlayButton.types.js +2 -0
- package/build/VideoAirPlayButton.types.js.map +1 -0
- package/build/VideoModule.d.ts +35 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +44 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +17 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +55 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +782 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +79 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +388 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +278 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +4 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +69 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +100 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +362 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +239 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +46 -0
- package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
- package/ios/Cache/CachedResource.swift +164 -0
- package/ios/Cache/CachingHelpers.swift +107 -0
- package/ios/Cache/MediaFileHandle.swift +139 -0
- package/ios/Cache/MediaInfo.swift +185 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +192 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +11 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/FullscreenOrientation.swift +34 -0
- package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/Enums/VideoRange.swift +21 -0
- package/ios/ExpoVideo.podspec +27 -0
- package/ios/NowPlayingManager.swift +304 -0
- package/ios/OrientationAVPlayerViewController.swift +263 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/FullscreenOptions.swift +14 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/ScrubbingModeOptions.swift +7 -0
- package/ios/Records/SeekTolerance.swift +20 -0
- package/ios/Records/SubtitleSource.swift +32 -0
- package/ios/Records/SubtitleStyle.swift +118 -0
- package/ios/Records/Tracks.swift +229 -0
- package/ios/Records/VideoEventPayloads.swift +81 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +28 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
- package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
- package/ios/Utils/FourCharCode+toString.swift +25 -0
- package/ios/Utils/HlsUriUtils.swift +27 -0
- package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
- package/ios/VideoAirPlayButtonView.swift +70 -0
- package/ios/VideoAsset.swift +98 -0
- package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
- package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
- package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
- package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +182 -0
- package/ios/VideoModule.swift +413 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +485 -0
- package/ios/VideoPlayerAudioTracks.swift +109 -0
- package/ios/VideoPlayerItem.swift +169 -0
- package/ios/VideoPlayerObserver.swift +612 -0
- package/ios/VideoPlayerSeeker.swift +77 -0
- package/ios/VideoPlayerSubtitleSideload.swift +203 -0
- package/ios/VideoPlayerSubtitles.swift +88 -0
- package/ios/VideoSourceLoader.swift +90 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +201 -0
- package/package.json +51 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +3 -0
- package/plugin/build/withExpoVideo.d.ts +9 -0
- package/plugin/build/withExpoVideo.js +73 -0
- package/plugin/index.d.ts +1 -0
- package/plugin/index.js +1 -0
- package/plugin/jest.config.js +1 -0
- package/plugin/src/index.ts +3 -0
- package/plugin/src/withExpoVideo.ts +103 -0
- package/plugin/tsconfig.json +10 -0
- package/src/NativeVideoAirPlayButtonView.ts +3 -0
- package/src/NativeVideoModule.ts +16 -0
- package/src/NativeVideoModule.web.ts +4 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoAirPlayButton.ios.tsx +8 -0
- package/src/VideoAirPlayButton.tsx +14 -0
- package/src/VideoAirPlayButton.types.ts +39 -0
- package/src/VideoModule.ts +47 -0
- package/src/VideoPlayer.tsx +74 -0
- package/src/VideoPlayer.types.ts +893 -0
- package/src/VideoPlayer.web.tsx +466 -0
- package/src/VideoPlayerEvents.types.ts +332 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +111 -0
- package/src/VideoView.types.ts +405 -0
- package/src/VideoView.web.tsx +293 -0
- package/src/index.ts +26 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +22 -0
- package/tsconfig.all.json +11 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
package expo.modules.video
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.view.accessibility.CaptioningManager
|
|
9
|
+
import android.view.LayoutInflater
|
|
10
|
+
import android.view.MotionEvent
|
|
11
|
+
import android.view.View
|
|
12
|
+
import android.view.ViewGroup
|
|
13
|
+
import android.widget.ImageButton
|
|
14
|
+
import androidx.media3.common.Tracks
|
|
15
|
+
import androidx.media3.ui.PlayerView
|
|
16
|
+
import com.facebook.react.bridge.ReactContext
|
|
17
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
18
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
19
|
+
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper
|
|
20
|
+
import expo.modules.kotlin.AppContext
|
|
21
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
22
|
+
import expo.modules.kotlin.views.ExpoView
|
|
23
|
+
import expo.modules.video.delegates.IgnoreSameSet
|
|
24
|
+
import expo.modules.video.enums.ContentFit
|
|
25
|
+
import expo.modules.video.player.VideoPlayer
|
|
26
|
+
import expo.modules.video.listeners.VideoPlayerListener
|
|
27
|
+
import expo.modules.video.listeners.VideoViewListener
|
|
28
|
+
import expo.modules.video.records.AudioTrack
|
|
29
|
+
import expo.modules.video.records.ButtonOptions
|
|
30
|
+
import expo.modules.video.records.SubtitleStyle
|
|
31
|
+
import expo.modules.video.records.SubtitleTrack
|
|
32
|
+
import expo.modules.video.records.VideoSource
|
|
33
|
+
import expo.modules.video.records.VideoTrack
|
|
34
|
+
import expo.modules.video.records.FullscreenOptions
|
|
35
|
+
import expo.modules.video.utils.SubtitleUtils
|
|
36
|
+
import expo.modules.video.utils.dispatchMotionEvent
|
|
37
|
+
import expo.modules.video.managers.VideoManager
|
|
38
|
+
import expo.modules.video.managers.calculateCurrentPipAspectRatio
|
|
39
|
+
import expo.modules.video.records.PiPParams
|
|
40
|
+
import expo.modules.video.utils.calculateRectHint
|
|
41
|
+
import java.lang.ref.WeakReference
|
|
42
|
+
import java.util.UUID
|
|
43
|
+
|
|
44
|
+
class SurfaceVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext)
|
|
45
|
+
class TextureVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext, true)
|
|
46
|
+
|
|
47
|
+
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
48
|
+
open class VideoView(context: Context, appContext: AppContext, useTextureView: Boolean = false) : ExpoView(context, appContext), VideoPlayerListener {
|
|
49
|
+
val videoViewId: String = UUID.randomUUID().toString()
|
|
50
|
+
val playerView: PlayerView = LayoutInflater.from(context.applicationContext).inflate(getPlayerViewLayoutId(useTextureView), null) as PlayerView
|
|
51
|
+
val onPictureInPictureStart by EventDispatcher<Unit>()
|
|
52
|
+
val onPictureInPictureStop by EventDispatcher<Unit>()
|
|
53
|
+
val onFullscreenEnter by EventDispatcher<Unit>()
|
|
54
|
+
val onFullscreenExit by EventDispatcher<Unit>()
|
|
55
|
+
val onFirstFrameRender by EventDispatcher<Unit>()
|
|
56
|
+
|
|
57
|
+
// In some situations we can't detect if the view will enter PiP, in that case the playback will be paused
|
|
58
|
+
// We can get an event after PiP has started, that's when we should resume playback
|
|
59
|
+
var wasAutoPaused: Boolean = false
|
|
60
|
+
var isInFullscreen: Boolean = false
|
|
61
|
+
private set
|
|
62
|
+
var currentTrackHasSubtitles = false
|
|
63
|
+
private set
|
|
64
|
+
var showsAudioTracksButton = false
|
|
65
|
+
private set
|
|
66
|
+
|
|
67
|
+
var requiresLinearPlayback: Boolean = false
|
|
68
|
+
set(value) {
|
|
69
|
+
field = value
|
|
70
|
+
videoPlayer?.requiresLinearPlayback = value
|
|
71
|
+
playerView.applyRequiresLinearPlayback(value)
|
|
72
|
+
applyButtonSettings()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var buttonOptions: ButtonOptions = ButtonOptions()
|
|
76
|
+
set(value) {
|
|
77
|
+
field = value
|
|
78
|
+
applyButtonSettings()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var subtitleStyle: SubtitleStyle? = null
|
|
82
|
+
set(value) {
|
|
83
|
+
field = value
|
|
84
|
+
SubtitleUtils.configureSubtitleView(playerView, context, value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private var listeners = mutableListOf<WeakReference<VideoViewListener>>()
|
|
88
|
+
private val currentActivity = appContext.throwingActivity
|
|
89
|
+
private val decorView = currentActivity.window.decorView
|
|
90
|
+
private val touchEventCoalescingKeyHelper = TouchEventCoalescingKeyHelper()
|
|
91
|
+
private var reactNativeEventDispatcher: EventDispatcher? = null
|
|
92
|
+
private var captioningChangeListener: CaptioningManager.CaptioningChangeListener? = null
|
|
93
|
+
|
|
94
|
+
private val windowFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
|
95
|
+
if (hasFocus) {
|
|
96
|
+
// Reconfigure when window gains focus (returning from settings)
|
|
97
|
+
SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var pipParams by IgnoreSameSet(PiPParams()) { new, old ->
|
|
102
|
+
listeners.forEach {
|
|
103
|
+
it.get()?.onPiPParamsChanged(this, old, new)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
private set
|
|
107
|
+
|
|
108
|
+
// We need to keep track of the target surface view visibility, but only apply it when `useExoShutter` is false.
|
|
109
|
+
var shouldHideSurfaceView: Boolean = true
|
|
110
|
+
|
|
111
|
+
var useExoShutter: Boolean? = null
|
|
112
|
+
set(value) {
|
|
113
|
+
if (value == true) {
|
|
114
|
+
playerView.setShutterBackgroundColor(Color.BLACK)
|
|
115
|
+
} else {
|
|
116
|
+
playerView.setShutterBackgroundColor(Color.TRANSPARENT)
|
|
117
|
+
}
|
|
118
|
+
applySurfaceViewVisibility()
|
|
119
|
+
field = value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var autoEnterPiP: Boolean by IgnoreSameSet(false) { new, _ ->
|
|
123
|
+
pipParams = pipParams.copy(autoEnter = new)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var contentFit: ContentFit = ContentFit.CONTAIN
|
|
127
|
+
set(value) {
|
|
128
|
+
playerView.resizeMode = value.toResizeMode()
|
|
129
|
+
field = value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var videoPlayer: VideoPlayer? = null
|
|
133
|
+
set(newPlayer) {
|
|
134
|
+
field?.let {
|
|
135
|
+
VideoManager.onVideoPlayerDetachedFromView(it, this)
|
|
136
|
+
}
|
|
137
|
+
val oldPlayer = videoPlayer
|
|
138
|
+
val hasEmittedFirstFrame = newPlayer?.firstFrameEventGenerator?.hasSentFirstFrameForCurrentMediaItem
|
|
139
|
+
?: false
|
|
140
|
+
oldPlayer?.removeListener(this)
|
|
141
|
+
newPlayer?.addListener(this)
|
|
142
|
+
field = newPlayer
|
|
143
|
+
shouldHideSurfaceView = !hasEmittedFirstFrame
|
|
144
|
+
applySurfaceViewVisibility()
|
|
145
|
+
attachPlayer()
|
|
146
|
+
newPlayer?.let {
|
|
147
|
+
VideoManager.onVideoPlayerAttachedToView(it, this)
|
|
148
|
+
}
|
|
149
|
+
if (oldPlayer != newPlayer) {
|
|
150
|
+
oldPlayer?.hasBeenDisconnectedFromVideoView()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var useNativeControls: Boolean = true
|
|
155
|
+
set(value) {
|
|
156
|
+
val shouldShowSubtitle = value && (buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
|
|
157
|
+
playerView.useController = value
|
|
158
|
+
playerView.setShowSubtitleButton(shouldShowSubtitle)
|
|
159
|
+
field = value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var allowsFullscreen: Boolean = true
|
|
163
|
+
set(value) {
|
|
164
|
+
if (value) {
|
|
165
|
+
playerView.setFullscreenButtonClickListener { enterFullscreen() }
|
|
166
|
+
} else {
|
|
167
|
+
playerView.setFullscreenButtonClickListener(null)
|
|
168
|
+
// Setting listener to null should hide the button, but judging by ExoPlayer source code
|
|
169
|
+
// there is a bug and the button isn't hidden. We need to do it manually.
|
|
170
|
+
playerView.setFullscreenButtonVisibility(false)
|
|
171
|
+
}
|
|
172
|
+
field = value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
var fullscreenOptions: FullscreenOptions = FullscreenOptions()
|
|
176
|
+
set(value) {
|
|
177
|
+
field = value
|
|
178
|
+
if (value.enable) {
|
|
179
|
+
playerView.setFullscreenButtonClickListener { enterFullscreen() }
|
|
180
|
+
} else {
|
|
181
|
+
playerView.setFullscreenButtonClickListener(null)
|
|
182
|
+
playerView.setFullscreenButtonVisibility(false)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private val mLayoutRunnable = Runnable {
|
|
187
|
+
measure(
|
|
188
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
189
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
190
|
+
)
|
|
191
|
+
layout(left, top, right, bottom)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
init {
|
|
195
|
+
VideoManager.registerVideoView(this)
|
|
196
|
+
playerView.setFullscreenButtonClickListener { enterFullscreen() }
|
|
197
|
+
// The prop `useNativeControls` prop is sometimes applied after the view is created, and sometimes there is a visible
|
|
198
|
+
// flash of controls event when they are set to off. Initially we set it to `false` and apply it in `onAttachedToWindow` to avoid this.
|
|
199
|
+
this.playerView.useController = false
|
|
200
|
+
|
|
201
|
+
// Start with the SurfaceView being transparent to avoid any flickers when the prop value is delivered.
|
|
202
|
+
this.playerView.setShutterBackgroundColor(Color.TRANSPARENT)
|
|
203
|
+
this.playerView.videoSurfaceView?.alpha = 0f
|
|
204
|
+
|
|
205
|
+
// Configure subtitle view to fix sizing issues with embedded styles
|
|
206
|
+
SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
|
|
207
|
+
addView(
|
|
208
|
+
playerView,
|
|
209
|
+
ViewGroup.LayoutParams(
|
|
210
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
211
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
reactNativeEventDispatcher = UIManagerHelper.getEventDispatcher(appContext.reactContext as ReactContext, id)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fun applySurfaceViewVisibility() {
|
|
219
|
+
if (useExoShutter != true && shouldHideSurfaceView) {
|
|
220
|
+
playerView.videoSurfaceView?.alpha = 0f
|
|
221
|
+
} else {
|
|
222
|
+
playerView.videoSurfaceView?.alpha = 1f
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fun enterFullscreen() {
|
|
227
|
+
val intent = Intent(context, FullscreenPlayerActivity::class.java)
|
|
228
|
+
intent.putExtra(VideoManager.INTENT_PLAYER_KEY, videoViewId)
|
|
229
|
+
intent.putExtra(FullscreenPlayerActivity.INTENT_FULLSCREEN_OPTIONS_KEY, fullscreenOptions)
|
|
230
|
+
// Set before starting the activity to avoid entering PiP unintentionally
|
|
231
|
+
isInFullscreen = true
|
|
232
|
+
currentActivity.startActivity(intent)
|
|
233
|
+
|
|
234
|
+
// Disable the enter transition
|
|
235
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
236
|
+
currentActivity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
|
|
237
|
+
} else {
|
|
238
|
+
@Suppress("DEPRECATION")
|
|
239
|
+
currentActivity.overridePendingTransition(0, 0)
|
|
240
|
+
}
|
|
241
|
+
onFullscreenEnter(Unit)
|
|
242
|
+
pipParams = pipParams.copy(blocksAppFromEntering = true)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fun attachPlayer() {
|
|
246
|
+
videoPlayer?.changeVideoView(this)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fun exitFullscreen() {
|
|
250
|
+
// Fullscreen uses a different PlayerView instance, because of that we need to manually update the non-fullscreen player icon after exiting
|
|
251
|
+
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
|
252
|
+
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
|
253
|
+
attachPlayer()
|
|
254
|
+
onFullscreenExit(Unit)
|
|
255
|
+
isInFullscreen = false
|
|
256
|
+
pipParams = pipParams.copy(blocksAppFromEntering = false)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fun enterPictureInPicture() {
|
|
260
|
+
if (!isPictureInPictureSupported(currentActivity)) {
|
|
261
|
+
throw PictureInPictureUnsupportedException()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (playerView.player == null) {
|
|
265
|
+
throw PictureInPictureEnterException("No player attached to the VideoView")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
pipParams = pipParams.copy(willEnter = true)
|
|
269
|
+
VideoManager.pictureInPicture.enterPictureInPicture(this)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param pipCandidate - VideoView that was elected to be displayed in PiP
|
|
274
|
+
*/
|
|
275
|
+
fun onStartPictureInPicture(pipCandidate: VideoView?) {
|
|
276
|
+
onPictureInPictureStart(Unit)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param pipCandidate - VideoView that was being displayed in PiP
|
|
281
|
+
*/
|
|
282
|
+
fun onStopPictureInPicture(pipCandidate: VideoView?) {
|
|
283
|
+
pipParams = pipParams.copy(willEnter = false)
|
|
284
|
+
onPictureInPictureStop(Unit)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fun addVideoViewListener(listener: VideoViewListener) {
|
|
288
|
+
if (listeners.any { it.get() == listener }) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
listeners.add(WeakReference(listener))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fun removeVideoViewListener(listener: VideoViewListener) {
|
|
295
|
+
listeners.retainAll { it.get() != listener }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {
|
|
299
|
+
// When replacing the media on the same player, keep the surface hidden until the first frame of
|
|
300
|
+
// the new item is rendered. Otherwise the previous stream can remain visible (often mis-scaled)
|
|
301
|
+
// while the new stream buffers — e.g. switching from a higher-resolution URL to a lower one.
|
|
302
|
+
// See https://github.com/expo/expo/issues/44385
|
|
303
|
+
if (player == videoPlayer && oldSource != null) {
|
|
304
|
+
shouldHideSurfaceView = true
|
|
305
|
+
applySurfaceViewVisibility()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override fun onVideoSourceLoaded(
|
|
310
|
+
player: VideoPlayer,
|
|
311
|
+
videoSource: VideoSource?,
|
|
312
|
+
duration: Double?,
|
|
313
|
+
availableVideoTracks: List<VideoTrack>,
|
|
314
|
+
availableSubtitleTracks: List<SubtitleTrack>,
|
|
315
|
+
availableAudioTracks: List<AudioTrack>
|
|
316
|
+
) {
|
|
317
|
+
pipParams = pipParams.copy(aspectRatio = calculateCurrentPipAspectRatio())
|
|
318
|
+
super.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
|
|
322
|
+
if (player == videoPlayer && isPlaying) {
|
|
323
|
+
wasAutoPaused = false
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
|
|
328
|
+
currentTrackHasSubtitles = player.subtitles.availableSubtitleTracks.isNotEmpty()
|
|
329
|
+
showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
|
|
330
|
+
playerView.setShowSubtitleButton(buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
|
|
331
|
+
super.onTracksChanged(player, tracks)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
override fun onRenderedFirstFrame(player: VideoPlayer) {
|
|
335
|
+
if (player.currentVideoView == this) {
|
|
336
|
+
shouldHideSurfaceView = false
|
|
337
|
+
applySurfaceViewVisibility()
|
|
338
|
+
onFirstFrameRender(Unit)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
override fun requestLayout() {
|
|
343
|
+
super.requestLayout()
|
|
344
|
+
|
|
345
|
+
// Code borrowed from:
|
|
346
|
+
// https://github.com/facebook/react-native/blob/d19afc73f5048f81656d0b4424232ce6d69a6368/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java#L166
|
|
347
|
+
// This fixes some layout issues with the exoplayer which caused the resizeMode to not work properly
|
|
348
|
+
post(mLayoutRunnable)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
352
|
+
super.onLayout(changed, l, t, r, b)
|
|
353
|
+
// On every re-layout ExoPlayer resets the timeBar to be enabled.
|
|
354
|
+
// We need to disable it to keep scrubbing impossible.
|
|
355
|
+
|
|
356
|
+
pipParams = pipParams.copy(rectHint = calculateRectHint(playerView))
|
|
357
|
+
playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
override fun onAttachedToWindow() {
|
|
361
|
+
super.onAttachedToWindow()
|
|
362
|
+
|
|
363
|
+
// Set up listener for accessibility caption changes when attached to window
|
|
364
|
+
setupCaptioningChangeListener()
|
|
365
|
+
// Reconfigure when view is attached (handles returning from settings)
|
|
366
|
+
SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
|
|
367
|
+
|
|
368
|
+
// Set up window focus change listener
|
|
369
|
+
decorView.onFocusChangeListener = windowFocusChangeListener
|
|
370
|
+
|
|
371
|
+
pipParams = pipParams.copy(canEnter = true)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
|
375
|
+
super.onVisibilityChanged(changedView, visibility)
|
|
376
|
+
if (visibility == View.VISIBLE) {
|
|
377
|
+
// Reconfigure subtitles when view becomes visible (immediate response)
|
|
378
|
+
SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override fun onDetachedFromWindow() {
|
|
383
|
+
super.onDetachedFromWindow()
|
|
384
|
+
|
|
385
|
+
// Clean up captioning change listener
|
|
386
|
+
captioningChangeListener?.let {
|
|
387
|
+
val captioningManager = context.getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
|
|
388
|
+
captioningManager?.removeCaptioningChangeListener(it)
|
|
389
|
+
captioningChangeListener = null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Clean up window focus listener
|
|
393
|
+
decorView.onFocusChangeListener = null
|
|
394
|
+
|
|
395
|
+
pipParams = pipParams.copy(canEnter = false)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// After adding the `PlayerView` to the hierarchy the touch events stop being emitted to the JS side.
|
|
399
|
+
// The only workaround I have found is to dispatch the touch events manually using the `EventDispatcher`.
|
|
400
|
+
// The behavior is different when the native controls are enabled and disabled.
|
|
401
|
+
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
|
402
|
+
if (!useNativeControls) {
|
|
403
|
+
event?.eventTime?.let {
|
|
404
|
+
touchEventCoalescingKeyHelper.addCoalescingKey(it)
|
|
405
|
+
reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, event, touchEventCoalescingKeyHelper)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (event?.actionMasked == MotionEvent.ACTION_UP) {
|
|
409
|
+
performClick()
|
|
410
|
+
}
|
|
411
|
+
// Mark the event as handled
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
|
|
416
|
+
if (useNativeControls) {
|
|
417
|
+
event?.eventTime?.let {
|
|
418
|
+
touchEventCoalescingKeyHelper.addCoalescingKey(it)
|
|
419
|
+
reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, MotionEvent.obtainNoHistory(event), touchEventCoalescingKeyHelper)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Return false to receive all other events before the target `onTouchEvent`
|
|
423
|
+
return false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private fun getPlayerViewLayoutId(useTextureView: Boolean): Int {
|
|
427
|
+
return if (useTextureView) {
|
|
428
|
+
R.layout.texture_player_view
|
|
429
|
+
} else {
|
|
430
|
+
R.layout.surface_player_view
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private fun setupCaptioningChangeListener() {
|
|
435
|
+
val captioningManager = context.getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
|
|
436
|
+
|
|
437
|
+
captioningChangeListener = SubtitleUtils.createCaptioningChangeListener(playerView, context) { subtitleStyle }
|
|
438
|
+
|
|
439
|
+
captioningChangeListener?.let { listener ->
|
|
440
|
+
captioningManager?.addCaptioningChangeListener(listener)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private fun applyButtonSettings() {
|
|
445
|
+
val shouldShowSubtitle = buttonOptions.showSubtitles ?: currentTrackHasSubtitles
|
|
446
|
+
playerView.applyButtonOptions(buttonOptions, requiresLinearPlayback)
|
|
447
|
+
playerView.setShowSubtitleButton(shouldShowSubtitle)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
companion object {
|
|
451
|
+
fun isPictureInPictureSupported(currentActivity: Activity): Boolean {
|
|
452
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentActivity.packageManager.hasSystemFeature(
|
|
453
|
+
android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package expo.modules.video.delegates
|
|
2
|
+
|
|
3
|
+
import kotlin.reflect.KProperty
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Property delegate, where the set is ignored unless the value has changed.
|
|
7
|
+
* @param T The type of the property.
|
|
8
|
+
* @param value The initial value of the property.
|
|
9
|
+
* @param propertyMapper A function that maps the new value to the property value.
|
|
10
|
+
* @param didSet A function that is called when the property value has changed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class IgnoreSameSet<T : Any?>(private var value: T, val propertyMapper: ((T) -> T) = { v -> v }, val didSet: ((new: T, old: T) -> Unit)? = null) {
|
|
14
|
+
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
|
19
|
+
if (this.value?.equals(propertyMapper(value)) ?: false) return
|
|
20
|
+
val oldValue = this.value
|
|
21
|
+
this.value = propertyMapper(value)
|
|
22
|
+
didSet?.invoke(this.value, oldValue)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
package expo.modules.video.drawing
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Outline
|
|
6
|
+
import android.graphics.Path
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.view.ViewOutlineProvider
|
|
11
|
+
import com.facebook.react.modules.i18nmanager.I18nUtil
|
|
12
|
+
import com.facebook.react.uimanager.FloatUtil
|
|
13
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
14
|
+
import com.facebook.yoga.YogaConstants
|
|
15
|
+
import expo.modules.video.utils.ifYogaUndefinedUse
|
|
16
|
+
|
|
17
|
+
class OutlineProvider(private val mContext: Context) : ViewOutlineProvider() {
|
|
18
|
+
enum class BorderRadiusConfig {
|
|
19
|
+
ALL,
|
|
20
|
+
TOP_LEFT,
|
|
21
|
+
TOP_RIGHT,
|
|
22
|
+
BOTTOM_RIGHT,
|
|
23
|
+
BOTTOM_LEFT,
|
|
24
|
+
TOP_START,
|
|
25
|
+
TOP_END,
|
|
26
|
+
BOTTOM_START,
|
|
27
|
+
BOTTOM_END
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enum class CornerRadius {
|
|
31
|
+
TOP_LEFT,
|
|
32
|
+
TOP_RIGHT,
|
|
33
|
+
BOTTOM_RIGHT,
|
|
34
|
+
BOTTOM_LEFT
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private var mLayoutDirection = View.LAYOUT_DIRECTION_LTR
|
|
38
|
+
private val mBounds = RectF()
|
|
39
|
+
val borderRadiiConfig = FloatArray(9) { YogaConstants.UNDEFINED }
|
|
40
|
+
private val mCornerRadii = FloatArray(4)
|
|
41
|
+
private var mCornerRadiiInvalidated = true
|
|
42
|
+
private val mConvexPath = Path()
|
|
43
|
+
private var mConvexPathInvalidated = true
|
|
44
|
+
|
|
45
|
+
init {
|
|
46
|
+
updateCornerRadiiIfNeeded()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun updateCornerRadiiIfNeeded() {
|
|
50
|
+
if (!mCornerRadiiInvalidated) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL
|
|
55
|
+
val isRTLSwap = I18nUtil.instance.doLeftAndRightSwapInRTL(mContext)
|
|
56
|
+
updateCornerRadius(
|
|
57
|
+
CornerRadius.TOP_LEFT,
|
|
58
|
+
BorderRadiusConfig.TOP_LEFT,
|
|
59
|
+
BorderRadiusConfig.TOP_RIGHT,
|
|
60
|
+
BorderRadiusConfig.TOP_START,
|
|
61
|
+
BorderRadiusConfig.TOP_END,
|
|
62
|
+
isRTL,
|
|
63
|
+
isRTLSwap
|
|
64
|
+
)
|
|
65
|
+
updateCornerRadius(
|
|
66
|
+
CornerRadius.TOP_RIGHT,
|
|
67
|
+
BorderRadiusConfig.TOP_RIGHT,
|
|
68
|
+
BorderRadiusConfig.TOP_LEFT,
|
|
69
|
+
BorderRadiusConfig.TOP_END,
|
|
70
|
+
BorderRadiusConfig.TOP_START,
|
|
71
|
+
isRTL,
|
|
72
|
+
isRTLSwap
|
|
73
|
+
)
|
|
74
|
+
updateCornerRadius(
|
|
75
|
+
CornerRadius.BOTTOM_LEFT,
|
|
76
|
+
BorderRadiusConfig.BOTTOM_LEFT,
|
|
77
|
+
BorderRadiusConfig.BOTTOM_RIGHT,
|
|
78
|
+
BorderRadiusConfig.BOTTOM_START,
|
|
79
|
+
BorderRadiusConfig.BOTTOM_END,
|
|
80
|
+
isRTL,
|
|
81
|
+
isRTLSwap
|
|
82
|
+
)
|
|
83
|
+
updateCornerRadius(
|
|
84
|
+
CornerRadius.BOTTOM_RIGHT,
|
|
85
|
+
BorderRadiusConfig.BOTTOM_RIGHT,
|
|
86
|
+
BorderRadiusConfig.BOTTOM_LEFT,
|
|
87
|
+
BorderRadiusConfig.BOTTOM_END,
|
|
88
|
+
BorderRadiusConfig.BOTTOM_START,
|
|
89
|
+
isRTL,
|
|
90
|
+
isRTLSwap
|
|
91
|
+
)
|
|
92
|
+
mCornerRadiiInvalidated = false
|
|
93
|
+
mConvexPathInvalidated = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun updateCornerRadius(
|
|
97
|
+
outputPosition: CornerRadius,
|
|
98
|
+
inputPosition: BorderRadiusConfig,
|
|
99
|
+
oppositePosition: BorderRadiusConfig,
|
|
100
|
+
startPosition: BorderRadiusConfig,
|
|
101
|
+
endPosition: BorderRadiusConfig,
|
|
102
|
+
isRTL: Boolean,
|
|
103
|
+
isRTLSwap: Boolean
|
|
104
|
+
) {
|
|
105
|
+
var radius = borderRadiiConfig[inputPosition.ordinal]
|
|
106
|
+
if (isRTL) {
|
|
107
|
+
if (isRTLSwap) {
|
|
108
|
+
radius = borderRadiiConfig[oppositePosition.ordinal]
|
|
109
|
+
}
|
|
110
|
+
if (YogaConstants.isUndefined(radius)) {
|
|
111
|
+
radius = borderRadiiConfig[endPosition.ordinal]
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
if (YogaConstants.isUndefined(radius)) {
|
|
115
|
+
radius = borderRadiiConfig[startPosition.ordinal]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
radius = radius
|
|
119
|
+
.ifYogaUndefinedUse(borderRadiiConfig[BorderRadiusConfig.ALL.ordinal])
|
|
120
|
+
.ifYogaUndefinedUse(0f)
|
|
121
|
+
mCornerRadii[outputPosition.ordinal] = PixelUtil.toPixelFromDIP(radius)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private fun updateConvexPathIfNeeded() {
|
|
125
|
+
if (!mConvexPathInvalidated) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
mConvexPath.reset()
|
|
129
|
+
mConvexPath.addRoundRect(
|
|
130
|
+
mBounds,
|
|
131
|
+
floatArrayOf(
|
|
132
|
+
mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
|
|
133
|
+
mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
|
|
134
|
+
mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
|
|
135
|
+
mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
|
|
136
|
+
mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
|
|
137
|
+
mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
|
|
138
|
+
mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal],
|
|
139
|
+
mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal]
|
|
140
|
+
),
|
|
141
|
+
Path.Direction.CW
|
|
142
|
+
)
|
|
143
|
+
mConvexPathInvalidated = false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fun hasEqualCorners(): Boolean {
|
|
147
|
+
updateCornerRadiiIfNeeded()
|
|
148
|
+
val initialCornerRadius = mCornerRadii[0]
|
|
149
|
+
return mCornerRadii.all { initialCornerRadius == it }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fun setBorderRadius(radius: Float, position: Int): Boolean {
|
|
153
|
+
if (!FloatUtil.floatsEqual(borderRadiiConfig[position], radius)) {
|
|
154
|
+
borderRadiiConfig[position] = radius
|
|
155
|
+
mCornerRadiiInvalidated = true
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun updateBoundsAndLayoutDirection(view: View) {
|
|
162
|
+
// Update layout direction
|
|
163
|
+
val layoutDirection = view.layoutDirection
|
|
164
|
+
if (mLayoutDirection != layoutDirection) {
|
|
165
|
+
mLayoutDirection = layoutDirection
|
|
166
|
+
mCornerRadiiInvalidated = true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update size
|
|
170
|
+
val left = 0
|
|
171
|
+
val top = 0
|
|
172
|
+
val right = view.width
|
|
173
|
+
val bottom = view.height
|
|
174
|
+
if (mBounds.left != left.toFloat() ||
|
|
175
|
+
mBounds.top != top.toFloat() ||
|
|
176
|
+
mBounds.right != right.toFloat() ||
|
|
177
|
+
mBounds.bottom != bottom.toFloat()
|
|
178
|
+
) {
|
|
179
|
+
mBounds[left.toFloat(), top.toFloat(), right.toFloat()] = bottom.toFloat()
|
|
180
|
+
mCornerRadiiInvalidated = true
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
185
|
+
updateBoundsAndLayoutDirection(view)
|
|
186
|
+
|
|
187
|
+
// Calculate outline
|
|
188
|
+
updateCornerRadiiIfNeeded()
|
|
189
|
+
if (hasEqualCorners()) {
|
|
190
|
+
val cornerRadius = mCornerRadii[0]
|
|
191
|
+
if (cornerRadius > 0) {
|
|
192
|
+
outline.setRoundRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt(), cornerRadius)
|
|
193
|
+
} else {
|
|
194
|
+
outline.setRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt())
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Clipping is not supported when using a convex path, but drawing the elevation
|
|
198
|
+
// shadow is. For the particular case, we fallback to canvas clipping in the view
|
|
199
|
+
// which is supposed to call `clipCanvasIfNeeded` in its `draw` method.
|
|
200
|
+
updateConvexPathIfNeeded()
|
|
201
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
202
|
+
outline.setPath(mConvexPath)
|
|
203
|
+
} else {
|
|
204
|
+
outline.setConvexPath(mConvexPath)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fun clipCanvasIfNeeded(canvas: Canvas, view: View) {
|
|
210
|
+
updateBoundsAndLayoutDirection(view)
|
|
211
|
+
updateCornerRadiiIfNeeded()
|
|
212
|
+
if (!hasEqualCorners()) {
|
|
213
|
+
updateConvexPathIfNeeded()
|
|
214
|
+
canvas.clipPath(mConvexPath)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|