From ceb044d80d445c240bbb9c896ebc7cd658cb049a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C3=96zel?= Date: Sun, 31 May 2026 00:41:38 +0300 Subject: [PATCH] Modernize player screen lock experience --- app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 8 +- .../ozel/exoplayerscreenlock/MainActivity.kt | 288 +++++++++++------- app/src/main/res/drawable/bg_bottom_panel.xml | 8 + app/src/main/res/drawable/bg_control_pill.xml | 8 + .../main/res/drawable/bg_player_overlay.xml | 8 + .../main/res/drawable/bg_primary_control.xml | 4 + app/src/main/res/layout/activity_main.xml | 25 +- app/src/main/res/layout/custom_controller.xml | 173 +++++++---- app/src/main/res/values-night/themes.xml | 31 +- app/src/main/res/values/colors.xml | 19 +- app/src/main/res/values/strings.xml | 13 +- app/src/main/res/values/styles.xml | 27 +- app/src/main/res/values/themes.xml | 27 +- gradle.properties | 2 +- 15 files changed, 420 insertions(+), 233 deletions(-) create mode 100644 app/src/main/res/drawable/bg_bottom_panel.xml create mode 100644 app/src/main/res/drawable/bg_control_pill.xml create mode 100644 app/src/main/res/drawable/bg_player_overlay.xml create mode 100644 app/src/main/res/drawable/bg_primary_control.xml diff --git a/app/build.gradle b/app/build.gradle index 80b8a5f..17eccd4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,12 @@ plugins { } android { - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.halil.ozel.exoplayerscreenlock" minSdk 24 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" @@ -41,6 +41,8 @@ dependencies { implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - // ExoPlayer - implementation 'com.google.android.exoplayer:exoplayer:2.19.1' -} \ No newline at end of file + // Media3 (modern ExoPlayer) + implementation 'androidx.media3:media3-exoplayer:1.8.1' + implementation 'androidx.media3:media3-exoplayer-hls:1.8.1' + implementation 'androidx.media3:media3-ui:1.8.1' +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04a0277..5069f8e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,9 @@ - + @@ -24,4 +22,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/halil/ozel/exoplayerscreenlock/MainActivity.kt b/app/src/main/java/com/halil/ozel/exoplayerscreenlock/MainActivity.kt index accfdeb..c3f92d4 100644 --- a/app/src/main/java/com/halil/ozel/exoplayerscreenlock/MainActivity.kt +++ b/app/src/main/java/com/halil/ozel/exoplayerscreenlock/MainActivity.kt @@ -7,152 +7,228 @@ import android.os.Bundle import android.view.View import android.widget.ImageView import android.widget.LinearLayout +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.source.hls.HlsMediaSource -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.media3.common.MediaItem +import androidx.media3.exoplayer.ExoPlayer import com.halil.ozel.exoplayerscreenlock.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { - private var playbackPosition = 0L - private lateinit var binding: ActivityMainBinding - private var exoPlayer: ExoPlayer? = null - private lateinit var imageViewFullScreen: ImageView - private lateinit var imageViewLock: ImageView - private lateinit var linearLayoutControlUp: LinearLayout - private lateinit var linearLayoutControlBottom: LinearLayout + private var player: ExoPlayer? = null + + private lateinit var fullScreenButton: ImageView + private lateinit var lockButton: ImageView + private lateinit var topControls: LinearLayout + private lateinit var bottomControls: LinearLayout + + private var playbackPosition = 0L + private var playWhenReady = true + private var isFullScreen = false + private var isLocked = false - @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + restorePlayerState(savedInstanceState) + configureEdgeToEdgePlayer() setView() - preparePlayer() - setFindViewById() - setLockScreen() - setFullScreen() + bindControllerViews() + setupControls() + setupBackNavigation() + initializePlayer() + } + + override fun onStart() { + super.onStart() + if (player == null) initializePlayer() + } + + override fun onResume() { + super.onResume() + if (player == null) initializePlayer() + updateSystemBars() + player?.playWhenReady = playWhenReady + } + + override fun onPause() { + savePlaybackState() + player?.pause() + super.onPause() + } + + override fun onStop() { + savePlaybackState() + releasePlayer() + super.onStop() + } + + override fun onDestroy() { + releasePlayer() + super.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + savePlaybackState() + outState.putLong(KEY_PLAYBACK_POSITION, playbackPosition) + outState.putBoolean(KEY_PLAY_WHEN_READY, playWhenReady) + outState.putBoolean(KEY_IS_FULLSCREEN, isFullScreen) + outState.putBoolean(KEY_IS_LOCKED, isLocked) + super.onSaveInstanceState(outState) } private fun setView() { binding = ActivityMainBinding.inflate(layoutInflater) - val view = binding.root - setContentView(view) + setContentView(binding.root) } - private fun setFindViewById() { - imageViewFullScreen = findViewById(R.id.imageViewFullScreen) - imageViewLock = findViewById(R.id.imageViewLock) - linearLayoutControlUp = findViewById(R.id.linearLayoutControlUp) - linearLayoutControlBottom = findViewById(R.id.linearLayoutControlBottom) + private fun configureEdgeToEdgePlayer() { + WindowCompat.setDecorFitsSystemWindows(window, false) + WindowCompat.getInsetsController(window, window.decorView).systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } - private fun preparePlayer() { - exoPlayer = ExoPlayer.Builder(this).setSeekBackIncrementMs(INCREMENT_MILLIS) - .setSeekForwardIncrementMs(INCREMENT_MILLIS) - .build() - exoPlayer?.playWhenReady = true - binding.player.player = exoPlayer - val defaultHttpDataSourceFactory = DefaultHttpDataSource.Factory() - val mediaItem = - MediaItem.fromUri(URL) - val mediaSource = - HlsMediaSource.Factory(defaultHttpDataSourceFactory).createMediaSource(mediaItem) - exoPlayer?.apply { - setMediaSource(mediaSource) - seekTo(playbackPosition) - playWhenReady = playWhenReady - prepare() - } + private fun bindControllerViews() { + fullScreenButton = findViewById(R.id.imageViewFullScreen) + lockButton = findViewById(R.id.imageViewLock) + topControls = findViewById(R.id.linearLayoutControlUp) + bottomControls = findViewById(R.id.linearLayoutControlBottom) } + private fun setupControls() { + updateLockState() + updateFullScreenState() - private fun lockScreen(lock: Boolean) { - if (lock) { - linearLayoutControlUp.visibility = View.INVISIBLE - linearLayoutControlBottom.visibility = View.INVISIBLE - } else { - linearLayoutControlUp.visibility = View.VISIBLE - linearLayoutControlBottom.visibility = View.VISIBLE + lockButton.setOnClickListener { + isLocked = !isLocked + updateLockState() + } + + fullScreenButton.setOnClickListener { + toggleFullScreen() } } - private fun setLockScreen() { - imageViewLock.setOnClickListener { - if (!isLock) { - imageViewLock.setImageDrawable( - ContextCompat.getDrawable( - applicationContext, - R.drawable.ic_baseline_lock - ) - ) - } else { - imageViewLock.setImageDrawable( - ContextCompat.getDrawable( - applicationContext, - R.drawable.ic_baseline_lock_open - ) - ) + private fun setupBackNavigation() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + when { + isLocked -> Unit + isFullScreen || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -> + exitFullScreen() + else -> { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + } } - isLock = !isLock - lockScreen(isLock) - } + ) } - @SuppressLint("SourceLockedOrientationActivity") - private fun setFullScreen() { - imageViewFullScreen.setOnClickListener { - if (!isFullScreen) { - imageViewFullScreen.setImageDrawable( - ContextCompat.getDrawable( - applicationContext, - R.drawable.ic_baseline_fullscreen_exit - ) - ) - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - } else { - imageViewFullScreen.setImageDrawable( - ContextCompat.getDrawable( - applicationContext, - R.drawable.ic_baseline_fullscreen - ) - ) - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + private fun restorePlayerState(savedInstanceState: Bundle?) { + savedInstanceState ?: return + playbackPosition = savedInstanceState.getLong(KEY_PLAYBACK_POSITION, 0L) + playWhenReady = savedInstanceState.getBoolean(KEY_PLAY_WHEN_READY, true) + isFullScreen = savedInstanceState.getBoolean(KEY_IS_FULLSCREEN, false) + isLocked = savedInstanceState.getBoolean(KEY_IS_LOCKED, false) + } + + private fun initializePlayer() { + if (player != null) return + + player = ExoPlayer.Builder(this) + .setSeekBackIncrementMs(SEEK_INCREMENT_MILLIS) + .setSeekForwardIncrementMs(SEEK_INCREMENT_MILLIS) + .build() + .also { exoPlayer -> + binding.player.player = exoPlayer + exoPlayer.setMediaItem(MediaItem.fromUri(STREAM_URL)) + exoPlayer.seekTo(playbackPosition) + exoPlayer.playWhenReady = playWhenReady + exoPlayer.prepare() } - isFullScreen = !isFullScreen + } + + private fun savePlaybackState() { + player?.let { exoPlayer -> + playbackPosition = exoPlayer.currentPosition.coerceAtLeast(0L) + playWhenReady = exoPlayer.playWhenReady } } - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (isLock) return - if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - imageViewFullScreen.performClick() - } else super.onBackPressed() + private fun releasePlayer() { + savePlaybackState() + binding.player.player = null + player?.release() + player = null } + private fun updateLockState() { + val controlsVisibility = if (isLocked) View.INVISIBLE else View.VISIBLE + topControls.visibility = controlsVisibility + bottomControls.visibility = controlsVisibility + lockButton.setImageResource( + if (isLocked) R.drawable.ic_baseline_lock else R.drawable.ic_baseline_lock_open + ) + lockButton.contentDescription = getString( + if (isLocked) R.string.unlock_controls else R.string.lock_controls + ) + } - override fun onStop() { - super.onStop() - exoPlayer?.stop() + @SuppressLint("SourceLockedOrientationActivity") + private fun toggleFullScreen() { + if (isFullScreen) { + exitFullScreen() + } else { + isFullScreen = true + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + updateFullScreenState() + } } - override fun onDestroy() { - super.onDestroy() - exoPlayer?.release() + @SuppressLint("SourceLockedOrientationActivity") + private fun exitFullScreen() { + isFullScreen = false + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + updateFullScreenState() } - override fun onPause() { - super.onPause() - exoPlayer?.pause() + private fun updateFullScreenState() { + fullScreenButton.setImageDrawable( + ContextCompat.getDrawable( + this, + if (isFullScreen) R.drawable.ic_baseline_fullscreen_exit else R.drawable.ic_baseline_fullscreen + ) + ) + fullScreenButton.contentDescription = getString( + if (isFullScreen) R.string.exit_full_screen else R.string.enter_full_screen + ) + updateSystemBars() + } + + private fun updateSystemBars() { + val controller = WindowCompat.getInsetsController(window, window.decorView) + if (isFullScreen || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + controller.hide(WindowInsetsCompat.Type.systemBars()) + } else { + controller.show(WindowInsetsCompat.Type.systemBars()) + } } companion object { - private const val URL = "https://d1gnaphp93fop2.cloudfront.net/videos/multiresolution/rendition_new10.m3u8" - private var isFullScreen = false - private var isLock = false - private const val INCREMENT_MILLIS = 5000L + private const val STREAM_URL = + "https://d1gnaphp93fop2.cloudfront.net/videos/multiresolution/rendition_new10.m3u8" + private const val SEEK_INCREMENT_MILLIS = 5_000L + private const val KEY_PLAYBACK_POSITION = "playback_position" + private const val KEY_PLAY_WHEN_READY = "play_when_ready" + private const val KEY_IS_FULLSCREEN = "is_fullscreen" + private const val KEY_IS_LOCKED = "is_locked" } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/bg_bottom_panel.xml b/app/src/main/res/drawable/bg_bottom_panel.xml new file mode 100644 index 0000000..24b8c45 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_panel.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_control_pill.xml b/app/src/main/res/drawable/bg_control_pill.xml new file mode 100644 index 0000000..96b3a4d --- /dev/null +++ b/app/src/main/res/drawable/bg_control_pill.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_player_overlay.xml b/app/src/main/res/drawable/bg_player_overlay.xml new file mode 100644 index 0000000..15b6a61 --- /dev/null +++ b/app/src/main/res/drawable/bg_player_overlay.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/bg_primary_control.xml b/app/src/main/res/drawable/bg_primary_control.xml new file mode 100644 index 0000000..080c864 --- /dev/null +++ b/app/src/main/res/drawable/bg_primary_control.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4c111ab..747340d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,15 +1,24 @@ - + android:layout_height="match_parent" + android:background="@color/player_surface"> - - \ No newline at end of file + + diff --git a/app/src/main/res/layout/custom_controller.xml b/app/src/main/res/layout/custom_controller.xml index 52a19d4..083d74f 100644 --- a/app/src/main/res/layout/custom_controller.xml +++ b/app/src/main/res/layout/custom_controller.xml @@ -1,103 +1,142 @@ - + android:background="@drawable/bg_player_overlay" + android:paddingStart="20dp" + android:paddingTop="20dp" + android:paddingEnd="20dp" + android:paddingBottom="20dp"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + - - + android:paddingStart="14dp" + android:paddingTop="10dp" + android:paddingEnd="14dp" + android:paddingBottom="10dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + android:id="@id/exo_rew" + style="@style/PlayerControlButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:contentDescription="@string/rewind" + android:src="@drawable/ic_baseline_replay" /> + + + + + + + + + android:id="@id/exo_ffwd" + style="@style/PlayerControlButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:contentDescription="@string/fast_forward" + android:src="@drawable/ic_baseline_forward" /> + android:paddingStart="16dp" + android:paddingTop="12dp" + android:paddingEnd="16dp" + android:paddingBottom="14dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + android:gravity="center_vertical" + android:orientation="horizontal"> + + android:layout_height="wrap_content" /> + + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:text="@string/time_separator" /> + + android:layout_weight="1" /> + + style="@style/PlayerControlButton" + android:layout_width="44dp" + android:layout_height="44dp" + android:contentDescription="@string/enter_full_screen" + android:src="@drawable/ic_baseline_fullscreen" /> - + android:layout_height="32dp" + android:layout_marginTop="8dp" + app:bar_height="4dp" + app:buffered_color="@color/player_buffered" + app:played_color="@color/player_accent" + app:scrubber_color="@color/player_accent" + app:scrubber_dragged_size="18dp" + app:scrubber_enabled_size="14dp" + app:unplayed_color="@color/player_unplayed" /> - \ No newline at end of file + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 580ad18..06d37c8 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,16 +1,17 @@ - - - - \ No newline at end of file + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..fff3871 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,17 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF - \ No newline at end of file + + #05070D + #FF3D71 + #FFFFFFFF + #F7F8FA + #B9C0CC + #B3121722 + #26FFFFFF + #A6000000 + #33000000 + #8A9AA8B5 + #595F6B78 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6f521d..33a465c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,12 @@ - ExoPlayerScreenLock - \ No newline at end of file + ExoPlayer Screen Lock + Tam ekrana geç + Tam ekrandan çık + Kontrolleri kilitle + Kontrollerin kilidini aç + Oynat + Duraklat + 5 saniye geri sar + 5 saniye ileri sar + / + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 96608e4..d5e6f0b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,7 +2,30 @@ - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 203fa1b..06d37c8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,22 +1,17 @@ - - - - - \ No newline at end of file + diff --git a/gradle.properties b/gradle.properties index 022338b..cccbfe6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,4 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false