Android应用性能优化系列视图篇——恼人的分割线留白解决之道


       

相信很多一线的开发者都遇到过分割线,作为视觉设计中的最常用的元素之一,虽然简单易画,但在布局排版中往往却起影响视图层级结构的重要作用。往往由于一道小小的分割线,不仅在layout中多个数个视图,而且容易导致布局层级的加深,甚至还需要在Java代码中做逻辑控制。




虽然Android官方提供的布局中,比如ListView、LinearLayout等对分割线都有了相应的实现,但是在处理分割线留白这种设计时常常有心无力。而在越来越多的APP中,分割线留白已经成为了一种设计趋势,所以,如何简单高效地实现分割线成为了一个值得研究的命题。








1、ListView的分割线




首先,ListView是自带分割线属性的(而GridView并没有,不知道android官方咋想的),一共有四个相关的属性,dividerdividerHeightheaderDividersEnabledfooterDividersEnabled。支持定义分割线的颜色和高度等,非常实用的属性,但在实际开发中设计师却不喜欢按照常理出牌。




比如下面这些列表类型的设计图,设计师最喜欢这种左侧留白右侧通栏或者两侧留白的设计方式。




这里写图片描述   这里写图片描述




而官方的分割线则是左右通栏,开发者遇到这种问题,大概是有三种常规的解决方案。








方案1:ListView设置左右margin或者padding




这种方式虽然能够达到分割线留白的效果,但是有一定的局限性,分割线左侧并不总是被设计成和内容对齐。同时,缺陷很明显,给ListView设置了左右margin或者padding,会导致Item点击的按压背景也被留白(效果如下图)。当然,如果不在意这些细节,那也无可厚非。




这里写图片描述




缺陷: 按压效果有瑕疵,另外如果分割线不与内容上下对齐,这种方式无效








方案2:给每个Item设置带有分割线留白的背景图




这是较为常用的方案之一,分割线正常显示,按压效果正常显示。看似很完美,但是不得不面对一个较大的问题,由于最后一个Item不显示分割线,所以需要在Java代码中控制,比如这样:








  @Override 
  
  public View getView(int position, View convertView, ViewGroup parent) {
      ....


      if (position == getCount() - 1) {
          convertView.setBackground(null);
      } else {
          convertView.setBackgroundResource(R.drawable.bg_line);
      }
      return convertView;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11



在Java代码中控制视图样式,无论如何都是一个相当不合理的选择。




其次,这种方式不支持footer和header的分割线,footerDividersEnabled和headerDividersEnabled两个属性就无法使用了。




接着,既然使用带有分割线的图片做背景,那么,如果页面设计修改,参数调整,就需要重新切图,所以维护起来相当吃力。




最后,为了一条分割线而给整个Item视图设置背景,基本可以认为是出现了过度绘制的情况。




缺陷:代码控制视图、footerDividersEnabled和headerDividersEnabled无用、留白距离难以维护,过度绘制








方案3:在Item中添加一个View设置高度背景作为分割线








<View 
  
    android:layout_width="match_parent"
     android:layout_height="0.5dip"
     android:layout_marginLeft="15dip"
     android:background="#99cccccc"/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5



这是最最最常用却最最最不合理的一个方案,然而很多人却意识不到不合理。以上面图为例,Item中只有一个TextView,所以正常情况下layout中只有TextView一个视图,但是如果以这种方式,必须在外层套上LinearLayout布局,这样一来,视图层次加深,视图冗余。假设一个列表显示10项,冗余出来的布局数就是20(分割线10+父布局10)。




另外,仍然避免不了在Java中控制最后一项显示与否,比如这样:








View line = convertView.findViewById(R.id.item_line); 
  
if (position == getCount() - 1) {
    line.setVisibility(View.INVISIBLE);
} else {
    line.setVisibility(View.VISIBLE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6



footerDividersEnabled和headerDividersEnabled两个属性同样也无法使用。




缺陷:布局大量冗余、代码控制视图、footerDividersEnabled和headerDividersEnabled无用








方案4:重新定义分割线Drawable的Bounds




首先,需要清楚一个事实:出现以上情况的矛盾点,是官方ListView的分割线属性不支持左右留白。所以最佳的解决方案,就是使得官方的分割线支持这种功能,这样既利于扩展,也利于提高性能。




先来简单看一下,ListView的源码是如何实现分割线的功能的。








    void drawDivider(Canvas canvas, Rect bounds, int childIndex) { 
  
        // This widget draws the same divider for all children
        final Drawable divider = mDivider;


        divider.setBounds(bounds);
        divider.draw(canvas);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7



在onDraw方法中,最终会调用到drawDivider方法。由于分割线是一个Drawable对象,上下左右的位置都是由Rect对象控制的,这个对象通过setBounds方法设置。




这个Rect对象的top和bottom属性我们是不需要关心的,只需要看left和right两个属性,默认情况下left=paddingLeft,right=width-paddingLeft-paddingRight,即表示分割线的起点和终点贯穿ListView的左右两侧。




dispatchDraw方法中可以验证这一点:








 protected void dispatchDraw(Canvas canvas) { 
  
    ...
    if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
        final Rect bounds = mTempRect;
        bounds.left = mPaddingLeft;
        bounds.right = mRight - mLeft - mPaddingRight;
        ...
        drawDivider(canvas, bounds, i);
    }
    ...
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11



如果我们能将bounds的left和right属性的值进行修改,那么就能实现控制分割线的左右边距了。




既然需要扩展ListView,最常用的方法就是继承重写了。不幸的是由于drawDivider方法的访问控制故并不能被复写,但值得庆幸的是ListView的Divider对象具有setter和getter方法。




具体的实现逻辑非常简单,核心是装饰模式,我就不去详细说了。




源码和范例详见:https://github.com/MegatronKing/DividerSample




用法非常简单,给ListView分割线扩充了两个属性:dividerPaddingLeftdividerPaddingRight,顾名思义。这两个属性值既可以在xml布局中配置也可以在代码中设置。




layout示例如下:








<com.megatronking.divider.view.DividerListView     
  
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:divider="http://schemas.android.com/apk/res-auto"
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="#99cccccc"
    android:dividerHeight="0.5dip"
    divider:dividerPaddingLeft="15dip"
    divider:dividerPaddingRight="15dip" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10







方案5:使用inset标签定义drawable




在Drawable家族中有一个特殊的存在:InsetDrawable,可以定义上下左右四个边界的留白,InsetDrawable同样使用了装饰模式,和方案4的机制有异曲同工之妙。当被装饰Drawable的Bound值变化时,重新定义Bound。另外,最强大的是可以直接使用xml定义。




<inset xmlns:android="http://schemas.android.com/apk/res/android" 
  
    android:insetLeft="15dip">

    <shape>
        <size android:height="0.5dip" />
        <solid android:color="#99cccccc" />
    </shape>
</inset>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7



通过insetLeft、insetRight、insetTop、insetRight四个属性可以定义不同方向的留白大小,另外还支持通过android:drawable熟悉引用另外一个Drawable即被装饰的Drawable。








2、LinearLayout的分割线




LinearLayout从Android 3.0版本开始,便添加了分割线属性。LinearLayout的分割线功能比ListView要强大一些,无论是横向线性布局还是纵向线性布局,都能够很好的支持。为了兼容3.0以下版本,v7包中提供一个LinearLayoutCompat布局,用法类似。




然而,知道这些属性的开发者并不多,很多时候还是使用着一条线一个View的方式。这种方式无疑使用繁琐,布局冗余。




现在先来简单介绍下LinearLayout的分割线功能。




LinearLayout的提供了三个分割线相关属性:




1、divider 必须引用一个drawable,无法使用或引用color。drawable一般是定义成shape,通过size指定宽度或高度,solid指定颜色。另外,由于LinearLayout分为横向布局和纵向布局,所以一般会定义两种分割线。




<shape xmlns:android="http://schemas.android.com/apk/res/android"> 
  
    <size android:height="0.5dip"/>
    <solid android:color="#99cccccc"/>
</shape>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4







<shape xmlns:android="http://schemas.android.com/apk/res/android"> 
  
    <size android:width="0.5dip"/>
    <solid android:color="#99cccccc"/>
</shape>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4



2、dividerPadding 控制分割线的留白距离,相比于ListView分割线的通栏,这个属性非常实用。但是却有一个缺陷:dividerPadding无法指定左右或上下,横向布局中上下都会有留白,纵向布局中,左右都会有留白。而,设计师往往喜欢左侧留白右侧通栏,所以这个属性有时就非常尴尬。




3、showDividers 这个属性是控制分割线显示位置的,一共有四个值:middle 在每一项中间添加分割线;end 在整体的最后一项添加分割线;beginning 在整体的最上方添加分割线;none 无;如果不设置这个属性的值,默认就是none,即不显示分割线。




虽然LinearLayout的分割线功能非常强大,但是遇到一侧留白的情况,还是无能为力。开发者还是要回到使用View作为分割线的老路上来,比如QQ浏览器的这种布局:




这里写图片描述




分割线左侧留白与文字对齐,右侧通栏,dividerPadding这个属性实在无能为力!




在分析解决方案之前,来简单看一下分割线的实现原理吧!








    void drawHorizontalDivider(Canvas canvas, int top) { 
  
        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
        mDivider.draw(canvas);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5







    void drawVerticalDivider(Canvas canvas, int left) { 
  
        mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
                left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
        mDivider.draw(canvas);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5



一个水平方向的分割线,一个是垂直方向的分割线。同样是通过setBounds方法确定分割线的位置,而且mDividerPadding属性会同时作用于左右或上下。




所以,解决方案和前面所讲的ListView的应该是完全一样的:重新定义分割线Drawable的Bounds




源码不细说了,详见:https://github.com/MegatronKing/DividerSample




扩展LinearLayout提供了分割线的额外四个属性:dividerPaddingLeft,dividerPaddingRight,dividerPaddingTop,dividerPaddingBottom。同样的,既可以在layout中配置也可以在代码中动态设置。




当orientation为vertical时,dividerPaddingLeft和dividerPaddingRight两个属性生效;orientation为horizontal时,dividerPaddingTop和dividerPaddingBottom两个属性生效。




layout示例如下:








<com.megatronking.divider.view.DividerLinearLayout         
  
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:divider="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="@drawable/line_horizontal"
    android:orientation="vertical"
    android:showDividers="middle"
    divider:dividerPaddingLeft="15dip"/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9



当然,通过InsetDrawable方式同样可以达到效果,原理其实差不多,不再赘述。








3、总结




在常规的应用开发中,用好分割线,能够减少视图数量,层次结构和代码逻辑,同时也方便后继者修改和维护。虽然性能提升有限,但是积少成多,对于追求完美和极致的开发者,希望能够有所帮助!








本博客不定期持续更新,欢迎关注和交流:




http://blog.csdn.net/megatronkings


       
   

你可能感兴趣的:(Android应用性能优化系列视图篇——恼人的分割线留白解决之道)