Android -自定义Binding Adapter实战应用

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_iconmedia_source_name。当 XML 中同时使用这两个属性时,会调用 setMediaSourceInfo 方法。


2. 常见应用场景与代码分析

这份代码覆盖了多种常见的 Binding Adapter 应用场景,我们可以逐一分析:

2.1. 绑定简单属性和多个属性

setMediaSourceInfo 方法:

注解: @BindingAdapter("media_source_icon", "media_source_name")

应用:DrawableString 类型的数据绑定到 MediaSourceBar 这个自定义 View 上,分别设置图标和名称。

知识点: @BindingAdapter 注解可以接收多个属性名,当这些属性都在 XML 中出现时,被注解的方法会被调用。方法参数的顺序和类型必须与属性值匹配。

setMediaSourceRightButton 方法:

注解: @BindingAdapter("media_source_right_button")

应用: 将一个 Boolean 值绑定到 MediaSourceBar 的一个按钮上,控制其旋转状态。

知识点: 展示了如何绑定单个自定义属性,将一个布尔值直接传递给 View 的方法。

2.2. 处理列表数据绑定到 RecyclerView

bindRecyclerView 方法:

注解: @BindingAdapter("data")

应用: 将一个 MutableList<ListItem> 数据列表直接绑定到 RecyclerView

知识点: 这个方法非常实用,它通过检查 RecyclerViewadapter 类型(确保是 RecyclerViewListAdapter),然后调用 adapter.submitList() 来更新数据。这在 ViewModel 中处理列表数据时非常方便,只需在 XML 中设置 app:data="@{viewModel.myList}",就能实现列表的自动更新。

2.3. 绑定复杂对象和多参数

bindMediaMetadataInfo 方法:

注解: @BindingAdapter(value = ["media_metadata", "media_source"], requireAll = false)

应用:MediaMetadataServiceBean 两个复杂的对象绑定到 PlayInfoView 上。

知识点:

value = [...]:用于定义多个属性。

requireAll = false:这是一个重要的参数。它表示这些属性不必同时出现在 XML 中。如果设置为 true(默认),只有当 XML 中同时包含了所有属性时,该方法才会被调用。设置为 false 意味着只要其中一个属性存在,方法就会被调用。

方法中的逻辑展示了如何根据 mediaSource.packageName 进行条件判断,然后设置不同的 View 状态,这是 Binding Adapter 中处理复杂逻辑的常见方式。

2.4. 绑定图片加载库

imageUricycleImageUri 方法:

注解: @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. @JvmStaticcompanion object

@JvmStatic 这是一个 Kotlin 注解,用于标记一个方法为静态方法。在 companion object 中使用此注解后,该方法可以像 Java 的静态方法一样,通过类名直接调用,这正是 Data Binding 编译器所要求的。

companion object Kotlin 中的伴生对象,用于存放与类相关的静态成员。将所有 Binding Adapter 方法放在 companion object 中是 Kotlin 的最佳实践。

OutlineProvider.kt

OutlineProvider 类是一个自定义的 ViewOutlineProvider,它的主要作用是为 View 设置圆角裁剪(clipping)效果。它接收 CornerTyperadius 作为参数,然后根据这些参数生成一个 Path,并用它来定义 View 的轮廓(outline)。这使得开发者可以在不修改 View 背景或使用其他复杂方法的情况下,轻松地为 View 的特定角或所有角设置圆角效果。

  • CornerType 枚举定义了九种不同的圆角类型,包括:

    • ALL: 所有四个角都设置圆角。

    • TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT: 仅设置单个角。

    • TOP, BOTTOM, LEFT, RIGHT: 设置特定边上的两个角。

  • getOutline 方法是核心逻辑所在,它根据 cornerTyperadius 的值创建一个 floatArray,该数组用于 PathaddRoundRect 方法来创建带有指定圆角的矩形路径。最后,它使用 outline.setPath(path) 将这个路径应用到 View 的轮廓上。

DatabindingAdapter.kt

DatabindingAdapter 类是一个包含大量静态 BindingAdapter 方法的集合,这些方法用于在 XML 布局文件中实现自定义属性和数据绑定。它的核心作用是:

  • 图片加载 : 提供了多种方法(如 setImageUrlsetCircleImageUrlsetIqiyiSpecifiedSizeImageUrl)来使用 Glide 库加载图片。这些方法支持处理不同的图片资源类型(ID、Drawable、URI),设置占位图、圆角半径、高斯模糊等效果。

  • UI 控件属性设置: 提供了许多用于设置各种 UI 控件属性的方法,例如:

    • bindTabLayoutTabs: 动态绑定和设置 TabLayout 的 Tab。

    • setLayoutHeight, layoutMarginTop 等: 设置 View 的布局参数(如高度、边距)。

    • setQrcodeStatus: 设置 QrcodeScreenView 的状态和内容。

    • setXimalayaAlbumTags: 根据媒体项的数据,动态显示喜马拉雅的VIP、付费、精品等标签图标。

    • setIqyNormalModelTag: 根据媒体项数据,动态显示爱奇艺的VIP、独家等标签图标。

    • setOperationPanelItemActivedDisabled: 管理操作面板项的激活和禁用状态。

    • strikeThrough: 为 TextView 的文本添加删除线效果。

  • 动画控制 : 提供了用于控制 View 显示和隐藏动画的方法,例如 setPopupOpenAnimationsetCancelAnimation 等。

  • 逻辑封装: 将复杂的逻辑(如根据媒体数据判断显示哪个标签图标)封装在 BindingAdapter 方法中,使得 XML 布局文件更加简洁和易读。

BindingResourceUtil.kt

BindingResourceUtil 是一个工具类,包含了一系列静态方法,用于将数据模型中的原始值(如浮点数、字符串、枚举)转换为可用于数据绑定的资源对象(如 ImageResourceTextResource)。它的核心作用是:

  • 数据到资源的转换: 提供了将逻辑值转换为 UI 资源的方法,例如:

    • playbackSpeedIcon: 根据播放速度的浮点值返回相应的图标资源 ID。

    • definitionText: 根据视频清晰度的字符串返回相应的文本资源 ID。

    • ximalayaDownloadIcon: 根据喜马拉雅媒体项的下载类型返回相应的下载图标资源 ID。

  • 多源媒体适配 : 提供了针对不同媒体源(酷我、喜马拉雅、乐听)的资源转换方法。例如,playingCoverPlaceHolder 会根据 mediaId 判断媒体源,并返回相应的占位图。

  • 文案和状态转换 : 将逻辑状态(如下载状态、二维码状态)转换为用户可读的文本资源 ID。例如,downloadDisabledReason 根据下载状态返回相应的禁用原因文本。

DataBindingConvertor.java

DataBindingConvertor 是一个简单的 Java 类,它使用 @BindingConversion 注解定义了一些类型转换方法。它的核心作用是:

  • 自动类型转换 : 实现了 Android Data Binding 框架中的自动类型转换功能。这使得开发者可以在 XML 布局文件中直接使用原始类型(如 intCharSequenceDrawableUri),而无需手动将其包装成 TextResourceImageResource

  • 简化 XML 绑定 : 例如,当你在 XML 中将一个 int 类型的资源 ID 绑定给一个需要 TextResource 的属性时,convertTextResource(int id) 方法会自动被调用,完成类型转换。

  • 处理特定数据模型 : 提供了针对 QrCodeTokenInfoOrderBeanIqiyiOrderBean 的转换方法,用于从这些数据模型中提取二维码 URL。

DataBindingNavigationUtil.kt

DataBindingNavigationUtil 是一个工具类,专门用于处理与导航相关的逻辑。它的核心作用是:

  • Fragment 和 Activity 导航 : 提供了 openFragment 等方法,用于在不同的上下文(ActivityFragment)中打开新的 Fragment。

  • 业务逻辑封装: 封装了与具体业务相关的导航逻辑,例如:

    • kuwoVipPayFragment, ximalayaLoginFragment: 创建并返回特定媒体源的 VIP 支付或登录 Fragment。

    • buildXimalayaPayDialog: 根据喜马拉雅媒体项的购买类型,构建并返回一个定制的购买弹窗对话框。

    • ximalayaBuyFragment: 根据登录状态和媒体项的购买类型,决定并返回正确的 Fragment(登录、VIP 购买或专辑购买)。

  • 播放控制和弹窗 : 提供了处理播放相关弹窗和 Fragment 的方法,例如 showRecognizeDialog 用于显示正在识别歌曲的弹窗,switchLrcFragment 用于切换歌词视图。

  • 上下文处理 : 包含了一些辅助方法,用于在 FragmentContextWrapper 中获取实际的 Fragment 对象,以及处理上下文的层级关系。这使得导航工具类可以在不同类型的上下文中通用。

提供的这五个类是为了实现一套强大、灵活且易于维护的数据绑定和UI构建框架,尤其是在处理多媒体应用中常见的复杂UI和动态数据时。这种设计模式遵循了许多软件工程的最佳实践,例如关注点分离、模块化和代码复用。

以下是详细解释为什么要这样设计的理由:

1. 关注点分离 (Separation of Concerns)

  • DataBindingConvertor.javaBindingResourceUtil.kt : 这两个类负责将后端数据模型(如 MediaMetadataCompatQrCodeTokenInfo)转换为UI所需的资源类型(如 TextResourceImageResource)或特定的文本格式(如播放速度字符串、二维码URL)。它们将数据处理和格式化的逻辑从视图层(XML布局)和视图模型层中分离出来,使得数据模型可以保持简洁,而不必包含与UI呈现相关的复杂逻辑。

  • DatabindingAdapter.kt: 这个类专门负责将数据绑定到实际的UI组件上。它处理诸如图片加载(使用Glide)、设置圆角、动态调整布局、添加动画等所有与视图操作相关的细节。这使得XML布局文件只专注于声明性的UI结构,而不需要包含任何实现细节。

  • OutlineProvider.kt : 这个类将View的轮廓裁剪逻辑独立出来,专门用于为View设置不同类型的圆角。这使得圆角效果可以被复用,并且与View的背景、内容等其他属性完全解耦。

2. 代码复用和模块化

  • DataBindingConvertor.javaBindingResourceUtil.kt : 许多UI元素(如播放速度图标、下载按钮图标)在应用的多个地方都会用到。将这些转换逻辑集中在这两个工具类中,可以避免在每个地方都重复编写相同的 whenif-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)封装在一个地方。这使得在需要修改导航流程时,开发者只需关注这个工具类,而不是在不同的 ActivityFragment 中修改分散的 startActivitybeginTransaction 代码。

  • 适应多媒体源 : 媒体应用通常需要支持多种媒体源(如酷我、喜马拉雅、爱奇艺)。BindingResourceUtil.kt 中的许多方法都通过判断 mediaId 来适配不同的媒体源,并返回相应的资源,这使得应用能够轻松地处理多媒体源带来的差异性。

相关推荐
每次的天空3 小时前
Android-Git技术总结
android·学习
一直向钱5 小时前
Android 6.0+ 动态权限请求模块,这个模块会包含 权限检查、请求、结果处理 等核心功能,并且支持 单个 / 多个权限请求、权限拒绝后的引导
android
ganshenml6 小时前
【Android】两个不同版本的jar放进一个工程打成aar会有问题么?
android·java·jar
2501_916008896 小时前
iOS 26 系统流畅度剖析:Liquid Glass 动画表现 + 用户反馈
android·macos·ios·小程序·uni-app·cocoa·iphone
alexhilton13 小时前
灵活、现代的Android应用架构:完整分步指南
android·kotlin·android jetpack
hnlgzb15 小时前
build.gradle中的dependencies 中API
android
xiaguangbo16 小时前
rust slint android 安卓
android·linux·rust
lichong95116 小时前
【大前端++】Android studio Log日志高对比度配色方案
android·java·前端·json·android studio·大前端·大前端++
00后程序员张18 小时前
iOS 开发环境搭建完整指南 Xcode 安装配置、iOS 开发工具选择、ipa 打包与 App Store 上架实战经验
android·macos·ios·小程序·uni-app·iphone·xcode