视频组件自定义

This commit is contained in:
2026-06-03 17:17:44 +08:00
parent b1aa844ff6
commit 60ecdf140b
17 changed files with 626 additions and 64 deletions

View File

@@ -104,4 +104,5 @@ dependencies {
//基于exoPlayer 自定义播放器 JPlayer支持功能
implementation("com.ycjiang:VideoPlayModule-Lite:2.3.61")
}

View File

@@ -6,7 +6,10 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
@@ -16,6 +19,7 @@ import chuangyuan.ycj.videolibrary.listener.VideoInfoListener
import chuangyuan.ycj.videolibrary.video.ExoUserPlayer
import chuangyuan.ycj.videolibrary.video.VideoPlayerManager
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.SimpleExoPlayer
import com.example.defenseapplication.R
import com.example.defenseapplication.adapter.AlarmLogAdapter
import com.example.defenseapplication.bean.DeviceAlarmBean
@@ -24,6 +28,8 @@ import com.example.defenseapplication.databinding.AlarmLogActivityBinding
import com.example.defenseapplication.utils.RetrofitNetwork
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.launch
import android.os.Handler
import android.os.Looper
import java.text.SimpleDateFormat
import java.util.*
import com.bigkoo.pickerview.builder.TimePickerBuilder
@@ -38,23 +44,38 @@ class AlarmLogActivity : AppCompatActivity() {
private val originalAlarmList = mutableListOf<DeviceAlarmBean>()
private lateinit var exoPlayerManager: ExoUserPlayer
private var isMuted = false
private var isVoiceEnabled = false
private var isLightOn = false
private var isPlaying = false
private var deviceId: String? = null
private var isLoading = false
private var fromLiveVideo = false
private var isFullScreen = false
private var isControlsVisible = false
private val hideDelay = 5000L // 5秒自动隐藏
private val hideHandler = Handler(Looper.getMainLooper())
private val hideRunnable = Runnable {
hideControls()
}
private var passedVideoUrl: String? = null
private var passedSelectedId: String? = null
private var passedCreateTime: String? = null
private var totalDuration: Long = 0
private val progressHandler = Handler(Looper.getMainLooper())
private val progressRunnable = object : Runnable {
override fun run() {
updateProgress()
progressHandler.postDelayed(this, 1000)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AlarmLogActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
ImmersionBar.with(this)
.statusBarDarkFont(true)
.transparentStatusBar()
.init()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
deviceId = intent.getStringExtra("device_id")
fromLiveVideo = intent.getBooleanExtra("from_live_video", false)
@@ -68,14 +89,14 @@ class AlarmLogActivity : AppCompatActivity() {
fetchAlarmList()
initListener()
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (exoPlayerManager.onBackPressed()) {
return
}
finish()
}
})
// onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
// if (exoPlayerManager.onBackPressed()) {
// return
// }
// finish()
// }
// })
}
private fun dpToPx(dp: Int): Int {
@@ -97,6 +118,9 @@ class AlarmLogActivity : AppCompatActivity() {
.addVideoInfoListener(object : VideoInfoListener {
override fun onPlayStart(p0: Long) {
Log.d("AlarmLogActivity", "视频开始播放")
startProgressUpdate()
showControls()
resetHideTimer()
}
override fun onLoadingChanged() {
@@ -108,9 +132,24 @@ class AlarmLogActivity : AppCompatActivity() {
override fun onPlayEnd() {
Log.d("AlarmLogActivity", "播放结束")
stopProgressUpdate()
hideControls()
}
override fun isPlaying(p0: Boolean) {
isPlaying = p0
if (p0) {
startProgressUpdate()
// 视频开始播放时,如果控制栏可见,启动自动隐藏计时器
if (isControlsVisible) {
resetHideTimer()
}
} else {
stopProgressUpdate()
// 视频暂停时,取消自动隐藏计时器,保持控制栏显示
hideHandler.removeCallbacks(hideRunnable)
showControls()
}
}
fun onRepeatModeChanged(p0: Int) {
@@ -121,25 +160,11 @@ class AlarmLogActivity : AppCompatActivity() {
exoPlayerManager.startPlayer()
setupControllerVisibilityListener()
updateProgress()
}
private fun setupControllerVisibilityListener() {
binding.exoPlayContextId.post {
val playerView = findPlayerView(binding.exoPlayContextId)
playerView?.setControllerVisibilityListener { visibility ->
if (visibility == View.VISIBLE) {
binding.ivBack.visibility = View.VISIBLE
binding.muteBar.visibility = View.VISIBLE
binding.ivDownload.visibility = View.VISIBLE
binding.ivDelete.visibility = View.VISIBLE
} else {
binding.ivBack.visibility = View.GONE
binding.muteBar.visibility = View.GONE
binding.ivDownload.visibility = View.GONE
binding.ivDelete.visibility = View.GONE
}
}
}
// 移除原来的控制器监听器,改为自定义控制逻辑
}
private fun findPlayerView(view: View): PlayerView? {
@@ -169,16 +194,19 @@ class AlarmLogActivity : AppCompatActivity() {
binding.alarmListRecyclerView.layoutManager = LinearLayoutManager(this)
binding.alarmListRecyclerView.adapter = adapter
}
private fun playVideo(videoUrl: String) {
exoPlayerManager.onDestroy()
exoPlayerManager = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_GESTURE, binding.exoPlayContextId)
.setPlayUri(videoUrl)
.setPosition(0)
.addVideoInfoListener(object : VideoInfoListener {
override fun onPlayStart(p0: Long) {
Log.d("AlarmLogActivity", "视频开始播放")
startProgressUpdate()
showControls()
resetHideTimer()
}
override fun onLoadingChanged() {
@@ -190,25 +218,51 @@ class AlarmLogActivity : AppCompatActivity() {
override fun onPlayEnd() {
Log.d("AlarmLogActivity", "播放结束")
stopProgressUpdate()
hideControls()
}
override fun isPlaying(p0: Boolean) {
isPlaying = p0
if (p0) {
startProgressUpdate()
// 视频开始播放时,如果控制栏可见,启动自动隐藏计时器
if (isControlsVisible) {
resetHideTimer()
}
} else {
stopProgressUpdate()
// 视频暂停时,取消自动隐藏计时器,保持控制栏显示
hideHandler.removeCallbacks(hideRunnable)
showControls()
}
}
fun onRepeatModeChanged(p0: Int) {
}
})
.create()
exoPlayerManager.startPlayer()
// 保持当前的静音状态
if (isMuted) {
try {
val method = exoPlayerManager.javaClass.getMethod("getPlayer")
val player = method.invoke(exoPlayerManager) as? SimpleExoPlayer
player?.volume = 0f
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun fetchAlarmList() {
val currentDeviceId = deviceId ?: return
Log.d("AlarmLogActivity", "=== 开始请求设备告警日志 ===")
Log.d("AlarmLogActivity", "请求参数 - deviceId: $currentDeviceId")
isLoading = true
lifecycleScope.launch {
try {
@@ -218,16 +272,16 @@ class AlarmLogActivity : AppCompatActivity() {
pageNum = "1",
pageSize = "10"
)
Log.d("AlarmLogActivity", "请求完成")
Log.d("AlarmLogActivity", "总条数: ${response.total}, 当前页数据条数: ${response.rows.size}")
val newData = response.rows
originalAlarmList.clear()
originalAlarmList.addAll(newData)
filterAlarmListByDate(calendar)
Log.d("AlarmLogActivity", "已获取 ${newData.size} 条数据")
} catch (e: Exception) {
Log.e("AlarmLogActivity", "请求异常: ${e.message}", e)
} finally {
@@ -236,11 +290,21 @@ class AlarmLogActivity : AppCompatActivity() {
}
}
}
private fun showEmptyView() {
binding.alarmListRecyclerView.visibility = View.GONE
binding.emptyLayout.visibility = View.VISIBLE
}
private fun hideEmptyView() {
binding.alarmListRecyclerView.visibility = View.VISIBLE
binding.emptyLayout.visibility = View.GONE
}
private fun filterAlarmListByDate(selectedDate: Calendar) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val selectedDateStr = dateFormat.format(selectedDate.time)
alarmList.clear()
originalAlarmList.forEach { item ->
val itemDateStr = item.createTime.substring(0, 10)
@@ -250,6 +314,12 @@ class AlarmLogActivity : AppCompatActivity() {
}
adapter?.notifyDataSetChanged()
Log.d("AlarmLogActivity", "日期筛选完成,筛选后条数: ${alarmList.size}")
if (alarmList.isEmpty()) {
showEmptyView()
} else {
hideEmptyView()
}
}
private fun updateDateDisplay() {
@@ -258,8 +328,51 @@ class AlarmLogActivity : AppCompatActivity() {
binding.tvDate.text = "${month}${day}"
}
private fun showControls() {
isControlsVisible = true
binding.functionBar.visibility = View.VISIBLE
binding.muteBar.visibility = View.VISIBLE
binding.playBar.visibility = View.VISIBLE
binding.progressBar.visibility = View.VISIBLE
// 只有在视频播放时才启动自动隐藏计时器
if (isPlaying) {
resetHideTimer()
}
}
private fun hideControls() {
isControlsVisible = false
binding.functionBar.visibility = View.GONE
binding.muteBar.visibility = View.GONE
binding.playBar.visibility = View.GONE
binding.progressBar.visibility = View.GONE
}
private fun toggleControls() {
if (isControlsVisible) {
hideControls()
} else {
showControls()
}
}
private fun resetHideTimer() {
hideHandler.removeCallbacks(hideRunnable)
// 只有在视频播放时才启动自动隐藏计时器
if (isPlaying) {
hideHandler.postDelayed(hideRunnable, hideDelay)
}
}
private fun initListener() {
// 点击视频区域切换控制栏显示/隐藏
binding.exoPlayContextId.setOnClickListener {
toggleControls()
}
// 控制栏上的按钮点击时重置隐藏计时器
binding.ivBack.setOnClickListener {
resetHideTimer()
finish()
}
@@ -268,26 +381,66 @@ class AlarmLogActivity : AppCompatActivity() {
}
binding.ivMute.setOnClickListener {
resetHideTimer()
toggleMute()
}
binding.ivDownload.setOnClickListener {
resetHideTimer()
downloadVideo()
}
binding.ivDelete.setOnClickListener {
resetHideTimer()
deleteVideo()
}
binding.ivFullScreen.setOnClickListener {
resetHideTimer()
toggleFullScreen()
}
binding.ivPlay.setOnClickListener {
resetHideTimer()
togglePlay()
}
binding.seekBar.setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
resetHideTimer()
exoPlayerManager.seekTo(progress.toLong() * 1000)
}
}
override fun onStartTrackingTouch(seekBar: android.widget.SeekBar?) {
resetHideTimer()
}
override fun onStopTrackingTouch(seekBar: android.widget.SeekBar?) {
}
})
}
private fun toggleMute() {
isMuted = !isMuted
if (isMuted) {
binding.ivMute.setImageResource(R.drawable.mute)
Toast.makeText(this, R.string.sound_off, Toast.LENGTH_SHORT).show()
} else {
binding.ivMute.setImageResource(R.drawable.voice)
Toast.makeText(this, R.string.sound_on, Toast.LENGTH_SHORT).show()
try {
// 通过反射获取底层的 ExoPlayer 实例
val method = exoPlayerManager.javaClass.getMethod("getPlayer")
val player = method.invoke(exoPlayerManager) as? SimpleExoPlayer
if (isMuted) {
binding.ivMute.setImageResource(R.drawable.mute)
player?.volume = 0f
Toast.makeText(this, R.string.sound_off, Toast.LENGTH_SHORT).show()
} else {
binding.ivMute.setImageResource(R.drawable.un_mute)
player?.volume = 1f
Toast.makeText(this, R.string.sound_on, Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, "音量控制失败", Toast.LENGTH_SHORT).show()
}
}
@@ -299,6 +452,63 @@ class AlarmLogActivity : AppCompatActivity() {
Toast.makeText(this, R.string.delete, Toast.LENGTH_SHORT).show()
}
private fun updateProgress() {
val currentPosition = exoPlayerManager.getCurrentPosition()
totalDuration = exoPlayerManager.getDuration()
binding.tvCurrentTime.text = formatTime(currentPosition)
binding.tvTotalTime.text = formatTime(totalDuration)
if (totalDuration > 0) {
val progress = (currentPosition * 100 / totalDuration).toInt()
binding.seekBar.progress = progress
}
}
private fun formatTime(milliseconds: Long): String {
val seconds = (milliseconds / 1000).toInt()
val hours = seconds / 3600
val minutes = (seconds % 3600) / 60
val secs = seconds % 60
return if (hours > 0) {
String.format("%02d:%02d:%02d", hours, minutes, secs)
} else {
String.format("%02d:%02d", minutes, secs)
}
}
private fun startProgressUpdate() {
stopProgressUpdate()
progressHandler.post(progressRunnable)
}
private fun stopProgressUpdate() {
progressHandler.removeCallbacks(progressRunnable)
}
private fun toggleFullScreen() {
isFullScreen = !isFullScreen
if (isFullScreen) {
binding.ivFullScreen.setImageResource(R.drawable.full_screen)
requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
binding.ivFullScreen.setImageResource(R.drawable.full_size)
requestedOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
private fun togglePlay() {
isPlaying = !isPlaying
if (isPlaying) {
binding.ivPlay.setImageResource(R.drawable.zt)
exoPlayerManager.startPlayer()
} else {
binding.ivPlay.setImageResource(R.drawable.start)
exoPlayerManager.onPause()
}
}
override fun onResume() {
super.onResume()
exoPlayerManager.onResume()

View File

@@ -83,14 +83,14 @@ class LiveVideoActivity : BaseActivity() {
initAlarmRecyclerView()
initListener()
fetchAlarmList()
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (exoPlayerManager.onBackPressed()) {
return
}
finish()
}
})
// onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
// override fun handleOnBackPressed() {
// if (exoPlayerManager.onBackPressed()) {
// return
// }
// finish()
// }
// })
}
private fun initVideoPlayer() {

View File

@@ -230,8 +230,6 @@ interface ApiService {
@Query("deviceId") deviceId: String,
@Query("pageNum") pageNum: String="1",
@Query("pageSize") pageSize: String="10",
@Query("startTime") startTime: String? = null,
@Query("endTime") endTime: String? = null
): DeviceAlarmResponse

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景 -->
<item android:id="@android:id/background"
android:height="4dp"
android:gravity="center_vertical">
<shape android:shape="rectangle">
<solid android:color="#44FFFFFF"/>
<corners android:radius="2dp"/>
</shape>
</item>
<!-- 进度 -->
<item android:id="@android:id/progress"
android:height="4dp"
android:gravity="center_vertical">
<clip>
<shape android:shape="rectangle">
<solid android:color="#09C2BD"/>
<corners android:radius="2dp"/>
</shape>
</clip>
</item>
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#09C2BD"/>
<size android:width="16dp" android:height="16dp"/>
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -2,9 +2,10 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_marginTop="30dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F9F9F9"
android:background="#000000"
android:orientation="vertical">
<!-- 视频播放区域和悬浮按钮 -->
@@ -18,11 +19,10 @@
android:layout_width="match_parent"
android:layout_height="280dp"
android:background="@android:color/transparent"
app:use_controller="true" />
app:use_controller="false"/>
<!-- 自定义顶部栏 - 返回、下载、删除在同一行 -->
<LinearLayout
android:layout_marginTop="20dp"
android:id="@+id/functionBar"
android:layout_width="match_parent"
android:layout_height="44dp"
@@ -60,22 +60,90 @@
</LinearLayout>
<!-- 右下角按钮区域 - 静音按钮 -->
<!-- 右下角按钮区域 - 静音、全屏按钮 -->
<LinearLayout
android:layout_marginBottom="40dp"
android:id="@+id/muteBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="6dp"
android:visibility="gone">
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/ivMute"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:src="@drawable/mute"/>
android:src="@drawable/un_mute"/>
<ImageView
android:id="@+id/ivFullScreen"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:layout_marginTop="8dp"
android:src="@drawable/full_size"/>
</LinearLayout>
<!-- 左下角播放按钮 -->
<LinearLayout
android:id="@+id/playBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start">
<ImageView
android:id="@+id/ivPlay"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
android:src="@drawable/zt"/>
</LinearLayout>
<!-- 底部进度条和时间区域 -->
<LinearLayout
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:paddingLeft="70dp"
android:paddingRight="70dp">
<!-- 当前时间 -->
<TextView
android:id="@+id/tvCurrentTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="#FFFFFF"
android:textSize="14sp"/>
<!-- 进度条 -->
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/custom_seekbar"
android:thumb="@drawable/custom_thumb"/>
<!-- 总时长 -->
<TextView
android:id="@+id/tvTotalTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="#FFFFFF"
android:textSize="14sp"/>
</LinearLayout>
@@ -121,5 +189,21 @@
android:layout_weight="1"
android:background="#FFFFFF" />
<LinearLayout
android:id="@+id/emptyLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical"
android:background="#FFFFFF"
android:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/no_message" />
</LinearLayout>
</LinearLayout>

View File

@@ -49,7 +49,7 @@
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="@android:color/transparent"
app:use_controller="true"/>
app:use_controller="false"/>
<!-- 功能按钮区域 -->
<LinearLayout