Android -自定义Binding Adapter实战应用
1. Binding Adapter 的基本概念和作用
Binding Adapter 是一个桥梁,它允许你在 XML 布局文件中,将自定义的属性(例如 app:image_corners_uri
)与 Kotlin/Java 代码中的方法绑定起来。它解决了 Android 默认的 Data Binding 无法处理复杂逻辑或自定义 View 属性的问题。
class DataBindingAdapter {companion object {@BindingAdapter("media_source_icon", "media_source_name")@JvmStaticfun setMediaSourceInfo(view: MediaSourceBar, drawable: Drawable?, name: String?) {name?.let { view.setMediaSourceName(it) }drawable?.let { view.setMediaSourceIcon(it) }}@BindingAdapter("media_source_right_button")@JvmStaticfun setMediaSourceRightButton(view: MediaSourceBar, rotate: Boolean?) {rotate?.let { view.setMediaSourceRightButton(it) }}@BindingAdapter("data")@JvmStaticfun bindRecyclerView(recyclerView: Recyclerview,list: MutableList<ListItem>?,) {val adapter = recyclerView.adapterif (adapter is RecyclerViewListAdapter) {list?.let { adapter.submitList(it) }}}@BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)@JvmStaticfun 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)@JvmStaticfun bindMediaMetadataProgress(simpleMediaView: PlayInfoView,duration: Long?,progress: Long?) {simpleMediaView.setProgress(duration, progress)}@BindingAdapter("player_commands", requireAll = false)@JvmStaticfun bindMediaMetadataCommands(simpleMediaView: PlayInfoView,commands: Player.Commands?,) {simpleMediaView.setPlayerCommands(commands)}@BindingAdapter("isPlaying", requireAll = false)@JvmStaticfun 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)@JvmStaticfun bindPlayMode(simpleMediaView: PlayInfoView,mode: PlayMode?) {if (mode != null) {simpleMediaView.setPlayMode(mode)}}@BindingAdapter("type")@JvmStaticfun setType(view: PlayInfoView, type: Int) {view.setType(type)}@BindingAdapter("discType")@JvmStaticfun setDiscType(view: PlayInfoView, type: Int) {view.setDiscType(type)}@BindingAdapter("image_corners_uri", "image_radius", requireAll = false)@JvmStaticfun 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)@JvmStaticfun 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)@JvmStaticfun playerAlbumImageCover(view: tech.jidouauto.component.widgets.basic.ImageView,uri: Any?) {}@BindingAdapter("banner_data", requireAll = false)@JvmStaticfun bindBannerViewData(view: TopBannerCarouselPager,data: MainBannerItem?,) {Logger.info("bind main banner data=${data?.items}")if (data == null) returnview.setAdapter(TopBannerCarouselPager.Adapter(data.items, data.itemOnClick))}@BindingAdapter("show_background", requireAll = false)@JvmStaticfun showBackGround(view: View, isShow: Boolean) {}/*** 控制Lottie动画的播放状态* @param view LottieAnimationView实例* @param isPlaying 是否正在播放(true:播放,false:暂停)*/@BindingAdapter("lottie_playing_state")@JvmStaticfun 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")@JvmStaticfun 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
来适配不同的媒体源,并返回相应的资源,这使得应用能够轻松地处理多媒体源带来的差异性。