1. Binding Adapter 的基本概念和作用
Binding Adapter 是一个桥梁,它允许你在 XML 布局文件中,将自定义的属性(例如 app:image_corners_uri
)与 Kotlin/Java 代码中的方法绑定起来。它解决了 Android 默认的 Data Binding 无法处理复杂逻辑或自定义 View 属性的问题。
Kotlin
class DataBindingAdapter {
companion object {
@BindingAdapter("media_source_icon", "media_source_name")
@JvmStatic
fun setMediaSourceInfo(view: MediaSourceBar, drawable: Drawable?, name: String?) {
name?.let { view.setMediaSourceName(it) }
drawable?.let { view.setMediaSourceIcon(it) }
}
@BindingAdapter("media_source_right_button")
@JvmStatic
fun setMediaSourceRightButton(view: MediaSourceBar, rotate: Boolean?) {
rotate?.let { view.setMediaSourceRightButton(it) }
}
@BindingAdapter("data")
@JvmStatic
fun bindRecyclerView(
recyclerView: Recyclerview,
list: MutableList<ListItem>?,
) {
val adapter = recyclerView.adapter
if (adapter is RecyclerViewListAdapter) {
list?.let { adapter.submitList(it) }
}
}
@BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)
@JvmStatic
fun bindMediaMetadataInfo(
simpleMediaView: PlayInfoView,
mediaMetadata: MediaMetadata?,
mediaSource: ServiceBean?
) {
simpleMediaView.setMediaMetadata(mediaMetadata)
if (mediaSource?.packageName in MediaBrowserManager.musicMediaSourceServices) {
simpleMediaView.setMediaServiceBean(mediaSource)
} else {
simpleMediaView.setMediaServiceBean(null)
}
}
@BindingAdapter("duration", "progress", requireAll = false)
@JvmStatic
fun bindMediaMetadataProgress(
simpleMediaView: PlayInfoView,
duration: Long?,
progress: Long?
) {
simpleMediaView.setProgress(duration, progress)
}
@BindingAdapter("player_commands", requireAll = false)
@JvmStatic
fun bindMediaMetadataCommands(
simpleMediaView: PlayInfoView,
commands: Player.Commands?,
) {
simpleMediaView.setPlayerCommands(commands)
}
@BindingAdapter("isPlaying", requireAll = false)
@JvmStatic
fun bindMediaMetadataPlayback(
view: View,
isPlaying: Boolean?,
) {
if (view is PlayInfoView) {
view.setPlayback(isPlaying)
}
if (view is tech.jidouauto.component.widgets.basic.ImageView) {
if (isPlaying == true) {
view.setImageResource(com.jidouauto.mediacenter.R.drawable.item_icon_pause)
} else {
view.setImageResource(com.jidouauto.mediacenter.R.drawable.item_icon_play)
}
}
}
@BindingAdapter("playMode", requireAll = false)
@JvmStatic
fun bindPlayMode(
simpleMediaView: PlayInfoView,
mode: PlayMode?
) {
if (mode != null) {
simpleMediaView.setPlayMode(mode)
}
}
@BindingAdapter("type")
@JvmStatic
fun setType(view: PlayInfoView, type: Int) {
view.setType(type)
}
@BindingAdapter("discType")
@JvmStatic
fun setDiscType(view: PlayInfoView, type: Int) {
view.setDiscType(type)
}
@BindingAdapter("image_corners_uri", "image_radius", requireAll = false)
@JvmStatic
fun imageUri(view: tech.jidouauto.component.widgets.basic.ImageView, uri: Any?, radius: Int?) {
uri?.let {
val imageResource = ImageResource.Remote(
uri,
transformationType = IImageLoader.ImageTransformationType.RoundCorners(radius?.dp ?: 24.dp),
placeholder = com.jidouauto.mediacenter.R.drawable.icon_placeholder,
error = com.jidouauto.mediacenter.R.drawable.icon_placeholder
)
view.imageSource = imageResource
}
}
@BindingAdapter("image_cycle_uri", requireAll = false)
@JvmStatic
fun cycleImageUri(view: tech.jidouauto.component.widgets.basic.ImageView, uri: Any?) {
uri?.let {
val imageResource = ImageResource.Remote(
uri,
transformationType = IImageLoader.ImageTransformationType.CircleCrop,
placeholder = com.jidouauto.mediacenter.R.drawable.icon_placeholder_circle,
error = com.jidouauto.mediacenter.R.drawable.icon_placeholder_circle
)
view.imageSource = imageResource
}
}
@BindingAdapter("player_album_image_cover", requireAll = false)
@JvmStatic
fun playerAlbumImageCover(
view: tech.jidouauto.component.widgets.basic.ImageView,
uri: Any?
) {
}
@BindingAdapter("banner_data", requireAll = false)
@JvmStatic
fun bindBannerViewData(
view: TopBannerCarouselPager,
data: MainBannerItem?,
) {
Logger.info("bind main banner data=${data?.items}")
if (data == null) return
view.setAdapter(TopBannerCarouselPager.Adapter(data.items, data.itemOnClick))
}
@BindingAdapter("show_background", requireAll = false)
@JvmStatic
fun showBackGround(view: View, isShow: Boolean) {
}
/**
* 控制Lottie动画的播放状态
* @param view LottieAnimationView实例
* @param isPlaying 是否正在播放(true:播放,false:暂停)
*/
@BindingAdapter("lottie_playing_state")
@JvmStatic
fun controlLottieAnimation(view: LottieAnimationView, isPlaying: Boolean?) {
isPlaying?.let {
if (it) {
// 如果需要播放且当前未播放,则开始/恢复动画
if (!view.isAnimating) {
view.playAnimation()
}
} else {
// 如果需要暂停且当前正在播放,则暂停动画
if (view.isAnimating) {
view.pauseAnimation()
}
}
}
}
/**
* 可选:控制Lottie动画的可见性(根据播放状态)
* 当播放时显示动画,暂停时隐藏
*/
@BindingAdapter("lottie_visible_with_playing")
@JvmStatic
fun setLottieVisibilityWithPlaying(view: LottieAnimationView, isPlaying: Boolean?) {
view.visibility = if (isPlaying == true) View.VISIBLE else View.GONE
}
}
}
在这份代码中,@BindingAdapter
注解就是关键。它告诉 Data Binding 编译器,当 XML 中使用了该注解中定义的属性时,应该调用被注解的方法。
例如:@BindingAdapter("media_source_icon", "media_source_name")
:定义了两个属性,media_source_icon
和 media_source_name
。当 XML 中同时使用这两个属性时,会调用 setMediaSourceInfo
方法。
2. 常见应用场景与代码分析
这份代码覆盖了多种常见的 Binding Adapter 应用场景,我们可以逐一分析:
2.1. 绑定简单属性和多个属性
setMediaSourceInfo
方法:
注解: @BindingAdapter("media_source_icon", "media_source_name")
应用: 将 Drawable
和 String
类型的数据绑定到 MediaSourceBar
这个自定义 View 上,分别设置图标和名称。
知识点: @BindingAdapter
注解可以接收多个属性名,当这些属性都在 XML 中出现时,被注解的方法会被调用。方法参数的顺序和类型必须与属性值匹配。
setMediaSourceRightButton
方法:
注解: @BindingAdapter("media_source_right_button")
应用: 将一个 Boolean
值绑定到 MediaSourceBar
的一个按钮上,控制其旋转状态。
知识点: 展示了如何绑定单个自定义属性,将一个布尔值直接传递给 View 的方法。
2.2. 处理列表数据绑定到 RecyclerView
bindRecyclerView
方法:
注解: @BindingAdapter("data")
应用: 将一个 MutableList<ListItem>
数据列表直接绑定到 RecyclerView
。
知识点: 这个方法非常实用,它通过检查 RecyclerView
的 adapter
类型(确保是 RecyclerViewListAdapter
),然后调用 adapter.submitList()
来更新数据。这在 ViewModel 中处理列表数据时非常方便,只需在 XML 中设置 app:data="@{viewModel.myList}"
,就能实现列表的自动更新。
2.3. 绑定复杂对象和多参数
bindMediaMetadataInfo
方法:
注解: @BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)
应用: 将 MediaMetadata
和 ServiceBean
两个复杂的对象绑定到 PlayInfoView
上。
知识点:
value = [...]
:用于定义多个属性。
requireAll = false
:这是一个重要的参数。它表示这些属性不必同时出现在 XML 中。如果设置为 true
(默认),只有当 XML 中同时包含了所有属性时,该方法才会被调用。设置为 false
意味着只要其中一个属性存在,方法就会被调用。
方法中的逻辑展示了如何根据 mediaSource.packageName
进行条件判断,然后设置不同的 View 状态,这是 Binding Adapter 中处理复杂逻辑的常见方式。
2.4. 绑定图片加载库
imageUri
和 cycleImageUri
方法:
注解: @BindingAdapter("image_corners_uri", "image_radius", requireAll = false)
和 @BindingAdapter("image_cycle_uri", requireAll = false)
应用: 加载远程图片 URL(URI),并根据不同的属性进行不同的处理。image_corners_uri
用于加载圆角图片,而 image_cycle_uri
用于加载圆形图片。
知识点: 这两个方法是 Binding Adapter 最常见的用途之一。它们将图片加载库(如 Glide、Picasso 或你代码中的 ImageResource.Remote
)的复杂调用逻辑封装起来,让开发者在 XML 中只需简单地提供一个 URL 即可,大大简化了 View 的使用。同时,它还演示了如何处理占位符和错误图片。
2.5. 条件判断和多类型 View 处理
bindMediaMetadataPlayback
方法:
注解: @BindingAdapter("isPlaying", requireAll = false)
应用: 根据 isPlaying
的布尔值,改变不同 View 的状态。
知识点: 方法参数中的 View
类型非常通用。
方法内部使用 if (view is PlayInfoView)
和 if (view is tech.jidouauto.component.widgets.basic.ImageView)
进行 类型判断 。这允许同一个 Binding Adapter 处理不同类型的 View,根据 View 的具体类型执行不同的逻辑。例如,如果是 PlayInfoView
就设置播放状态,如果是 ImageView
就改变图标。
3. @JvmStatic
和 companion object
@JvmStatic
: 这是一个 Kotlin 注解,用于标记一个方法为静态方法。在 companion object
中使用此注解后,该方法可以像 Java 的静态方法一样,通过类名直接调用,这正是 Data Binding 编译器所要求的。
companion object
: Kotlin 中的伴生对象,用于存放与类相关的静态成员。将所有 Binding Adapter 方法放在 companion object
中是 Kotlin 的最佳实践。
OutlineProvider.kt
OutlineProvider
类是一个自定义的 ViewOutlineProvider
,它的主要作用是为 View
设置圆角裁剪(clipping)效果。它接收 CornerType
和 radius
作为参数,然后根据这些参数生成一个 Path
,并用它来定义 View 的轮廓(outline)。这使得开发者可以在不修改 View 背景或使用其他复杂方法的情况下,轻松地为 View 的特定角或所有角设置圆角效果。
-
CornerType
枚举定义了九种不同的圆角类型,包括:-
ALL
: 所有四个角都设置圆角。 -
TOP_LEFT
,TOP_RIGHT
,BOTTOM_LEFT
,BOTTOM_RIGHT
: 仅设置单个角。 -
TOP
,BOTTOM
,LEFT
,RIGHT
: 设置特定边上的两个角。
-
-
getOutline
方法是核心逻辑所在,它根据cornerType
和radius
的值创建一个floatArray
,该数组用于Path
的addRoundRect
方法来创建带有指定圆角的矩形路径。最后,它使用outline.setPath(path)
将这个路径应用到 View 的轮廓上。
DatabindingAdapter.kt
DatabindingAdapter
类是一个包含大量静态 BindingAdapter
方法的集合,这些方法用于在 XML 布局文件中实现自定义属性和数据绑定。它的核心作用是:
-
图片加载 : 提供了多种方法(如
setImageUrl
、setCircleImageUrl
、setIqiyiSpecifiedSizeImageUrl
)来使用 Glide 库加载图片。这些方法支持处理不同的图片资源类型(ID、Drawable、URI),设置占位图、圆角半径、高斯模糊等效果。 -
UI 控件属性设置: 提供了许多用于设置各种 UI 控件属性的方法,例如:
-
bindTabLayoutTabs
: 动态绑定和设置TabLayout
的 Tab。 -
setLayoutHeight
,layoutMarginTop
等: 设置 View 的布局参数(如高度、边距)。 -
setQrcodeStatus
: 设置QrcodeScreenView
的状态和内容。 -
setXimalayaAlbumTags
: 根据媒体项的数据,动态显示喜马拉雅的VIP、付费、精品等标签图标。 -
setIqyNormalModelTag
: 根据媒体项数据,动态显示爱奇艺的VIP、独家等标签图标。 -
setOperationPanelItemActivedDisabled
: 管理操作面板项的激活和禁用状态。 -
strikeThrough
: 为TextView
的文本添加删除线效果。
-
-
动画控制 : 提供了用于控制 View 显示和隐藏动画的方法,例如
setPopupOpenAnimation
、setCancelAnimation
等。 -
逻辑封装: 将复杂的逻辑(如根据媒体数据判断显示哪个标签图标)封装在 BindingAdapter 方法中,使得 XML 布局文件更加简洁和易读。
BindingResourceUtil.kt
BindingResourceUtil
是一个工具类,包含了一系列静态方法,用于将数据模型中的原始值(如浮点数、字符串、枚举)转换为可用于数据绑定的资源对象(如 ImageResource
、TextResource
)。它的核心作用是:
-
数据到资源的转换: 提供了将逻辑值转换为 UI 资源的方法,例如:
-
playbackSpeedIcon
: 根据播放速度的浮点值返回相应的图标资源 ID。 -
definitionText
: 根据视频清晰度的字符串返回相应的文本资源 ID。 -
ximalayaDownloadIcon
: 根据喜马拉雅媒体项的下载类型返回相应的下载图标资源 ID。
-
-
多源媒体适配 : 提供了针对不同媒体源(酷我、喜马拉雅、乐听)的资源转换方法。例如,
playingCoverPlaceHolder
会根据mediaId
判断媒体源,并返回相应的占位图。 -
文案和状态转换 : 将逻辑状态(如下载状态、二维码状态)转换为用户可读的文本资源 ID。例如,
downloadDisabledReason
根据下载状态返回相应的禁用原因文本。
DataBindingConvertor.java
DataBindingConvertor
是一个简单的 Java 类,它使用 @BindingConversion
注解定义了一些类型转换方法。它的核心作用是:
-
自动类型转换 : 实现了 Android Data Binding 框架中的自动类型转换功能。这使得开发者可以在 XML 布局文件中直接使用原始类型(如
int
、CharSequence
、Drawable
、Uri
),而无需手动将其包装成TextResource
或ImageResource
。 -
简化 XML 绑定 : 例如,当你在 XML 中将一个
int
类型的资源 ID 绑定给一个需要TextResource
的属性时,convertTextResource(int id)
方法会自动被调用,完成类型转换。 -
处理特定数据模型 : 提供了针对
QrCodeTokenInfo
、OrderBean
和IqiyiOrderBean
的转换方法,用于从这些数据模型中提取二维码 URL。
DataBindingNavigationUtil.kt
DataBindingNavigationUtil
是一个工具类,专门用于处理与导航相关的逻辑。它的核心作用是:
-
Fragment 和 Activity 导航 : 提供了
openFragment
等方法,用于在不同的上下文(Activity
或Fragment
)中打开新的 Fragment。 -
业务逻辑封装: 封装了与具体业务相关的导航逻辑,例如:
-
kuwoVipPayFragment
,ximalayaLoginFragment
: 创建并返回特定媒体源的 VIP 支付或登录 Fragment。 -
buildXimalayaPayDialog
: 根据喜马拉雅媒体项的购买类型,构建并返回一个定制的购买弹窗对话框。 -
ximalayaBuyFragment
: 根据登录状态和媒体项的购买类型,决定并返回正确的 Fragment(登录、VIP 购买或专辑购买)。
-
-
播放控制和弹窗 : 提供了处理播放相关弹窗和 Fragment 的方法,例如
showRecognizeDialog
用于显示正在识别歌曲的弹窗,switchLrcFragment
用于切换歌词视图。 -
上下文处理 : 包含了一些辅助方法,用于在
FragmentContextWrapper
中获取实际的Fragment
对象,以及处理上下文的层级关系。这使得导航工具类可以在不同类型的上下文中通用。
提供的这五个类是为了实现一套强大、灵活且易于维护的数据绑定和UI构建框架,尤其是在处理多媒体应用中常见的复杂UI和动态数据时。这种设计模式遵循了许多软件工程的最佳实践,例如关注点分离、模块化和代码复用。
以下是详细解释为什么要这样设计的理由:
1. 关注点分离 (Separation of Concerns)
-
DataBindingConvertor.java
和BindingResourceUtil.kt
: 这两个类负责将后端数据模型(如MediaMetadataCompat
、QrCodeTokenInfo
)转换为UI所需的资源类型(如TextResource
、ImageResource
)或特定的文本格式(如播放速度字符串、二维码URL)。它们将数据处理和格式化的逻辑从视图层(XML布局)和视图模型层中分离出来,使得数据模型可以保持简洁,而不必包含与UI呈现相关的复杂逻辑。 -
DatabindingAdapter.kt
: 这个类专门负责将数据绑定到实际的UI组件上。它处理诸如图片加载(使用Glide)、设置圆角、动态调整布局、添加动画等所有与视图操作相关的细节。这使得XML布局文件只专注于声明性的UI结构,而不需要包含任何实现细节。 -
OutlineProvider.kt
: 这个类将View的轮廓裁剪逻辑独立出来,专门用于为View设置不同类型的圆角。这使得圆角效果可以被复用,并且与View的背景、内容等其他属性完全解耦。
2. 代码复用和模块化
-
DataBindingConvertor.java
和BindingResourceUtil.kt
: 许多UI元素(如播放速度图标、下载按钮图标)在应用的多个地方都会用到。将这些转换逻辑集中在这两个工具类中,可以避免在每个地方都重复编写相同的when
或if-else
语句来判断状态和选择资源。 -
DatabindingAdapter.kt
: 通过创建自定义的BindingAdapter
,可以在多个XML布局文件中复用相同的UI绑定逻辑。例如,setImageUrlWithRadius
方法可以用于应用中任何需要加载带圆角图片的ImageView
,而无需在每个布局或Fragment
中手动调用 Glide。 -
OutlineProvider.kt
: 同样的,如果多个 View 需要相同的圆角效果(例如,所有专辑封面都需要四个角的圆角),只需在 XML 中通过数据绑定设置OutlineProvider
即可,无需在每个地方都创建新的ViewOutlineProvider
对象。
3. 易于维护和扩展
-
清晰的职责 : 当需要修改某个功能时,开发者可以快速定位到相应的类。例如,要改变所有播放速度图标的样式,只需修改
BindingResourceUtil.kt
中的playbackSpeedIcon
方法即可,而无需在多个文件中寻找和修改代码。要调整图片加载逻辑,只需修改DatabindingAdapter.kt
中的 Glide 相关方法。 -
灵活的导航 :
DataBindingNavigationUtil.kt
将所有复杂的导航逻辑(例如,根据媒体项类型决定打开哪个支付或登录Fragment)封装在一个地方。这使得在需要修改导航流程时,开发者只需关注这个工具类,而不是在不同的Activity
或Fragment
中修改分散的startActivity
或beginTransaction
代码。 -
适应多媒体源 : 媒体应用通常需要支持多种媒体源(如酷我、喜马拉雅、爱奇艺)。
BindingResourceUtil.kt
中的许多方法都通过判断mediaId
来适配不同的媒体源,并返回相应的资源,这使得应用能够轻松地处理多媒体源带来的差异性。