feat(android): optimizes video controls for small video players (#17)

Co-authored-by: Pieczasz <bartekp854@gmail.com>
Co-authored-by: Krzysztof Moch <krzysmoch.programs@gmail.com>
This commit is contained in:
pieczasz-thewidlarzgroup
2025-06-25 21:23:19 +02:00
committed by GitHub
parent 146471d23c
commit 6baa1e4f4a
3 changed files with 187 additions and 0 deletions
@@ -1,6 +1,7 @@
package com.video.core.fragments
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
@@ -9,6 +10,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageButton
import androidx.activity.OnBackPressedCallback
@@ -17,6 +19,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import com.video.core.utils.PictureInPictureUtils.createPictureInPictureParams
import com.video.core.utils.SmallVideoPlayerOptimizer
import com.video.view.VideoView
import java.util.UUID
@@ -131,6 +134,9 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() {
setupFullscreenButton()
videoView.playerView.setShowSubtitleButton(true)
// Apply optimizations based on video player size in fullscreen mode
SmallVideoPlayerOptimizer.applyOptimizations(videoView.playerView, requireContext(), isFullscreen = true)
}
@SuppressLint("PrivateResource")
@@ -220,6 +226,8 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() {
videoView.isInFullscreen = false
}
override fun onDestroy() {
super.onDestroy()
@@ -0,0 +1,157 @@
package com.video.core.utils
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.media3.ui.PlayerView
object SmallVideoPlayerOptimizer {
fun isSmallVideoPlayer(playerView: PlayerView): Boolean {
// Check if the PlayerView dimensions are small enough to warrant optimizations
val width = playerView.width
val height = playerView.height
// If view hasn't been measured yet, use layout params or return false
if (width <= 0 || height <= 0) {
val layoutParams = playerView.layoutParams
if (layoutParams != null) {
// Convert any specific dimensions to pixels if needed
val widthPx = if (layoutParams.width > 0) layoutParams.width else playerView.measuredWidth
val heightPx = if (layoutParams.height > 0) layoutParams.height else playerView.measuredHeight
if (widthPx <= 0 || heightPx <= 0) return false
return isSmallDimensions(widthPx, heightPx, playerView.context)
}
return false
}
return isSmallDimensions(width, height, playerView.context)
}
private fun isSmallDimensions(widthPx: Int, heightPx: Int, context: Context): Boolean {
val density = context.resources.displayMetrics.density
val widthDp = widthPx / density
val heightDp = heightPx / density
// Consider the video player "small" if width <= 400dp or height <= 300dp
// These thresholds are more appropriate for actual video player sizes
return widthDp <= 400 || heightDp <= 300
}
fun applyOptimizations(
playerView: PlayerView,
context: Context,
isFullscreen: Boolean = false
) {
playerView.post {
try {
if (isFullscreen) {
// For fullscreen mode, use system defaults - no custom optimizations
// Let ExoPlayer use its default controller timeout and styling
return@post
}
// Only apply optimizations if the video player itself is small
if (!isSmallVideoPlayer(playerView)) {
return@post
}
val controllerView = playerView.findViewById<ViewGroup>(androidx.media3.ui.R.id.exo_controller)
controllerView?.let { controller ->
optimizeControlElementsForSmallPlayer(controller, context)
}
} catch (e: Exception) {
Log.w("ReactNativeVideo", "Error applying small video player optimizations: ${e.message}")
}
}
}
private fun optimizeControlElementsForSmallPlayer(
controller: ViewGroup,
context: Context
) {
val density = context.resources.displayMetrics.density
val primaryButtonSize = (48 * density).toInt()
val secondaryButtonSize = (44 * density).toInt()
optimizeButtons(controller, primaryButtonSize, secondaryButtonSize)
optimizeProgressBar(controller, context)
optimizeTextElements(controller)
}
private fun optimizeButtons(
container: ViewGroup,
primarySize: Int,
secondarySize: Int
) {
for (i in 0 until container.childCount) {
val child = container.getChildAt(i)
when (child) {
is ImageButton -> {
val buttonSize = when (child.id) {
androidx.media3.ui.R.id.exo_play_pause -> primarySize
androidx.media3.ui.R.id.exo_fullscreen -> primarySize
androidx.media3.ui.R.id.exo_settings -> primarySize
androidx.media3.ui.R.id.exo_rew -> secondarySize
androidx.media3.ui.R.id.exo_ffwd -> secondarySize
androidx.media3.ui.R.id.exo_subtitle -> secondarySize
androidx.media3.ui.R.id.exo_prev -> secondarySize
androidx.media3.ui.R.id.exo_next -> secondarySize
else -> secondarySize
}
val params = child.layoutParams
params.width = buttonSize
params.height = buttonSize
child.layoutParams = params
// Hide less essential buttons on small video players
when (child.id) {
androidx.media3.ui.R.id.exo_shuffle,
androidx.media3.ui.R.id.exo_repeat_toggle,
androidx.media3.ui.R.id.exo_vr -> {
child.visibility = View.GONE
}
}
}
is ViewGroup -> {
optimizeButtons(child, primarySize, secondarySize)
}
}
}
}
private fun optimizeProgressBar(
controller: ViewGroup,
context: Context
) {
val progressContainer = controller.findViewById<View>(androidx.media3.ui.R.id.exo_progress)
progressContainer?.let { progress ->
val params = progress.layoutParams as? ViewGroup.MarginLayoutParams
params?.let {
it.height = (4 * context.resources.displayMetrics.density).toInt()
progress.layoutParams = it
}
}
}
private fun optimizeTextElements(
controller: ViewGroup
) {
val timeContainer = controller.findViewById<ViewGroup>(androidx.media3.ui.R.id.exo_time)
timeContainer?.let { time ->
val positionView = time.findViewById<View>(androidx.media3.ui.R.id.exo_position)
val durationView = time.findViewById<View>(androidx.media3.ui.R.id.exo_duration)
listOf(positionView, durationView).forEach { textView ->
if (textView is android.widget.TextView) {
textView.textSize = 12f
}
}
}
}
}
@@ -6,9 +6,11 @@ import android.content.Context
import android.graphics.Color
import android.os.Build
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageButton
import androidx.annotation.RequiresApi
@@ -32,6 +34,7 @@ import com.video.core.utils.Threading.runOnMainThread
import com.video.core.extensions.toAspectRatioFrameLayout
import com.video.core.utils.PictureInPictureUtils
import com.video.core.utils.PictureInPictureUtils.createDisabledPictureInPictureParams
import com.video.core.utils.SmallVideoPlayerOptimizer
@UnstableApi
class VideoView @JvmOverloads constructor(
@@ -109,6 +112,9 @@ class VideoView @JvmOverloads constructor(
setShutterBackgroundColor(Color.TRANSPARENT)
setShowSubtitleButton(true)
useController = false
// Apply optimizations based on video player size if needed
configureForSmallPlayer()
}
var isInFullscreen: Boolean = false
set(value) {
@@ -145,6 +151,9 @@ class VideoView @JvmOverloads constructor(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
layout(left, top, right, bottom)
// Additional layout fixes for small video players
applySmallPlayerLayoutFixes()
}
override fun requestLayout() {
@@ -363,4 +372,17 @@ class VideoView @JvmOverloads constructor(
setupPipHelper()
super.onAttachedToWindow()
}
private fun PlayerView.configureForSmallPlayer() {
SmallVideoPlayerOptimizer.applyOptimizations(this, context, isFullscreen = false)
// Also apply after any layout changes
viewTreeObserver.addOnGlobalLayoutListener {
SmallVideoPlayerOptimizer.applyOptimizations(this, context, isFullscreen = false)
}
}
private fun applySmallPlayerLayoutFixes() {
SmallVideoPlayerOptimizer.applyOptimizations(playerView, context, isFullscreen = false)
}
}