【Android UI开发】Android VideoView 使用介绍(一)

Android VideoView 使用介绍(一)

一、前言

在 Android 开发中,简单视频预览、本地视频播放、相册视频播放场景非常普遍。对于轻量化、无需复杂定制、不想引入第三方播放器 的需求,Android 原生 VideoView 是最佳选择。

它系统原生、零依赖、API 简洁、适配广,非常适合新手学习和项目轻量场景使用。

全文 纯 Kotlin + AndroidX + ViewBinding,所有代码可直接复制运行、上线使用。

二、VideoView 适用与不适用场景

✅ 适用场景

  • App 内简单本地视频预览、教程视频展示

  • 相册选择视频、本地视频播放

  • 无需变速、滤镜、弹幕、精细缓冲控制

  • 不想引入第三方播放器库,追求轻量稳定

❌ 不适用场景

  • 短视频流、直播、HLS/DASH 流媒体

  • 需要无缝续播、倍速、静音、自定义 UI 控制器

  • 车载、大屏、高兼容、高稳定性播放器项目(推荐 ExoPlayer/Media3)

三、VideoView 底层核心原理

VideoView 是 Android 系统原生封装的视频播放控件,底层基于 MediaPlayer + SurfaceView 封装,屏蔽了底层复杂的解码、渲染、音视频同步逻辑。

工作流程

  1. MediaPlayer:负责视频解码、音轨解析、播放控制、进度读取;

  2. SurfaceView:独立窗口渲染视频画面,层级高于普通 View;

  3. 自动绑定解码数据与渲染画布,对外暴露极简 API;

  4. 内置播放、暂停、完成、错误、准备完成全套回调。

核心优缺点

优点:零依赖、开箱即用、兼容性好、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 完美支持直播、点播
适用场景 简单本地/相册视频预览 正式播放器、短视频、直播项目

十、总结

  1. VideoView 是轻量视频播放最优原生方案,零依赖、上手简单,满足日常简单预览需求;

  2. 开发核心规范:等待 Prepared 就绪再播放、固定控件宽高、页面销毁强制释放资源、适配分区存储 Uri

  3. 黑屏 99% 是 时序错误 + 视频编码不兼容,本文代码已彻底修复;

  4. 简单相册视频播放选用本文原生方案,复杂播放器业务直接升级 Media3 ExoPlayer。

相关推荐
Mr数据杨1 小时前
【Codex】用学生支持周报模块跟踪个性化支持进展
android·java·javascript·django·codex·项目开发
名剑走天下1 小时前
Android 面试题大全
android·kotlin
mmsx1 小时前
osmdroid 自定义地图视图:继承 MapView 的正确姿势与生命周期管理
android·osmdroid
ii_best3 小时前
移动开发工具按键精灵安卓版 OcrEx 识别总是漏字错字?改这一个参数,精度直接翻倍
android·ai编程·按键精灵
chjif3 小时前
Android 设备管控开发实战:自定义 Launcher 如何只显示指定 App?
android·kotlin
Wang's Blog3 小时前
Vibe Coding一人即团队系列23: 基于Stitch/Sketch的UI原型1:1复刻工作流
人工智能·macos·ui·sketch
Dovis(誓平步青云)4 小时前
模拟器横评:电脑上看小说、追短剧用什么模拟器?MuMu、雷电、腾讯手游助手实测
android·java·服务器·前端·javascript·电脑
非凡ghost4 小时前
用 Kotlin 和 Jetpack Compose 重写的安卓视频播放器,原生体验有多流畅?
android·java·kotlin·音视频·软件需求