zoukankan      html  css  js  c++  java
  • Android 自定义控件 优雅实现元素间的分割线 (支持3.0以下)

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/42407923 ,本文出自:【张鸿洋的博客】

    1、概述

    话说,随着Android SDK版本的升级,很多控件增加了新的属性方便我们的使用,比如LinearLayout中多了:divider、showDividers等,用于为其内部元素添加分隔;但是呢,这样的属性在较低版本的SDK中不能被支持,那么,我们在开发过程中,可能会出现这样的需求:将这个新的特性想办法做到尽可能的向下兼容。有人说,可以自己写个新的控件去实现,这样的确可以,但是会不会太霸气了点。难道就没有接地气一点的方式么?嗯,本文就是这样的一个目的,以一种较为接地气的方式,实现新的属性的向下兼容。

    这样的情况在Android中肯定会很多,希望可以以此进行抛砖引玉,大家遇到类似的情况,提供一定的思路。这才是这篇博客的真正目的!

     

    2、divider相关用法

    为了保证简介性,这里就不讨论divider有多么多么好用神马的,因为不是我们的重点。当然了这里提供一篇divider的参考:grid-spacing-on-android (基本就是引出divider的用处,有兴趣的看下,本文的demo样子也将参考本链接)。

    大家先看一个效果图:

    如果要实现,这样的效果图,对于这3个Button大家会怎么做(主要看button):

    简单嘛:一个水平的线性布局,内部三个Button的weight都为1,然后第二个Button设置leftMargin,rightMargin就可以了。

    嗯,没问题,假设现在我有一个需求:经过某个操作Button3隐藏,然后让Button1和Button2按如下布局:

    这样的感觉是不是不错,虽然少了一个,完全不影响美观;但是,如果按照上述的答案

    “一个水平的线性布局,内部三个Button的weight都为1,然后第二个Button设置leftMargin,rightMargin就可以了”  Button2的右边会多出一个rightMargin 。 

    所以,这样的制作方式很明显不是最优秀的,最优秀的方案是,使用Linearlayout的divider、showDividers属性:

    布局代码如下:

     

    [html] view plaincopy
     
    1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     android:layout_width="match_parent"  
    3.     android:layout_height="wrap_content"  
    4.     android:padding="20dp"  
    5.     android:layout_margin="10dp"  
    6.     android:background="#22444444"  
    7.     android:orientation="vertical" >  
    8.   
    9.     <TextView  
    10.         android:layout_width="match_parent"  
    11.         android:layout_height="128dp"  
    12.         android:background="@android:color/darker_gray"  
    13.         android:gravity="center"  
    14.         android:text="application_logo" />  
    15.   
    16.     <LinearLayout  
    17.         android:id="@+id/buttons_container"  
    18.         android:layout_width="match_parent"  
    19.         android:layout_height="wrap_content"  
    20.         android:layout_marginTop="10dp"  
    21.         android:divider="@drawable/divider"  
    22.         android:orientation="horizontal"  
    23.         android:showDividers="middle" >  
    24.   
    25.         <Button  
    26.             android:id="@+id/btn_first"  
    27.             android:layout_width="0dp"  
    28.             android:layout_height="wrap_content"  
    29.             android:layout_weight="1"  
    30.             android:background="#ff0000"  
    31.             android:text="button_1" />  
    32.   
    33.         <Button  
    34.             android:id="@+id/btn_second"  
    35.             android:layout_width="0dp"  
    36.             android:layout_height="wrap_content"  
    37.             android:layout_weight="1"  
    38.             android:background="#00ff00"  
    39.             android:text="button_2" />  
    40.   
    41.         <Button  
    42.             android:id="@+id/btn_third"  
    43.             android:layout_width="0dp"  
    44.             android:layout_height="wrap_content"  
    45.             android:layout_weight="1"  
    46.             android:background="#0000ff"  
    47.             android:text="button_3" />  
    48.     </LinearLayout>  
    49.   
    50. </LinearLayout>  


    其实核心就是放置Button的LinearLayout设置了 android:divider="@drawable/divider"和 android:showDividers="middle" ;

     

    当然了,有人会说,我就是任性,我就用margin来实现,消失的时候,我显示去控制button的rightMargin为0也可以。嗯,是的,你不嫌麻烦的确没问题。那么现在问题又来了,我现在要求每个Button间的间隔是蓝色的,你怎么办?注意:我们这里的divider的值设置的是一个drawable噢~~没辙了吧。

    本例的drawable(divider.xml):

     

    [html] view plaincopy
     
    1. <?xml version="1.0" encoding="utf-8"?>  
    2. <shape xmlns:android="http://schemas.android.com/apk/res/android"  
    3.     android:shape="rectangle" >  
    4.     <size android:width="15dp" />  
    5.     <solid android:color="@android:color/transparent" />  
    6. </shape>  


    下面简单介绍下divider、showDividers、dividerPadding:

     

    divider可以设置一个drawable作为元素间的间隔;

    showDividers:可取值为:middle(子元素间)、beginning(第一个元素左边)、end(最后一个元素右边)、none;【关于垂直方向的类似】

    dividerPadding:设置绘制间隔元素的上下padding。

    很简单,大家自己动手做下实验就知道了。

    好了,到此,我们简单介绍了divider等的好处以及使用方式。但是这么优雅的来实现元素间的间隔只有在3.0以上才被支持,那么3.0以下怎么办呢?

    别怕,下面开始本文的重点,让divider兼容至3.0一下。

    3、自定义LinearLayout

    看了标题,大家认为又是自定义LinearLayout么~~

    嗯,继承LinearLayout是肯定的,我们没有办法改变它的源码,但是可以通过继承去改变一些特性。

    注意下:现在的目的是兼容至3.0以下:

    首先看一个3.0以下的效果图,不然你说我骗你:

    上面的布局文件在3.0以下显示就是这么个样子,完全无视间隔。

    首先考虑一个问题,对于divider、showDividers 3.0以下的LinearLayout肯定无视呀,咋办呢?

    我们实现个LinearLayout的子类,让它认识divider和showDividers~~~重视一下这里,这里就是我们向前迈进的一大步,以后遇到类似问题,都这么干。

    1、识别高版本的属性

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class IcsLinearLayout extends LinearLayout  
    2. {  
    3.     private static final int[] LL = new int[]  
    4.     { //  
    5.         android.R.attr.divider,//  
    6.             android.R.attr.showDividers,//  
    7.             android.R.attr.dividerPadding //  
    8.     };  
    9.   
    10.     private static final int LL_DIVIDER = 0;  
    11.     private static final int LL_SHOW_DIVIDER = 1;  
    12.     private static final int LL_DIVIDER_PADDING = 2;  
    13.   
    14.     /** 
    15.      * android:dividers 
    16.      */  
    17.     private Drawable mDivider;  
    18.     /** 
    19.      * 对应:android:showDividers 
    20.      */  
    21.     private int mShowDividers;  
    22.     /** 
    23.      * 对应:android:dividerPadding 
    24.      */  
    25.     private int mDividerPadding;  
    26.       
    27.     private int mDividerWidth;  
    28.     private int mDividerHeight;  
    29.   
    30.     public IcsLinearLayout(Context context, AttributeSet attrs)  
    31.     {  
    32.         super(context, attrs);  
    33.   
    34.         TypedArray a = context.obtainStyledAttributes(attrs, LL);  
    35.         setDividerDrawable(a.getDrawable(IcsLinearLayout.LL_DIVIDER));  
    36.         mDividerPadding = a.getDimensionPixelSize(LL_DIVIDER_PADDING, 0);  
    37.         mShowDividers = a.getInteger(LL_SHOW_DIVIDER, SHOW_DIVIDER_NONE);  
    38.         a.recycle();  
    39.     }  
    40.       
    41.     /** 
    42.      * 设置分隔元素,初始化宽高等 
    43.      */  
    44.     public void setDividerDrawable(Drawable divider)  
    45.     {  
    46.         if (divider == mDivider)  
    47.         {  
    48.             return;  
    49.         }  
    50.         mDivider = divider;  
    51.         if (divider != null)  
    52.         {  
    53.             mDividerWidth = divider.getIntrinsicWidth();  
    54.             mDividerHeight = divider.getIntrinsicHeight();  
    55.         } else  
    56.         {  
    57.             mDividerWidth = 0;  
    58.             mDividerHeight = 0;  
    59.         }  
    60.         setWillNotDraw(divider == null);  
    61.         requestLayout();  
    62.     }  



     

    这里贴出了成员变量和我们的构造方法,成员变量中包含了3个属性对应的接收变量;然后我们在构造里面对这三个属性进行了获取并赋值给相应的属性;

    这里大家肯定会困惑,我上面定义了一个整型数组,然后几个变量为数组下标,最后利用这个数组和下标在构造里面获取了值。是不是要问,你为什么这么写,你咋知道的?

    嗯,这样,大家随便下载我之前包含自定义属性的文章,或者你自己写的:

    这里我拿了Android BitmapShader 实战 实现圆形、圆角图片这个例子中的源代码,大家就不用下载了,看看我下面就明白了,我在这里例子中自定义了两个属性:type和border_radius,看看我们的R.java里面生成了什么样的代码:

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.  public static final int border_radius=0x7f010001;  
    2.  public static final int type=0x7f010000;  
    3.   
    4.   public static final int[] RoundImageViewByShader = {  
    5.           0x7f010000, 0x7f010001  
    6.       };  
    7. public static final int RoundImageViewByShader_type = 0;  
    8. public static final int RoundImageViewByShader_border_radius = 1;  


    看见木有,整型数组,下标;我们的android.R.attr.xxx对应于上面的常量。是不是和我们上例定义的一模一样~~

     

    对,自定义属性怎么获取的,你照着模仿就是,无非现在的属性是android.R.attr.xxx而不是你自定义的,本质没区别。

    好了,现在大家应该知道怎么获取高版本的属性了~~

    2、onMeasure

    获取到分隔元素以后,分隔元素肯定有宽和高,我们这里把分隔元素的宽和高转化为合适的margin

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @Override  
    2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    3. {  
    4.     //将分隔元素的宽高转化为对应的margin  
    5.     setChildrenDivider();  
    6.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
    7. }  
    8.   
    9. /** 
    10.  * 将分隔元素的宽高转化为对应的margin 
    11.  */  
    12. protected void setChildrenDivider()  
    13. {  
    14.     final int count = getChildCount();  
    15.     for (int i = 0; i < count; i++)  
    16.     {  
    17.         //遍历每个子View  
    18.         View child = getChildAt(i);  
    19.         //拿到索引  
    20.         final int index = indexOfChild(child);  
    21.         //方向  
    22.         final int orientation = getOrientation();  
    23.       
    24.         final LayoutParams params = (LayoutParams) child.getLayoutParams();  
    25.         //判断是否需要在子View左边绘制分隔  
    26.         if (hasDividerBeforeChildAt(index))  
    27.         {  
    28.             if (orientation == VERTICAL)  
    29.             {  
    30.                 //如果需要,则设置topMargin为分隔元素的高度(垂直时)  
    31.                 params.topMargin = mDividerHeight;  
    32.             } else  
    33.             {  
    34.                 //如果需要,则设置leftMargin为分隔元素的宽度(水平时)  
    35.                 params.leftMargin = mDividerWidth;  
    36.             }  
    37.         }  
    38.     }  
    39. }  
    40.   
    41. /** 
    42.  * 判断是否需要在子View左边绘制分隔 
    43.  */  
    44. public boolean hasDividerBeforeChildAt(int childIndex)  
    45. {  
    46.     if (childIndex == 0 || childIndex == getChildCount())  
    47.     {  
    48.         return false;  
    49.     }  
    50.     if ((mShowDividers & SHOW_DIVIDER_MIDDLE) != 0)  
    51.     {  
    52.         boolean hasVisibleViewBefore = false;  
    53.         for (int i = childIndex - 1; i >= 0; i--)  
    54.         {  
    55.             //当前index的前一个元素不为GONE则认为需要  
    56.             if (getChildAt(i).getVisibility() != GONE)  
    57.             {  
    58.                 hasVisibleViewBefore = true;  
    59.                 break;  
    60.             }  
    61.         }  
    62.         return hasVisibleViewBefore;  
    63.     }  
    64.     return false;  
    65. }  


    onMeasure中,将divider的宽和高,根据mShowDividers的情况,设置给了合适的View的margin;

     

    其实就是,将divider需要占据的地方,利用margin空出来,我们最后会在这个空的区域进行绘制divider,别忘了,我们的divider是个drawable。

    3、onDraw

    好了,既然已经通过margin把需要绘制的地方空出来了,那么下面就是绘制了~~~

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @Override  
    2.     protected void onDraw(Canvas canvas)  
    3.     {  
    4.   
    5.         if (mDivider != null)  
    6.         {  
    7.             if (getOrientation() == VERTICAL)  
    8.             {  
    9.                 //绘制垂直方向的divider  
    10.                 drawDividersVertical(canvas);  
    11.             } else  
    12.             {  
    13.                 //绘制水平方向的divider  
    14.                 drawDividersHorizontal(canvas);  
    15.             }  
    16.         }  
    17.         super.onDraw(canvas);  
    18.     }  
    19.   
    20.     /** 
    21.      * 绘制水平方向的divider 
    22.      * @param canvas 
    23.      */  
    24.     private void drawDividersHorizontal(Canvas canvas)  
    25.     {  
    26.         final int count = getChildCount();  
    27.         //遍历所有的子View  
    28.         for (int i = 0; i < count; i++)  
    29.         {  
    30.             final View child = getChildAt(i);  
    31.   
    32.             if (child != null && child.getVisibility() != GONE)  
    33.             {  
    34.                 //如果需要绘制divider  
    35.                 if (hasDividerBeforeChildAt(i))  
    36.                 {  
    37.                     final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child  
    38.                             .getLayoutParams();  
    39.                     //得到开始的位置,getLeft为当前View的左侧,而左侧有margin,所以之差为divider绘制的开始区域  
    40.                     final int left = child.getLeft() - lp.leftMargin/* 
    41.                                                                      * - 
    42.                                                                      * mDividerWidth 
    43.                                                                      */;  
    44.                     //绘制divider  
    45.                     drawVerticalDivider(canvas, left);  
    46.                 }  
    47.             }  
    48.         }  
    49.     }  
    50.       
    51.     /** 
    52.      * 绘制divider,根据left,水平方向绘制 
    53.      * @param canvas 
    54.      * @param left 
    55.      */  
    56.     public void drawVerticalDivider(Canvas canvas, int left)  
    57.     {  
    58.         //设置divider的范围  
    59.         mDivider.setBounds(left, getPaddingTop() + mDividerPadding, left  
    60.                 + mDividerWidth, getHeight() - getPaddingBottom()  
    61.                 - mDividerPadding);  
    62.         //绘制  
    63.         mDivider.draw(canvas);  
    64.     }  


    为了代码的简短以及帮助大家的理解,这里没有贴出垂直方向的,水平方向的整个流程是完整的 。后面会贴出来垂直方向的绘制代码。

     

    其实也比较简单,在onDraw里面判断方向,这里以水平为例:遍历所有的子View,如果发现需要在其前绘制divider的,则算出divider的开始的位置(child.getLeft() - lp.leftMargin),然后调用drawVerticalDivider(),设置divider范围,紧接着绘制出来。

    垂直方向同理,就不赘述了,贴上代码:

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private void drawDividersVertical(Canvas canvas)  
    2. {  
    3.     final int count = getChildCount();  
    4.     for (int i = 0; i < count; i++)  
    5.     {  
    6.         final View child = getChildAt(i);  
    7.   
    8.         if (child != null && child.getVisibility() != GONE)  
    9.         {  
    10.             if (hasDividerBeforeChildAt(i))  
    11.             {  
    12.                 final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child  
    13.                         .getLayoutParams();  
    14.                 final int top = child.getTop() - lp.topMargin/* 
    15.                                                              * - 
    16.                                                              * mDividerHeight 
    17.                                                              */;  
    18.                 drawHorizontalDivider(canvas, top);  
    19.             }  
    20.         }  
    21.     }  
    22. }  
    23. private void drawHorizontalDivider(Canvas canvas, int top)  
    24. {  
    25.     mDivider.setBounds(getPaddingLeft() + mDividerPadding, top, getWidth()  
    26.             - getPaddingRight() - mDividerPadding, top + mDividerHeight);  
    27.     mDivider.draw(canvas);  
    28. }  


    代码说完了,下面干嘛呢?当然是测试了~~不测试怎么知道结果~~

     

    4、测试

    首先我们把布局文件中包含Button的Linelayout换成我们的com.zhy.view.IcsLinearLayout

    在3.0以下机子上运行:

     

    [html] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     android:layout_width="match_parent"  
    3.     android:layout_height="wrap_content"  
    4.     android:padding="20dp"  
    5.     android:layout_margin="10dp"  
    6.     android:background="#22444444"  
    7.     android:orientation="vertical" >  
    8.   
    9.     <TextView  
    10.         android:layout_width="match_parent"  
    11.         android:layout_height="128dp"  
    12.         android:background="@android:color/darker_gray"  
    13.         android:gravity="center"  
    14.         android:text="application_logo" />  
    15.   
    16.     <com.zhy.view.IcsLinearLayout  
    17.         android:id="@+id/buttons_container"  
    18.         android:layout_width="match_parent"  
    19.         android:layout_height="wrap_content"  
    20.         android:layout_marginTop="10dp"  
    21.         android:divider="@drawable/divider"  
    22.         android:orientation="horizontal"  
    23.         android:showDividers="middle" >  
    24.   
    25.         <Button  
    26.             android:id="@+id/btn_first"  
    27.             android:layout_width="0dp"  
    28.             android:layout_height="wrap_content"  
    29.             android:layout_weight="1"  
    30.             android:background="#ff0000"  
    31.             android:text="button_1" />  
    32.   
    33.         <Button  
    34.             android:id="@+id/btn_second"  
    35.             android:layout_width="0dp"  
    36.             android:layout_height="wrap_content"  
    37.             android:layout_weight="1"  
    38.             android:background="#00ff00"  
    39.             android:text="button_2" />  
    40.   
    41.         <Button  
    42.             android:id="@+id/btn_third"  
    43.             android:layout_width="0dp"  
    44.             android:layout_height="wrap_content"  
    45.             android:layout_weight="1"  
    46.             android:background="#0000ff"  
    47.             android:text="button_3" />  
    48.     </com.zhy.view.IcsLinearLayout>  
    49.   
    50. </LinearLayout>  


    效果图:

     

    久违了~~我们的分隔~~可以看到在3.0以下机器完美实现~~~

    but,别高兴太早,我们这么改,3.0以上机器是什么样子呢?

    哈哈,是不是完美实现了间隔~~~

    现在可以高兴了~~~

    大家现在肯定有困惑,我擦,你在构造里面获取divider,然后在onDraw里面自己绘制了divider,大家都知道3.0以上是支持的呀,肯定也会绘制呀,你说没冲突谁信呀~~~!!!

    5、答疑

    1、为什么和3.0以上没有发生一些该有的冲突?

    嗯,是的,3.0以上是支持的,为什么我们在onDraw里面自己绘制,然后调用super.onDraw竟然没有发生什么冲突?

    原因很简单:我们看4.4LinearLayout的源码:

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @Override  
    2.     protected void onDraw(Canvas canvas) {  
    3.         if (mDivider == null) {  
    4.             return;  
    5.         }  


    其实,源码中也是在onDraw里面去绘制divider,但是如果mDivider为null,就会return。之所以没有冲突,是因为我们前面的某个操作让其mDivider成员变量为null了~~

     

    现在去LinearLayout的构造方法:

     

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public LinearLayout(Context context, AttributeSet attrs, int defStyle) {  
    2.         super(context, attrs, defStyle);  
    3.   
    4.         ...  
    5.         setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));  
    6.         ...  
    7.     }  
    8.   public void setDividerDrawable(Drawable divider) {  
    9.         if (divider == mDivider) {  
    10.             return;  
    11.         }  
    12.         mDivider = divider;  
    13.         if (divider != null) {  
    14.             mDividerWidth = divider.getIntrinsicWidth();  
    15.             mDividerHeight = divider.getIntrinsicHeight();  
    16.         } else {  
    17.             mDividerWidth = 0;  
    18.             mDividerHeight = 0;  
    19.         }  
    20.         setWillNotDraw(divider == null);  
    21.         requestLayout();  
    22.     }  


    可以看到它在其构造中调用setDividerDrawable为其mDivider赋值,关键来了~~~~我们的自定义的LinearLayout复写了这个方法,也就是说,setDividerDrawable会调用子类的方法,这个父类的setDividerDrawable根本不会调用,从而导致mDivider为null了~~~

     

    为null就对应了onDraw里面的绘制~~ok~解答完毕。

    2、这篇博客怎么想到的?你咋知道代码这么写?

    我相信这样的问题,很多人感兴趣,其实也算巧合,之前知道有divider这个属性;然后前段时间写Android 教你打造炫酷的ViewPagerIndicator 不仅仅是高仿MIUI 这篇博客的时候,特意去看了ViewPagerIndicator那个开源项目源码,发现了一个IcsLinearLayout这样的一个类,类似我们上面实现的,当然了,我做了一定的修改;于是乎,仔细研究了这了类,觉得很有必要写成博客,达到文章开头所叙述的的目的~其实大家有心的话,根据我们上述的代码,去看看LinearLayout源码中如何去绘制divider,你会发现代码基本是一样的(ps:你没发现问题1中的LinearLayout源码的setDividerDrawable和我们写的一模一样么~);

     

    好了,到此整篇文章就结束了,还是那句话:”这样的情况在Android中肯定会很多,希望可以以此进行抛砖引玉,大家遇到类似的情况,提供一定的思路。这才是这篇博客的真正目的!“ 不要偷懒花点时间去敲一敲,看一看,想一想,你会发现里面还藏着很多东西,别怕浪费时间,我研究和写这篇博客的时间绝对超出你所学习这篇博客的时间~~

     

    最后,欧来来~~

     

     

    源码点击下载

     

  • 相关阅读:
    软件工程概论第十二周学习进度
    冲刺第十天
    评价搜狗输入法
    冲刺第九天
    冲刺第八天
    冲刺第七天
    冲刺第六天
    冲刺第五天
    软件工程概论第十一周学习进度
    冲刺第四天
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/4545506.html
Copyright © 2011-2022 走看看