Android音视频全面介绍与代码实践
Posted 徐福记456
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音视频全面介绍与代码实践相关的知识,希望对你有一定的参考价值。
android在应用层提供丰富的音视频多媒体接口,包括MediaPlayer、MediaCodec、AudioTrack、MediaMuxer、MediaExtractor、MediaRecorder、MediaMetadataRetriever、AudioRecord、AudioManager、Camera/Camera2/CameraX等。本文对咱们常用的多媒体API进行介绍,并且结合代码实例,希望能让大家对Android多媒体有深刻认识。
目录
一、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使用步骤如下:
- 创建MediaExtractor解封装器;
- 设置DataSource数据源;
- 遍历所有媒体轨道,根据MediaFormat的mimetype选择轨道;
- 调用readSampleData读取音视频的数据包,调用advance更新;
- 调用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的使用步骤如下:
- 创建MediaMuxer;
- 添加MediaFormat到轨道;
- 开启MediaMuxer;
- 调用writeSampleData来封装音视频流;
- 释放资源;
五、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框架 - 数据绑定
本教程是跟着 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
修改 BasicActivity
的 onCreate
方法,用 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