小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

QMUILayout, 让 UI 开发更简单

在 Android UI开发过程中,我们会觉得非常繁琐的事情是什么呢?分隔线、圆角、边框、阴影、点击态等,在以往的实践中,我们都是通过 drawable 去实现的,非常麻烦。以分隔线为例,如果左右都打通,那还能复用,但现在的设计师都喜欢左边或右边有一定的 inset,而 inset 的值并非一成不变的,这不经意间就使得项目添加了数不清的 drawable,因此目前大多数人都开始尝试不使用 drawble 去解决这些问题了,QMUI 也不例外。

QMUI 1.1.0 版本带来的新的组件套装 QMUILayout (QMUILinearLayout、QMUIRelativeLayout、QMUIFramelayout),这篇文章将介绍 QMUI 提出的解决方法。

点击态

点击态最为简单,QMUI 早已经提出用 alpha 来表示点击态,因此提供了了 QMUIAlpha 系列,QMUILayout 继承自 QMUIAlpha 系列,但默认禁止了 alpha 行为, 你可以通过以下两个方法开启:

// change alpha when pressed
setChangeAlphaWhenPress(boolean changeAlphaWhenPress);

// change alpha when disabled
setChangeAlphaWhenDisable(boolean changeAlphaWhenDisable)  

当然,点击时背景变灰也是非常常见的。qmui 最开始提供了一系列的用于 list item 的 drawable, 现在我们基本只需要 qmui_s_list_item_bg_with_border_none, 因为现在分割线不再受背景控制了。

分隔线

分隔线是平常使用得非常多的,上下分隔下用的较多,左右分隔线用得比较少,QMUILayout 都提供了 xml 和 java 代码的支持:

<declare-styleable name="QMUILayout">  
    ...
    <attr name="qmui_bottomDividerHeight" format="dimension"/>
    <attr name="qmui_bottomDividerColor" format="color|reference"/>
     <attr name="qmui_bottomDividerInsetLeft" format="dimension"/>
     <attr name="qmui_bottomDividerInsetRight" format="dimension"/>
     <attr name="qmui_topDividerHeight" format="dimension"/>
     <attr name="qmui_topDividerColor" format="color|reference"/>
    <attr name="qmui_topDividerInsetLeft" format="dimension"/>
    <attr name="qmui_topDividerInsetRight" format="dimension"/>
    <attr name="qmui_leftDividerWidth" format="dimension"/>
    <attr name="qmui_leftDividerColor" format="color|reference"/>
    <attr name="qmui_leftDividerInsetTop" format="dimension"/>
    <attr name="qmui_leftDividerInsetBottom" format="dimension"/>
    <attr name="qmui_rightDividerWidth" format="dimension"/>
    <attr name="qmui_rightDividerColor" format="color|reference"/>
    <attr name="qmui_rightDividerInsetTop" format="dimension"/>
    <attr name="qmui_rightDividerInsetBottom" format="dimension"/>
    ...
</declare-styleable>  

四个维度的支持,使用非常方便, java 接口:

/**
 * config the top divider
 */
void updateTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor);

/**
 * show top divider, and hide others
 */
void onlyShowTopDivider(int topInsetLeft, int topInsetRight, int topDividerHeight, int topDividerColor);

// same for others

其实现原理也很简单,就是在 dispatchDraw 里主动 draw 上去的,那我们为什么是在 dispatchDraw 而不是在 onDraw 里操作呢? 因为我们不想分隔线被子元素、背景等因素遮挡。

阴影、圆角、边框

这是 QMUILayout 最令人兴奋的地方,大家可以在 QMUIDemo 上体验下效果:

其实原理很简单,利用 android 5.0 后提供的 elevation 来实现的, 大多数人通过阅读官方文档或者别人的教材,了解了 elevation 可以控制 shadow 的大小,但是很少人知道 shadow 还会被另外一个因素所控制:outline 的 alpha。 有了这两个因素,可以更精确的控制 shadow 的深浅,能无限接近设计师的阴影要求,当然,除了阴影颜色,那是 QMUILayout 所不能控制的。

利用 outline 还可以用来实现圆角,以前我们用背景去实现圆角效果,但你需要非常小心的保护圆角背景不被子元素所覆盖,但 outline 不会有这个问题。outline 如此强大,如果还不知道它,那么赶紧去通过View.setOutlineProvider() 了解一下它吧。

但 outline 也有缺陷的,它没办法做到四个圆角分别控制,现实使用过程中,有一个圆角或三个圆角的情况比较少,所以我在 QMUILayout 中支持了 显示两个圆角的方法,但可惜的是会丢失阴影,现实总是不尽人意,尽自己最大努力就好。

另外一个比较关心的话题是, 4.x的手机怎么办?网页重构早就讲究降级兼容,所以我也认为没有必要让 4.x 和 5.x+ 保持同样的用户体验。因此在 4.x 下,阴影肯定是没办法使用的,只能使用圆角和边框,要使用它们,我们需要提供一个额外的参数: qmui_outerNormalColor, 这个参数是什么意思呢?这首先要理解圆角在 4.x 上的实现: 它的实现是在 dispatchDraw 后,draw 上一层 mask, 然后镂空中间部分,因此 qmui_outerNormalColor 就是 mask 的颜色,一般要与 QMUILayout 外部环境相同, 这样在用户眼中,看到的就是圆角了。如果你没有提供这个属性,那么圆角将不会生效。此外,它也无法解决 dialog 这种外部环境透明的场景。

接下来看看提供的非常有用的 API:

/**
 * use the shadow elevation from the theme
 */
void setUseThemeGeneralShadowElevation();  
/**
 * See {@link android.view.View#setElevation(float)}
 *
 * @param elevation
 */
void setShadowElevation(int elevation);

/**
 * See {@link View#getElevation()}
 *
 * @return
 */
int getShadowElevation();

/**
 * set the outline alpha, which will change the shadow
 *
 * @param shadowAlpha
 */
void setShadowAlpha(float shadowAlpha);

/**
 * get the outline alpha we set
 *
 * @return
 */
float getShadowAlpha();

/**
 * set the layout radius
 * @param radius
 */
void setRadius(int radius);

/**
 * set the layout radius with one or none side been hidden
 * @param radius
 * @param hideRadiusSide
 */
void setRadius(int radius, @QMUILayoutHelper.HideRadiusSide int hideRadiusSide);

/**
 * get the layout r
 * @return
 */
int getRadius();  
/**
 * the shadow elevation only work after L, so we provide a downgrading compatible solutions for android 4.x
 * usually we use border, but the border may be redundant for android L+. so will not show border default,
 * if your designer like the border exists with shadow, you can call setShowBorderOnlyBeforeL(false)
 *
 * @param showBorderOnlyBeforeL
 */
void setShowBorderOnlyBeforeL(boolean showBorderOnlyBeforeL);

/**
 * in some case, we maybe hope the layout only have radius in one side.
 * but there is no convenient way to write the code like canvas.drawPath,
 * so we take another way that hide one radius side
 *
 * @param hideRadiusSide
 */
void setHideRadiusSide(@HideRadiusSide int hideRadiusSide);

/**
 * get the side that we have hidden the radius
 *
 * @return
 */
int getHideRadiusSide();

/**
 * this method will determine the radius and shadow.
 *
 * @param radius
 * @param shadowElevation
 * @param shadowAlpha
 */
void setRadiusAndShadow(int radius, int shadowElevation, float shadowAlpha);

/**
 * this method will determine the radius and shadow with one or none side be hidden
 *
 * @param radius
 * @param hideRadiusSide
 * @param shadowElevation
 * @param shadowAlpha
 */
void setRadiusAndShadow(int radius, @HideRadiusSide int hideRadiusSide, int shadowElevation, float shadowAlpha);

/**
 * border color, if you don not set it, the layout will not draw the border
 *
 * @param borderColor
 */
void setBorderColor(@ColorInt int borderColor);

/**
 * border width, default is 1px, usually no need to set
 *
 * @param borderWidth
 */
void setBorderWidth(int borderWidth);  

QMUILayout 所有的 API 都由 IQMUILayout 所规范, 由 QMUILayoutHelper 所实现,如果你需要为自己的自定义 view 实现这些方法, 那么你可以在你的类中引入 QMUILayoutHelper,具体可以参考 QMUILinearLayout/QMUIFrameLayout/QMUIRelativeLayout

最后一点,关于性能,View.setOutlineProvider() 会存在性能消耗,不过就我们的测试数据统计,这点消耗是可以容忍的。目前 QMUILayout 已经为读书项目服务了几个版本了,稳定性应该是蛮好的,希望大家喜欢。

←微信← →支付宝 →