
Android VideoView 使用介绍(一)
一、前言
在 Android 开发中,简单视频预览、本地视频播放、相册视频播放场景非常普遍。对于轻量化、无需复杂定制、不想引入第三方播放器 的需求,Android 原生 VideoView 是最佳选择。
它系统原生、零依赖、API 简洁、适配广,非常适合新手学习和项目轻量场景使用。
全文 纯 Kotlin + AndroidX + ViewBinding,所有代码可直接复制运行、上线使用。
二、VideoView 适用与不适用场景
✅ 适用场景
-
App 内简单本地视频预览、教程视频展示
-
相册选择视频、本地视频播放
-
无需变速、滤镜、弹幕、精细缓冲控制
-
不想引入第三方播放器库,追求轻量稳定
❌ 不适用场景
-
短视频流、直播、HLS/DASH 流媒体
-
需要无缝续播、倍速、静音、自定义 UI 控制器
-
车载、大屏、高兼容、高稳定性播放器项目(推荐 ExoPlayer/Media3)
三、VideoView 底层核心原理
VideoView 是 Android 系统原生封装的视频播放控件,底层基于 MediaPlayer + SurfaceView 封装,屏蔽了底层复杂的解码、渲染、音视频同步逻辑。
工作流程
-
MediaPlayer:负责视频解码、音轨解析、播放控制、进度读取;
-
SurfaceView:独立窗口渲染视频画面,层级高于普通 View;
-
自动绑定解码数据与渲染画布,对外暴露极简 API;
-
内置播放、暂停、完成、错误、准备完成全套回调。
核心优缺点
优点:零依赖、开箱即用、兼容性好、API 简单、适合轻量业务。
缺点:定制性差、依赖机型硬解、生命周期管理不当极易出 Bug、不支持复杂流媒体。
四、VideoView 核心 API 详解
4.1 数据源设置(三种主流方式)
VideoView 支持三种视频来源,适配绝大多数本地视频场景:
-
res/raw 内置视频:无需权限,打包进 APK
-
手机本地存储视频:需要存储/媒体权限
-
系统相册视频:通过相册 Uri 播放(最常用业务场景)
4.2 核心播放方法
| 方法 | 作用 | 关键注意点 |
|---|---|---|
setVideoURI() |
设置视频资源 Uri | 仅加载,不会自动播放 |
start() |
开始/继续播放 | 必须在视频准备完成后调用 |
pause() |
暂停播放,保留进度 | 不释放播放器资源 |
stopPlayback() |
彻底释放播放器资源 | 页面销毁必须调用 |
seekTo(ms) |
跳转播放进度 | 单位毫秒 |
isPlaying() |
判断播放状态 | 用于播放/暂停切换 |
4.3 四大关键监听(解决 90% 问题)
-
OnPreparedListener :视频解析就绪,唯一能获取时长、宽高的时机
-
OnCompletionListener:视频播放完毕回调
-
OnErrorListener:拦截播放异常,避免系统弹窗
-
OnClickListener:实现点击播放/暂停交互
五、完整可运行工程配置
5.1 build.gradle.kts
kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.videoviewdemo"
compileSdk = 34
defaultConfig {
applicationId = "com.example.videoviewdemo"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
viewBinding {
enable = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}
5.2 AndroidManifest 权限适配
适配 Android13+ 媒体权限 + 低版本存储权限,用于读取相册视频:
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.videoviewdemo">
<!-- Android13+ 读取相册视频 -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android12及以下存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VideoViewDemo">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
5.3 布局文件(选择视频 + 播放展示)
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btnSelectVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打开相册选择视频"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="260dp"
android:layout_marginTop="20dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@id/btnSelectVideo" />
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="状态:未选择视频"
app:layout_constraintStart_toStartOf="@id/videoView"
app:layout_constraintTop_toBottomOf="@id/videoView" />
</androidx.constraintlayout.widget.ConstraintLayout>
六、核心实战:相册选视频 + VideoView 播放(完整可用)
kotlin
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.videoviewdemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 标记视频是否准备完成(解决黑屏核心变量)
private var isVideoPrepared = false
// 权限申请
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) openVideoGallery()
else Toast.makeText(this, "需要视频读取权限", Toast.LENGTH_SHORT).show()
}
// 相册选择视频回调
private val pickVideoLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let { playGalleryVideo(it) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initClickEvent()
initVideoView()
}
private fun initClickEvent() {
binding.btnSelectVideo.setOnClickListener { checkPermissionAndOpenGallery() }
}
/** 检查权限并打开相册 */
private fun checkPermissionAndOpenGallery() {
val permission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_VIDEO
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
openVideoGallery()
} else {
requestPermissionLauncher.launch(permission)
}
}
/** 打开系统相册,仅选择视频 */
private fun openVideoGallery() {
pickVideoLauncher.launch("video/*")
binding.tvStatus.text = "状态:请选择相册视频"
}
/** 播放相册选中的视频 */
private fun playGalleryVideo(videoUri: Uri) {
// 重置状态,防止时序错乱
isVideoPrepared = false
binding.videoView.setVideoURI(videoUri)
binding.tvStatus.text = "状态:视频加载中..."
}
/** 初始化视频播放器监听 */
private fun initVideoView() {
// 视频准备完成(解决黑屏核心回调)
binding.videoView.setOnPreparedListener {
isVideoPrepared = true
val totalSecond = it.duration / 1000
binding.tvStatus.text = "✅ 加载完成,视频时长:${totalSecond}s,点击播放"
}
// 点击播放/暂停
binding.videoView.setOnClickListener {
if (!isVideoPrepared) {
Toast.makeText(this, "视频尚未加载完成,请稍等", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (binding.videoView.isPlaying) {
binding.videoView.pause()
binding.tvStatus.text = "状态:已暂停"
} else {
binding.videoView.start()
binding.tvStatus.text = "状态:播放中"
}
}
// 播放完成
binding.videoView.setOnCompletionListener {
binding.videoView.seekTo(0)
binding.tvStatus.text = "状态:播放完毕"
}
// 播放异常拦截
binding.videoView.setOnErrorListener { _, _, _ ->
binding.tvStatus.text = "❌ 播放失败(视频编码不支持)"
Toast.makeText(this, "当前视频编码不兼容,请更换H264格式视频", Toast.LENGTH_SHORT).show()
true
}
}
// 页面切后台暂停
override fun onPause() {
super.onPause()
if (binding.videoView.isPlaying) {
binding.videoView.pause()
binding.tvStatus.text = "状态:后台暂停"
}
}
// 页面销毁释放资源(杜绝内存泄漏、后台发声)
override fun onDestroy() {
super.onDestroy()
binding.videoView.stopPlayback()
}
}
八、高频踩坑点汇总(生产避坑)
1. getDuration 获取时长为 0
原因:视频未解析完成。解决方案:仅在 OnPrepared 回调中获取时长、宽高。
2. 页面销毁后声音继续播放
原因:未释放播放器资源。解决方案:onDestroy 必须调用 stopPlayback()。
3. 屏幕旋转视频重启
原因:Activity 重建。解决方案:保存播放进度,重建后 seekTo 恢复。
4. 相册视频无法转 File 文件
Android 分区存储限制,相册返回为 content:// 类型 Uri,直接 setVideoURI 使用即可,无需转 File。
九、VideoView vs ExoPlayer(Media3) 选型对比
| 对比维度 | VideoView | ExoPlayer(Media3) |
|---|---|---|
| 依赖 | 原生零依赖 | 需要引入第三方库 |
| 编码兼容性 | 仅支持 H264,HEVC 黑屏 | 软硬解兜底,全格式兼容 |
| 定制能力 | 极弱 | 极强,支持变速、缓冲、滤镜 |
| 流媒体支持 | 不支持 HLS/DASH | 完美支持直播、点播 |
| 适用场景 | 简单本地/相册视频预览 | 正式播放器、短视频、直播项目 |
十、总结
-
VideoView 是轻量视频播放最优原生方案,零依赖、上手简单,满足日常简单预览需求;
-
开发核心规范:等待 Prepared 就绪再播放、固定控件宽高、页面销毁强制释放资源、适配分区存储 Uri;
-
黑屏 99% 是 时序错误 + 视频编码不兼容,本文代码已彻底修复;
-
简单相册视频播放选用本文原生方案,复杂播放器业务直接升级 Media3 ExoPlayer。
