Visibility Extension of Android View

Posted on By ᵇᵒ

如下的扩展方法应该是大家都很容易写出来的:

fun View.visible(): View = apply { visibility = View.VISIBLE }

fun View.invisible(): View = apply { visibility = View.INVISIBLE }

fun View.gone(): View = apply { visibility = View.GONE }

实际业务中,绝大部分情况(甚至可以说全部)都是需要根据某种条件判断再决定是否需要显示 view,所以很自然地想到将判断条件一起加入扩展:

fun View.visibleIf(visible: Boolean): View = apply {
    visibility = if (visible) View.VISIBLE else View.GONE
}

但是如此一来,一个判断条件最多只能涵盖两种情况,无法完全覆盖 View.VISIBLE、View.INVISIBLE、View.GONE 三种 case, 上面的代码我们采用了实际应用更多的 View.GONE,而无视了 View.INVISIBLE。想做到 case 完备,要么增加判断条件,要么增加扩展 api。

fun View.visibleIf(visible: Boolean, elseTakeGone: Boolean = true): View = apply {
    visibility = if (visible) View.VISIBLE else if (elseTakeGone) View.GONE else View.INVISIBLE
}

如上代码增加判断条件,elseTakeGone 用于决定 else 分支使用 INVISIBLE 还是 GONE,默认值 true 使用 GONE。该方法存在的问题是, 如果使用 INVISIBLE,则调用方的代码类似这样 view.visibleIf(bool, false),两个同样 Boolean 类型的参数一定程度上容易造成理解歧义。

@IntDef(value = [View.VISIBLE, View.INVISIBLE, View.GONE])
@Retention(AnnotationRetention.SOURCE)
annotation class Visibility

fun View.visibleIf(visible: Boolean, @Visibility `else`: Int = View.GONE): View = apply {
    visibility = if (visible) View.VISIBLE else `else`
}

改进后的方法使用 visibility 的 Int 值作为第二个参数,直观了很多。但是该方法也有个明显的问题, Kotlin 代码并不会对 IntDef 的进行检查告警discuss-7029, 这就导致调用方可以传 View.VISIBLE、View.INVISIBLE、View.GONE 之外的任何整数,尽管继续执行也不会报错,但这不是我们希望看到的。

enum class Visibility(val viewVis: Int) {
    Visible(View.VISIBLE),
    Invisible(View.INVISIBLE),
    Gone(View.GONE)
}

fun View.visibleIf(visible: Boolean, `else`: Visibility = Visibility.Gone): View = apply {
    visibility = if (visible) View.VISIBLE else `else`.viewVis
}

如上的代码使用枚举完美地解决了 IntDef 无法告警的问题,同时我们也看到代码更加复杂了,而且枚举会带来一定程度的性能问题(但据说简单的枚举是可以被编译器优化成整型的)。 相比起来个人更倾向于上面的 IntDef,简单明了,虽然我们没法做到禁止调用方传其他整型值,但至少调用方是清楚自己传了不应该传的值。程序的正确性更应该由编写的人自行保证 而不是交由编译器,IDE 之类的工具只是尽可能多地给程序员提供帮助。

上面的都是通过增加参数来解决,下面说说增加 api 的思路:

fun View.invisibleIf(invisible: Boolean): View = apply {
    visibility = if (invisible) View.INVISIBLE else View.VISIBLE
}

fun View.goneIf(gone: Boolean): View = apply {
    visibility = if (gone) View.GONE else View.VISIBLE
}

由于可见都是 View.VISIBLE,而不可见有 View.INVISIBLE 和 View.GONE 两种,故我们反过来定义 api,使用 invisibleIfgoneIf。该方案有两个缺点,一是 api 个数增加了(貌似废话), 优秀 library 的设计准则之一应该是用足够少的 api 涵盖足够多的使用 case;另一个缺点是由于反过来定义,思维上有点别扭,比较直观的思维都是考虑什么时候 view 可见。

啰里八嗦了一大堆,貌似最后也没个完美的方案,但生活不就是这样么,哪有什么完美的事物。就这个扩展来说,个人还是更倾向使用 IntDef 的单个 api 方案。