Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android 自定义Binding Adapter

Android 自定义Binding Adapter实战应用及作用详解

作者:每次的天空

本文详解Android BindingAdapter的使用,通过自定义属性绑定和复杂逻辑封装,提升XML与代码的解耦,实现模块化、复用和易维护,适用于列表、图片加载等场景,并结合@JvmStatic和companion object优化静态方法调用,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧

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")
        @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.@JvmStatic和companion 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 的特定角或所有角设置圆角效果。

DatabindingAdapter.kt

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

BindingResourceUtil.kt

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

DataBindingConvertor.java

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

DataBindingNavigationUtil.kt

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

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

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

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

2. 代码复用和模块化

3. 易于维护和扩展

到此这篇关于Android 自定义Binding Adapter实战应用的文章就介绍到这了,更多相关Android 自定义Binding Adapter内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文