feat: 暂停显示选集、亮度音量手势调节、播放进度续播

This commit is contained in:
xiaji
2026-05-27 22:33:46 +08:00
parent 98e96f3438
commit eb72b061e6
7 changed files with 269 additions and 15 deletions

View File

@@ -1,8 +1,11 @@
package com.videoapp.tv
import android.content.Context
import android.media.AudioManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
@@ -10,10 +13,12 @@ import android.widget.Button
import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.videoapp.tv.data.AppDatabase
@@ -33,29 +38,49 @@ class PlayerActivity : AppCompatActivity() {
private lateinit var episodeList: LinearLayout
private lateinit var episodeScroll: HorizontalScrollView
private lateinit var loadingIndicator: View
private lateinit var errorText: android.widget.TextView
private lateinit var errorText: TextView
private lateinit var btnClose: ImageButton
private lateinit var brightnessVolumeIndicator: TextView
private var exoPlayer: ExoPlayer? = null
private val videoExtractor = VideoExtractor()
private val configRepo by lazy { ConfigRepository(this) }
private val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager }
private var sources: List<PlaySource> = emptyList()
private var currentSourceIndex = 0
private var currentEpisode: Episode? = null
private var historyEpisode: String? = null
private var resumePosition: Long? = null
private var currentPlayHistoryId: Long? = null
private val hideHandler = Handler(Looper.getMainLooper())
private val hideRunnable = Runnable { hideControls() }
private var controlsVisible = true
// 用于保存播放历史的信息
private val positionSaveHandler = Handler(Looper.getMainLooper())
private val positionSaveRunnable = object : Runnable {
override fun run() {
saveCurrentPosition()
positionSaveHandler.postDelayed(this, 5000)
}
}
private var videoTitle: String = ""
private var videoCategory: String? = null
private var coverUrl: String? = null
private var detailUrl: String = ""
private var touchStartY = 0f
private var touchStartX = 0f
private var isAdjusting = false
private var isBrightnessMode = false
private var startBrightness = 0f
private var startVolume = 0
private var maxVolume = 0
private val indicatorHideHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
@@ -69,12 +94,16 @@ class PlayerActivity : AppCompatActivity() {
loadingIndicator = findViewById(R.id.loading_indicator)
errorText = findViewById(R.id.error_text)
btnClose = findViewById(R.id.btn_close)
brightnessVolumeIndicator = findViewById(R.id.brightness_volume_indicator)
detailUrl = intent.getStringExtra("detail_url") ?: ""
videoTitle = intent.getStringExtra("title") ?: ""
videoCategory = intent.getStringExtra("category")
coverUrl = intent.getStringExtra("cover_url")
historyEpisode = intent.getStringExtra("history_episode")
resumePosition = if (intent.hasExtra("resume_position")) intent.getLongExtra("resume_position", 0) else null
maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
btnClose.setOnClickListener { finish() }
@@ -86,6 +115,16 @@ class PlayerActivity : AppCompatActivity() {
private fun initExoPlayer() {
exoPlayer = ExoPlayer.Builder(this).build().also { player ->
playerView.player = player
player.addListener(object : Player.Listener {
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
if (playWhenReady) {
hideHandler.postDelayed(hideRunnable, 4000)
} else {
hideHandler.removeCallbacks(hideRunnable)
showControls()
}
}
})
}
}
@@ -193,7 +232,6 @@ class PlayerActivity : AppCompatActivity() {
showLoading(true)
val config = configRepo.getConfig()
// 保存播放历史
savePlayHistory(ep.title)
lifecycleScope.launch {
@@ -212,13 +250,11 @@ class PlayerActivity : AppCompatActivity() {
private fun savePlayHistory(episodeName: String?) {
lifecycleScope.launch {
try {
// 检查是否已存在相同的播放记录
val existing = playHistoryDao.findByDetailUrl(detailUrl)
if (existing != null) {
// 更新现有记录的播放时间和剧集
playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName)
currentPlayHistoryId = existing.id
} else {
// 插入新记录
val playHistory = PlayHistory(
title = videoTitle,
episodeName = episodeName,
@@ -227,19 +263,43 @@ class PlayerActivity : AppCompatActivity() {
category = videoCategory,
playTime = System.currentTimeMillis()
)
playHistoryDao.insert(playHistory)
val id = playHistoryDao.insert(playHistory)
currentPlayHistoryId = id
}
} catch (e: Exception) {
// 保存播放历史失败不影响播放功能
e.printStackTrace()
}
}
}
private fun saveCurrentPosition() {
val player = exoPlayer ?: return
val historyId = currentPlayHistoryId ?: return
if (playerView.visibility != View.VISIBLE) return
val pos = player.currentPosition
if (pos > 0) {
lifecycleScope.launch {
try {
playHistoryDao.updatePosition(historyId, pos)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
private fun startPositionSaving() {
positionSaveHandler.removeCallbacks(positionSaveRunnable)
positionSaveHandler.postDelayed(positionSaveRunnable, 5000)
}
private fun stopPositionSaving() {
positionSaveHandler.removeCallbacks(positionSaveRunnable)
}
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
// 保存播放历史(直接播放时使用标题作为剧集名)
savePlayHistory(videoTitle)
lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
if (directUrl != null) {
@@ -265,10 +325,17 @@ class PlayerActivity : AppCompatActivity() {
setMediaItem(mediaItem)
prepare()
playWhenReady = true
if (resumePosition != null && resumePosition!! > 0) {
seekTo(resumePosition!!)
resumePosition = null
}
}
startPositionSaving()
}
private fun playWithWebView(url: String) {
stopPositionSaving()
playerView.visibility = View.GONE
playerWebView.visibility = View.VISIBLE
showLoading(false)
@@ -302,9 +369,60 @@ class PlayerActivity : AppCompatActivity() {
}
private fun setupTouchListeners() {
val listener = View.OnClickListener { toggleControls() }
playerView.setOnClickListener(listener)
playerWebView.setOnClickListener(listener)
val touchListener = View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
touchStartX = event.x
touchStartY = event.y
isAdjusting = false
val halfWidth = view.width / 2
isBrightnessMode = touchStartX < halfWidth
startBrightness = window.attributes.screenBrightness
if (startBrightness < 0) startBrightness = 0.5f
startVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
true
}
MotionEvent.ACTION_MOVE -> {
val deltaY = touchStartY - event.y
if (kotlin.math.abs(event.y - touchStartY) > 10f) {
isAdjusting = true
}
if (isAdjusting) {
if (isBrightnessMode) {
val newBrightness = (startBrightness + deltaY / view.height).coerceIn(0.01f, 1.0f)
window.attributes.screenBrightness = newBrightness
window.attributes = window.attributes
showIndicator("亮度", (newBrightness * 100).toInt())
} else {
val range = maxVolume.toFloat()
val newVolume = (startVolume + (deltaY / view.height) * range).toInt().coerceIn(0, maxVolume)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)
showIndicator("音量", (newVolume * 100 / maxVolume))
}
}
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
val movedFar = kotlin.math.abs(event.x - touchStartX) > 20f || kotlin.math.abs(event.y - touchStartY) > 20f
if (!isAdjusting && !movedFar) {
toggleControls()
}
true
}
else -> false
}
}
playerView.setOnTouchListener(touchListener)
playerWebView.setOnTouchListener(touchListener)
}
private fun showIndicator(label: String, percent: Int) {
brightnessVolumeIndicator.text = "$label: $percent%"
brightnessVolumeIndicator.visibility = View.VISIBLE
indicatorHideHandler.removeCallbacksAndMessages(null)
indicatorHideHandler.postDelayed({
brightnessVolumeIndicator.visibility = View.GONE
}, 1500)
}
private fun toggleControls() {
@@ -338,11 +456,15 @@ class PlayerActivity : AppCompatActivity() {
override fun onPause() {
super.onPause()
saveCurrentPosition()
stopPositionSaving()
exoPlayer?.playWhenReady = false
}
override fun onDestroy() {
super.onDestroy()
saveCurrentPosition()
stopPositionSaving()
hideHandler.removeCallbacks(hideRunnable)
exoPlayer?.release()
exoPlayer = null

View File

@@ -11,5 +11,6 @@ data class PlayHistory(
val detailUrl: String,
val coverUrl: String? = null,
val category: String? = null,
val playTime: Long = System.currentTimeMillis()
val playTime: Long = System.currentTimeMillis(),
val playbackPosition: Long? = null
)

View File

@@ -13,7 +13,7 @@ interface PlayHistoryDao {
fun getRecentPlayHistory(): Flow<List<PlayHistory>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(playHistory: PlayHistory)
suspend fun insert(playHistory: PlayHistory): Long
@Query("DELETE FROM play_history WHERE detailUrl = :detailUrl")
suspend fun deleteByDetailUrl(detailUrl: String)
@@ -26,4 +26,7 @@ interface PlayHistoryDao {
@Query("UPDATE play_history SET playTime = :playTime, episodeName = :episodeName WHERE id = :id")
suspend fun updatePlayTime(id: Long, playTime: Long, episodeName: String?)
@Query("UPDATE play_history SET playbackPosition = :position WHERE id = :id")
suspend fun updatePosition(id: Long, position: Long)
}

View File

@@ -226,6 +226,9 @@ class SearchFragment : Fragment() {
putExtra("category", playHistory.category)
putExtra("cover_url", playHistory.coverUrl)
putExtra("history_episode", playHistory.episodeName)
if (playHistory.playbackPosition != null && playHistory.playbackPosition > 0) {
putExtra("resume_position", playHistory.playbackPosition)
}
}
startActivity(intent)
}

View File

@@ -89,4 +89,15 @@
android:textSize="16sp"
android:visibility="gone" />
<TextView
android:id="@+id/brightness_volume_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#CC000000"
android:padding="16dp"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:visibility="gone" />
</FrameLayout>