Android音视频全面介绍与代码实践

Posted 徐福记456

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音视频全面介绍与代码实践相关的知识,希望对你有一定的参考价值。

android在应用层提供丰富的音视频多媒体接口,包括MediaPlayer、MediaCodec、AudioTrack、MediaMuxer、MediaExtractor、MediaRecorder、MediaMetadataRetriever、AudioRecord、AudioManager、Camera/Camera2/CameraX等。本文对咱们常用的多媒体API进行介绍,并且结合代码实例,希望能让大家对Android多媒体有深刻认识。

目录

一、MediaPlayer播放器

1、播放状态图

2、初始化播放器

3、监听播放状态

4、获取音视频信息

5、播放操作

二、MediaExtractor解封装器

三、MediaCodec硬编解码

1、MediaCodec状态图

2、创建MediaCodec

3、同步与异步编解码

4、参数配置

5、编解码队列

6、视频解码示例

四、MediaMuxer封装器

五、AudioTrack音频播放


一、MediaPlayer播放器

MediaPlayer是Android提供的多媒体播放器,支持播放音频和视频,可监听播放状态,可获取音视频信息,支持播放常规操作。详情请查看官方文档:MediaPlayer文档

1、播放状态图

MediaPlayer播放状态包括:Idle、Initialized、Preparing、Prepared、Started、Paused、Stopped、PlaybackCompleted、End、Error。有严格时序和状态转换,需要按照时序来调用播放接口,如下图所示:

2、初始化播放器

初始化播放器步骤:创建播放器、设置DataSource、设置显示Surface、设置播放状态监听、准备播放。代码如下:

    fun initPlayer(filePath: String, surface: Surface) 
        try 
            renderFirstFrame = false
            mediaPlayer = MediaPlayer()
            mediaPlayer!!.setDataSource(filePath)
            mediaPlayer!!.setSurface(surface)
            // 监听播放状态
            setListener()
            mediaPlayer!!.prepareAsync()
         catch (e: IOException) 
            e.printStackTrace()
        
    

3、监听播放状态

播放状态包括:准备完毕、播放信息、缓冲进度、字幕更新、播放出错、播放完成等。有同步初始化和异步初始化两种方式,一般采用异步初始化,在准备完毕onPrepared时开始播放;监听InfoListener为MEDIA_INFO_VIDEO_RENDERING_START时,说明视频渲染第一帧;监听到播放出错onError时,应该结束播放。相关的播放状态监听如下:

    private fun setListener() 
        mediaPlayer!!.setOnPreparedListener 
            mediaPlayer!!.start()
            playerCallback?.onPrepare()
        

        mediaPlayer!!.setOnInfoListener  mp: MediaPlayer?, what: Int, extra: Int ->
            (
                    if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) 
                        if (!renderFirstFrame) 
                            renderFirstFrame = true
                            playerCallback?.onRenderFirstFrame()
                        
                    )
            return@setOnInfoListener true
        

        mediaPlayer!!.setOnBufferingUpdateListener  mp, percent ->
            Log.i("MediaPlayer", "buffer percent=$percent")
        

        mediaPlayer!!.setOnTimedTextListener  mp: MediaPlayer?, text: TimedText? ->
            Log.i("MediaPlayer", "subtitle=" + text?.text)
        

        mediaPlayer!!.setOnErrorListener  mp: MediaPlayer?, what: Int, extra: Int ->
            return@setOnErrorListener playerCallback?.onError(what, extra)!!
        

        mediaPlayer!!.setOnCompletionListener 
            playerCallback?.onCompleteListener()
        
    

4、获取音视频信息

在onPrepared回调后,可以获取视频宽高、时长等信息。在播放过程中,可以获取当前播放位置。相关代码如下:

    // 当前播放位置
    fun currentPosition(): Int 
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.currentPosition
    
    // 播放时长
    fun duration(): Int 
        if (mediaPlayer == null)
            return 0
        return mediaPlayer!!.duration
    
    // 视频宽
    fun getVideoWidth(): Int 
        return mediaPlayer!!.videoWidth
    
    // 视频高
    fun getVideoHeight(): Int 
        return mediaPlayer!!.videoHeight
    

5、播放操作

播放操作包括:seek拖动、播放/暂停、静音播放、倍速播放、设置音频、切换音轨等。其中,切换音轨或字幕轨,前提是存在多音轨或多字幕轨。相关代码如下:

    // seek拖动
    fun seekTo(position: Int) 
        mediaPlayer?.seekTo(position)
    
    // 播放/暂停
    fun togglePlay() 
        if (mediaPlayer!!.isPlaying) 
            mediaPlayer!!.pause()
         else 
            mediaPlayer!!.start()
        
    
    // 静音播放
    fun mute() 
        mediaPlayer?.setVolume(0.0f, 0.0f)
    
    // 播放音量
    fun setVolume(volume: Float) 
        if (volume < 0 || volume > 1)
            return
        mediaPlayer?.setVolume(volume, volume)
    

    // 倍速播放
    fun setSpeed(speed: Float) 
        if (speed <= 0 || speed > 8)
            return
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) 
            val params = PlaybackParams()
            params.speed = speed
            mediaPlayer?.playbackParams = params
        
    
    // 切换音轨或字幕轨
    fun selectTrack(trackId: Int) 
        mediaPlayer?.selectTrack(trackId)
    

二、MediaExtractor解封装器

MediaExtractor用于媒体文件的解封装,解析媒体信息、获取音视频流。比如,播放视频流程:解封装——>解码——>渲染播放。示例代码如下:

  MediaExtractor extractor = new MediaExtractor();
  extractor.setDataSource(...);
  int numTracks = extractor.getTrackCount();
  for (int i = 0; i < numTracks; ++i) 
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (weAreInterestedInThisTrack) 
      extractor.selectTrack(i);
    
  
  ByteBuffer inputBuffer = ByteBuffer.allocate(...)
  while (extractor.readSampleData(inputBuffer, ...) >= 0) 
    int trackIndex = extractor.getSampleTrackIndex();
    long presentationTimeUs = extractor.getSampleTime();
    ...
    extractor.advance();
  
 
  extractor.release();
  extractor = null;

由此可见,MediaExtractor使用步骤如下:

  1. 创建MediaExtractor解封装器;
  2. 设置DataSource数据源;
  3. 遍历所有媒体轨道,根据MediaFormat的mimetype选择轨道;
  4. 调用readSampleData读取音视频的数据包,调用advance更新;
  5. 调用release释放资源;

三、MediaCodec硬编解码

Android提供MediaCodec进行硬编码、硬解码,包括硬件芯片厂商的硬编解码、系统内置的软编解码。MediaCodec的效率比FFmpeg软解效率高,速度快,占用CPU少。但是FFmpeg软解的兼容性好,支持更广泛的格式和参数。详情请看文档:MediaCodec

1、MediaCodec状态图

MediaCodec的先后顺序状态包括:Unitialized、Configured、Flushed、Running、End of Stream、(Error)、Released。其中,如果编解码过程中出错,会转移到Error状态。具体的状态迁移如下图所示:

2、创建MediaCodec

Android提供两种方式创建MediaCodec,根据name和type。示例代码如下:

    // 根据名字创建编解码
    MediaCodec.createByCodecName(name)
    // 根据mimetype创建编码器
    MediaCodec.createEncoderByType(mimeType)
    // 根据mimetype创建解码器
    MediaCodec.createDecoderByType(mimeType)

3、同步与异步编解码

MediaCodec以前使用同步方式进行编解码。虽然同步调用比较方便,但是会导致阻塞。示例代码如下:

int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) 
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  
  int outputBufferId = codec.dequeueOutputBuffer(...);
  if (outputBufferId >= 0) 
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  

在Android5.0后,提供异步方式进行编解码,通过设置回调监听实现。示例代码如下:

codec.setCallback(new MediaCodec.Callback() 
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) 
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // 填充待编码/解码数据
    ......
    codec.queueInputBuffer(inputBufferId, ...);
  
 
  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, ...) 
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    codec.releaseOutputBuffer(outputBufferId, ...);
  
 
  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) 

  
 
  @Override
  void onError(...) 

  
 );

4、参数配置

MediaCodec提供一系列参数配置,包括:码率、码率模式、GOP间隔等等。而MediaFormat的参数包括:width、height、frameRate、duration、bitrate、sampleRate、channelCount等。另外,在比较新的API版本有新增参数。

(1) HDR10_PLUS_INFO

在Android10(API 29),新增支持HDR10+,可以设置HDR10+的metadata到mediacodec的输入队列,适用于编解码场景。详情请看文档:PARAMETER_KEY_HDR10_PLUS_INFO

(2) LOW_LATENCY

在Android11(API 30),新增支持低延时解码,如果开启,那么解码器不会在内部缓存多余数据。

5、编解码队列

编解码队列包括:编解码前队列和编解码后队列,以生产者和消费者形式存在。队列是环形缓冲区,会循环使用。从输入端角度,客户端作为生产者,为codec提供编解码前的数据,codec作为消费者取出数据进行编解码;从输出端角度,codec作为生产者,为客户端提供编解码后的数据,客户端作为消费者取出数据去渲染。示意图如下:

6、视频解码示例

以视频解码渲染为例,使用MediaExtractor解封装、MediaCodec解码,并且关联到Surface进行渲染。示例代码如下:

    fun decodeVideo() 
        try 
            // 调用MediaExtractor解析得到MediaFormat
            mediaExtractor!!.setDataSource(mFilePath)
            for (i in 0 until mediaExtractor!!.trackCount) 
                mediaFormat = mediaExtractor!!.getTrackFormat(i)
                mimeType = mediaFormat!!.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video/")) 
                    mediaExtractor!!.selectTrack(i)
                    break
                
            
            // 创建MediaCodec,配置与启动
            mediaCodec = MediaCodec.createDecoderByType(mimeType)
            mediaCodec!!.configure(mediaFormat, mSurface, null, 0)
            mediaCodec!!.start()

            while (!isRunning) 
                val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
                if (inputIndex >= 0) 
                    val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                    val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                    // 待解码数据入队列
                    if (sampleSize < 0) 
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                     else 
                        mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize, mediaExtractor!!.sampleTime, 0)
                        mediaExtractor!!.advance()
                    
                
                // 解码后数据出队列
                val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
                if (outputIndex != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED 
                        && outputIndex != MediaCodec.INFO_TRY_AGAIN_LATER
                        && outputIndex != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) 
                        mediaCodec!!.releaseOutputBuffer(outputIndex, true)
                
            
         catch (e: Exception) 
            Log.e(TAG, "decode error=$e")
        
    

四、MediaMuxer封装器

MediaMuxer封装器用于封装音视频流,和MediaExtractor作用刚好相反。比如,录制视频流程:采集音视频流——>编码——>封装。接下来,以MediaExtractor解封装和MediaMuxer封装作为示例,参考代码如下:

   fun muxMediaFile(inputPath: String, outputPath: String): Boolean 
        if (inputPath.isEmpty() || outputPath.isEmpty()) 
            return false
        
        var happenError = false
        // 1、创建MediaMuxer
        val mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        val mediaExtractor = MediaExtractor()
        try 
            var videoIndex = 0
            var audioIndex = 0
            var audioFormat: MediaFormat? = null
            var videoFormat: MediaFormat? = null
            var finished = false
            val bufferInfo = MediaCodec.BufferInfo()
            val inputBuffer = ByteBuffer.allocate(2 * 1024 * 1024)
            mediaExtractor.setDataSource(inputPath)
            // 遍历所有轨道,根据mimetype选择轨道
            for (i in 0 until mediaExtractor.trackCount) 
                val mediaFormat = mediaExtractor.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("video")) 
                    videoIndex = i
                    videoFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                 else if (mimeType != null && mimeType.startsWith("audio") && audioFormat == null) 
                    audioIndex = i
                    audioFormat = mediaFormat
                    mediaExtractor.selectTrack(i)
                
            
            // 2、添加轨道,传入MediaFormat
            if (videoFormat != null) 
                mediaMuxer.addTrack(videoFormat)
            
            if (audioFormat != null) 
                mediaMuxer.addTrack(audioFormat)
            
            // 3、开启MediaMuxer
            mediaMuxer.start()

            while (!finished) 
                // 解封装获取音视频流数据
                val sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)
                if (sampleSize > 0) 
                    bufferInfo.size = sampleSize
                    bufferInfo.flags = mediaExtractor.sampleFlags
                    bufferInfo.presentationTimeUs = mediaExtractor.sampleTime
                    // 4、调用MediaMuxer把音视频流重新封装
                    if (mediaExtractor.sampleTrackIndex == videoIndex) 
                        mediaMuxer.writeSampleData(videoIndex, inputBuffer, bufferInfo)
                     else if (mediaExtractor.sampleTrackIndex == audioIndex) 
                        mediaMuxer.writeSampleData(audioIndex, inputBuffer, bufferInfo)
                    
                    inputBuffer.flip()
                    mediaExtractor.advance()
                 else if (sampleSize < 0) 
                    finished = true
                
            

         catch (e: Exception) 
            happenError = true
         finally 
            // 5、释放资源
            mediaMuxer.release()
            mediaExtractor.release()
            return !happenError
        
    

由此可见,MediaMuxer的使用步骤如下:

  1. 创建MediaMuxer;
  2. 添加MediaFormat到轨道;
  3. 开启MediaMuxer;
  4. 调用writeSampleData来封装音视频流;
  5. 释放资源;

五、AudioTrack音频播放

AudioTrack是Android在应用层提供的音频播放器。如果对延时有严格要求,可以使用底层提供的OpenSL ES,或者AAudio,而oboe库有对AAudio的封装。其中AAudio通过共享内存,降低延时,提高处理效率。

以MediaExtractor解封装、MediaCodec解码、AudioTrack播放三者结合,看看AudioTrack的代码示例。首先是初始化工作:

    // 初始化MediaExtractor
    private fun parseAudioFormat(path: String): MediaFormat? 
        mediaExtractor = MediaExtractor()
        try 
            mediaExtractor?.setDataSource(path)
            for (i in 0 until mediaExtractor!!.trackCount) 
                val mediaFormat = mediaExtractor!!.getTrackFormat(i)
                val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
                if (mimeType != null && mimeType.startsWith("audio")) 
                    mediaExtractor!!.selectTrack(i)
                    return mediaFormat
                
            
         catch (e: Exception) 
            Log.e(TAG, "parseAudioFormat err=$e")
        
        return null
    
    // 初始化MediaCodec
    private fun initMediaCodec(mediaFormat: MediaFormat): Boolean 
        val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
        mediaCodec = MediaCodec.createDecoderByType(mimeType)
        return try 
            mediaCodec!!.configure(mediaFormat, null, null, 0)
            mediaCodec!!.start()
            true
         catch (e: Exception) 
            Log.e(TAG, "initMediaCodec err=$e")
            false
        
    
    // 初始化AudioTrack
    private fun initAudioTrack(mediaFormat: MediaFormat): Boolean 
        val sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        val channelConfig = if (channelCount == 1) 
            AudioFormat.CHANNEL_OUT_MONO
         else  
            AudioFormat.CHANNEL_OUT_STEREO
        
        val encoding = AudioFormat.ENCODING_PCM_16BIT
        val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
        Log.e(TAG, "sampleRate=$sampleRate, channelCount=$channelCount, bufferSize=$bufferSize")

        try 
            val audioFormat = AudioFormat.Builder()
                    .setEncoding(encoding)
                    .setSampleRate(sampleRate)
                    .setChannelMask(channelConfig)
                    .build()
            val audioAttributes = AudioAttributes.Builder()
                    .setLegacyStreamType(AudioManager.STREAM_MUSIC)
                    .build()
            audioTrack = AudioTrack(audioAttributes, audioFormat,
                    bufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE)
            audioTrack!!.play()
         catch (e: Exception) 
            Log.e(TAG, "initAudioTrack err=$e")
            return false
        
        return true
    

接下来,调用MediaExtractor的readSampleData方法解析音视频流,调用MediaCodec的queueInputBuffer和dequeueOutputBuffer方法进行解码,最后调用AudioTrack的write方法进行播放。需要注意的是,解封装、解码、播放应该分为三个线程,这里只是简单演示使用方法。相关代码如下:

    fun playAudio(path: String) 
        var finished = false
        val data = ByteArray(10 * 1024)
        running = AtomicBoolean(true)
        val bufferInfo = MediaCodec.BufferInfo()
        val mediaFormat = parseAudioFormat(path) ?: return release()
        var result = initMediaCodec(mediaFormat)
        if (!result) 
            return release()
        
        result = initAudioTrack(mediaFormat)
        if (!result) 
            return release()
        

        while (!finished) 
            if (!running!!.get()) 
                break
            
            val inputIndex = mediaCodec!!.dequeueInputBuffer(DEQUEUE_TIME)
            if (inputIndex >= 0) 
                val inputBuffer = mediaCodec!!.getInputBuffer(inputIndex)
                // demux
                val sampleSize = mediaExtractor!!.readSampleData(inputBuffer!!, 0)
                // decode
                if (sampleSize < 0) 
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, 0,
                            0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                    finished = true
                 else 
                    mediaCodec!!.queueInputBuffer(inputIndex, 0, sampleSize,
                            mediaExtractor!!.sampleTime, mediaExtractor!!.sampleFlags)
                    mediaExtractor!!.advance()
                
            

            val outputIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIME)
            // play
            if (outputIndex >= 0) 
                val outputBuffer = mediaCodec!!.getOutputBuffer(outputIndex)
                val size = outputBuffer!!.limit()
                outputBuffer.get(data, outputBuffer.position(), size - outputBuffer.position())
                audioTrack!!.write(data, 0, size)
                mediaCodec!!.releaseOutputBuffer(outputIndex, false)
                SystemClock.sleep(SLEEP_TIME)
            
        

        release()
    

至此,关于Android多媒体的视频播放器、音频播放器、MediaCodec编解码、多媒体封装与解封装器介绍完毕。完整代码与学习音视频,可查看GitHub:FFmpegAndroid

全面介绍Android的MVVM框架 - 数据绑定

原文地址 MasteringAndroidDataBinding


本教程是跟着 Data Binding Guide 学习过程中得出的一些实践经验,同时修改了官方教程的一些错误,每一个知识点都有对应的源码,争取做到实践与理论相结合。

Data Binding 解决了 Android UI 编程中的一个痛点,官方原生支持 MVVM 模型可以让我们在不改变既有代码框架的前提下,非常容易地使用这些新特性。其实在此之前,已经有些第三方的框架(RoboAndroid) 可以支持 MVVM 模型,无耐由于框架的侵入性太强,导致一直没有流行起来。

准备

Android Studio 更新到 1.3 版本

打开 Preferences,找到 Appearances & Behavior 下的 Updates 选项,把 Automatically Check updates for 修改成 Canary Channel

注意

Data Binding 是一个 support 包,因此与 Android M 没什么关系,可以不用下载 Android MNC Preview 的 SDK。

新建一个 Project

修改 Project 的 build.gradle,为 build script 添加一条依赖,Gradle 版本为 1.2.3。

classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.databinding:dataBinder:1.0-rc0'

为用到 Data Binding 的模块添加插件,修改对应的 build.gradle

apply plugin: 'com.android.databinding'

注意

如果 Module 用到的 buildToolsVersion 高于 22.0.1,比如 23 rc1,那 com.android.databinding:dataBinder 的版本要改为 1.3.0-beta1,否则会出现如下错误:

基础

工程创建完成后,我们通过一个最简单的例子来说明 Data Binding 的基本用法。

布局文件

使用 Data Binding 之后,xml的布局文件就不再单纯地展示 UI 元素,还需要定义 UI 元素用到的变量。所以,它的根节点不再是一个 ViewGroup,而是变成了 layout,并且新增了一个节点 data

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
    </data>
    <!--原先的根节点(Root Element)-->
    <LinearLayout>
    ....
    </LinearLayout>
</layout>

要实现 MVVM 的 ViewModel 就需要把数据与UI进行绑定,data 节点就为此提供了一个桥梁,我们先在 data 中声明一个 variable,这个变量会为 UI 元素提供数据(例如 TextView 的 android:text),然后在 Java 代码中把"后台"数据与这个 variable 进行绑定。

如果要用一个表格来展示用户的基本信息,用 Data Binding 应该怎么实现呢?

数据对象

添加一个 POJO 类 - User,非常简单,四个属性以及他们的 getter 和 setter。

public class User 
    private final String firstName;
    private final String lastName;
    private String displayName;
    private int age;

    public User(String firstName, String lastName) 
        this.firstName = firstName;
        this.lastName = lastName;
    

    public User(String firstName, String lastName, int age) 
        this(firstName, lastName);
        this.age = age;
    

    public int getAge() 
        return age;
    

    public String getFirstName() 
        return firstName;
    

    public String getLastName() 
        return lastName;
    

    public String getDisplayName() 
        return firstName + " " + lastName;
    

    public boolean isAdult() 
        return age >= 18;
    

稍后,我们会新建一个 User 类型的变量,然后把它跟布局文件中声明的变量进行绑定。

定义 Variable

再回到布局文件,在 data 节点中声明一个变量 user

<data>
	<variable name="user" type="com.liangfeizc.databindingsamples.basic.User" />
</data>

其中 type 属性就是我们在 Java 文件中定义的 User 类。

当然,data 节点也支持 import,所以上面的代码可以换一种形式来写。

<data>
    <import type="com.liangfeizc.databindingsamples.basic.User" />
    <variable name="user" type="User" />
</data>

然后我们刚才在 build.gradle 中添加的那个插件 - com.android.databinding会根据xml文件的名称 Generate 一个继承自 ViewDataBinding 的类。

例如,这里 xml 的文件名叫 activity_basic.xml,那么生成的类就是 ActivityBasicBinding

注意

java.lang.* 包中的类会被自动导入,可以直接使用,例如要定义一个 String 类型的变量:

<variable name="firstName" type="String" />

绑定 Variable

修改 BasicActivityonCreate 方法,用 DatabindingUtil.setContentView() 来替换掉 setContentView(),然后创建一个 user 对象,通过 binding.setUser(user)variable 进行绑定。

@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    ActivityBasicBinding binding = DataBindingUtil.setContentView(
            this, R.layout.activity_basic);
    User user = new User("fei", "Liang");
    binding.setUser(user);

注意

ActivityBasicBinding 类是自动生成的,所有的 set 方法也是根据 variable 名称生成的。例如,我们定义了两个变量。

<data>
    <variable name="firstName" type="String" />
    <variable name="firstName" type=""
</data>

那么就会生成对应的两个 set 方法。

setFirstName(String firstName);
setLastName(String lastName);

使用 Variable

数据与 Variable 绑定之后,xml 的 UI 元素就可以直接使用了。

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@user.lastName" />

至此,一个简单的数据绑定就完成了,可参考完整代码

高级用法

使用类方法

首先为类添加一个静态方法

public class MyStringUtils 
    public static String capitalize(final String word) 
        if (word.length() > 1) 
            return String.valueOf(word.charAt(0)).toUpperCase() + word.substring(1);
        
        return word;
    

然后在 xml 的 data 节点中导入:

<import type="com.liangfeizc.databindingsamples.utils.MyStringUtils" />

使用方法与 Java 语法一样:

<TextView
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:text="@StringUtils.capitalize(user.firstName)" />

类型别名

如果我们在 data 节点了导入了两个同名的类怎么办?

<import type="com.example.home.data.User" />
<import type="com.examle.detail.data.User" />
<variable name="user" type="User" />

这样一来出现了两个 User 类,那 user 变量要用哪一个呢?不用担心,import 还有一个 alias 属性。

<import type="com.example.home.data.User" />
<import type="com.examle.detail.data.User" alias="DetailUser" />
<variable name="user" type="DetailUser" />

Null Coalescing 运算符

android:text="@user.displayName ?? user.lastName"

就等价于

android:text="@user.displayName != null ? user.displayName : user.lastName"

属性值

通过 $ 可以直接把 Java 中定义的属性值赋值给 xml 属性。

<TextView
   android:text="@user.lastName"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@user.isAdult ? View.VISIBLE : View.GONE"/>

使用资源数据

这个例子,官方教程有错误,可以参考Android Data Binder 的一个bug,完整代码在此

<TextView
    android:padding="@large? (int)@dimen/largePadding : (int)@dimen/smallPadding"
    android:background="@android:color/black"
    android:textColor="@android:color/white"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />

以上是关于Android音视频全面介绍与代码实践的主要内容,如果未能解决你的问题,请参考以下文章

音视频开发系列——全面了解Android MediaMetadataRetriever

iOS 高刷屏监控 + 优化:从理论到实践全面解析

Vlc for Android 全面阐述

Android20.0 第20章 音频视频拍照截图

Android MediaCodec+OpenGL视频编解码实践笔记

Android MediaCodec+OpenGL视频编解码实践笔记