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属性:
布局代码如下:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="20dp"
- android:layout_margin="10dp"
- android:background="#22444444"
- android:orientation="vertical" >
- <TextView
- android:layout_width="match_parent"
- android:layout_height="128dp"
- android:background="@android:color/darker_gray"
- android:gravity="center"
- android:text="application_logo" />
- <LinearLayout
- android:id="@+id/buttons_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:divider="@drawable/divider"
- android:orientation="horizontal"
- android:showDividers="middle" >
- <Button
- android:id="@+id/btn_first"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#ff0000"
- android:text="button_1" />
- <Button
- android:id="@+id/btn_second"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#00ff00"
- android:text="button_2" />
- <Button
- android:id="@+id/btn_third"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#0000ff"
- android:text="button_3" />
- </LinearLayout>
- </LinearLayout>
其实核心就是放置Button的LinearLayout设置了 android:divider="@drawable/divider"和 android:showDividers="middle" ;
当然了,有人会说,我就是任性,我就用margin来实现,消失的时候,我显示去控制button的rightMargin为0也可以。嗯,是的,你不嫌麻烦的确没问题。那么现在问题又来了,我现在要求每个Button间的间隔是蓝色的,你怎么办?注意:我们这里的divider的值设置的是一个drawable噢~~没辙了吧。
本例的drawable(divider.xml):
- <?xml version="1.0" encoding="utf-8"?>
- <shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle" >
- <size android:width="15dp" />
- <solid android:color="@android:color/transparent" />
- </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、识别高版本的属性
- public class IcsLinearLayout extends LinearLayout
- {
- private static final int[] LL = new int[]
- { //
- android.R.attr.divider,//
- android.R.attr.showDividers,//
- android.R.attr.dividerPadding //
- };
- private static final int LL_DIVIDER = 0;
- private static final int LL_SHOW_DIVIDER = 1;
- private static final int LL_DIVIDER_PADDING = 2;
- /**
- * android:dividers
- */
- private Drawable mDivider;
- /**
- * 对应:android:showDividers
- */
- private int mShowDividers;
- /**
- * 对应:android:dividerPadding
- */
- private int mDividerPadding;
- private int mDividerWidth;
- private int mDividerHeight;
- public IcsLinearLayout(Context context, AttributeSet attrs)
- {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, LL);
- setDividerDrawable(a.getDrawable(IcsLinearLayout.LL_DIVIDER));
- mDividerPadding = a.getDimensionPixelSize(LL_DIVIDER_PADDING, 0);
- mShowDividers = a.getInteger(LL_SHOW_DIVIDER, SHOW_DIVIDER_NONE);
- a.recycle();
- }
- /**
- * 设置分隔元素,初始化宽高等
- */
- public void setDividerDrawable(Drawable divider)
- {
- if (divider == mDivider)
- {
- return;
- }
- mDivider = divider;
- if (divider != null)
- {
- mDividerWidth = divider.getIntrinsicWidth();
- mDividerHeight = divider.getIntrinsicHeight();
- } else
- {
- mDividerWidth = 0;
- mDividerHeight = 0;
- }
- setWillNotDraw(divider == null);
- requestLayout();
- }
这里贴出了成员变量和我们的构造方法,成员变量中包含了3个属性对应的接收变量;然后我们在构造里面对这三个属性进行了获取并赋值给相应的属性;
这里大家肯定会困惑,我上面定义了一个整型数组,然后几个变量为数组下标,最后利用这个数组和下标在构造里面获取了值。是不是要问,你为什么这么写,你咋知道的?
嗯,这样,大家随便下载我之前包含自定义属性的文章,或者你自己写的:
这里我拿了Android BitmapShader 实战 实现圆形、圆角图片这个例子中的源代码,大家就不用下载了,看看我下面就明白了,我在这里例子中自定义了两个属性:type和border_radius,看看我们的R.java里面生成了什么样的代码:
- public static final int border_radius=0x7f010001;
- public static final int type=0x7f010000;
- public static final int[] RoundImageViewByShader = {
- 0x7f010000, 0x7f010001
- };
- public static final int RoundImageViewByShader_type = 0;
- public static final int RoundImageViewByShader_border_radius = 1;
看见木有,整型数组,下标;我们的android.R.attr.xxx对应于上面的常量。是不是和我们上例定义的一模一样~~
对,自定义属性怎么获取的,你照着模仿就是,无非现在的属性是android.R.attr.xxx而不是你自定义的,本质没区别。
好了,现在大家应该知道怎么获取高版本的属性了~~
2、onMeasure
获取到分隔元素以后,分隔元素肯定有宽和高,我们这里把分隔元素的宽和高转化为合适的margin
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- //将分隔元素的宽高转化为对应的margin
- setChildrenDivider();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- /**
- * 将分隔元素的宽高转化为对应的margin
- */
- protected void setChildrenDivider()
- {
- final int count = getChildCount();
- for (int i = 0; i < count; i++)
- {
- //遍历每个子View
- View child = getChildAt(i);
- //拿到索引
- final int index = indexOfChild(child);
- //方向
- final int orientation = getOrientation();
- final LayoutParams params = (LayoutParams) child.getLayoutParams();
- //判断是否需要在子View左边绘制分隔
- if (hasDividerBeforeChildAt(index))
- {
- if (orientation == VERTICAL)
- {
- //如果需要,则设置topMargin为分隔元素的高度(垂直时)
- params.topMargin = mDividerHeight;
- } else
- {
- //如果需要,则设置leftMargin为分隔元素的宽度(水平时)
- params.leftMargin = mDividerWidth;
- }
- }
- }
- }
- /**
- * 判断是否需要在子View左边绘制分隔
- */
- public boolean hasDividerBeforeChildAt(int childIndex)
- {
- if (childIndex == 0 || childIndex == getChildCount())
- {
- return false;
- }
- if ((mShowDividers & SHOW_DIVIDER_MIDDLE) != 0)
- {
- boolean hasVisibleViewBefore = false;
- for (int i = childIndex - 1; i >= 0; i--)
- {
- //当前index的前一个元素不为GONE则认为需要
- if (getChildAt(i).getVisibility() != GONE)
- {
- hasVisibleViewBefore = true;
- break;
- }
- }
- return hasVisibleViewBefore;
- }
- return false;
- }
onMeasure中,将divider的宽和高,根据mShowDividers的情况,设置给了合适的View的margin;
其实就是,将divider需要占据的地方,利用margin空出来,我们最后会在这个空的区域进行绘制divider,别忘了,我们的divider是个drawable。
3、onDraw
好了,既然已经通过margin把需要绘制的地方空出来了,那么下面就是绘制了~~~
- @Override
- protected void onDraw(Canvas canvas)
- {
- if (mDivider != null)
- {
- if (getOrientation() == VERTICAL)
- {
- //绘制垂直方向的divider
- drawDividersVertical(canvas);
- } else
- {
- //绘制水平方向的divider
- drawDividersHorizontal(canvas);
- }
- }
- super.onDraw(canvas);
- }
- /**
- * 绘制水平方向的divider
- * @param canvas
- */
- private void drawDividersHorizontal(Canvas canvas)
- {
- final int count = getChildCount();
- //遍历所有的子View
- for (int i = 0; i < count; i++)
- {
- final View child = getChildAt(i);
- if (child != null && child.getVisibility() != GONE)
- {
- //如果需要绘制divider
- if (hasDividerBeforeChildAt(i))
- {
- final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child
- .getLayoutParams();
- //得到开始的位置,getLeft为当前View的左侧,而左侧有margin,所以之差为divider绘制的开始区域
- final int left = child.getLeft() - lp.leftMargin/*
- * -
- * mDividerWidth
- */;
- //绘制divider
- drawVerticalDivider(canvas, left);
- }
- }
- }
- }
- /**
- * 绘制divider,根据left,水平方向绘制
- * @param canvas
- * @param left
- */
- public void drawVerticalDivider(Canvas canvas, int left)
- {
- //设置divider的范围
- mDivider.setBounds(left, getPaddingTop() + mDividerPadding, left
- + mDividerWidth, getHeight() - getPaddingBottom()
- - mDividerPadding);
- //绘制
- mDivider.draw(canvas);
- }
为了代码的简短以及帮助大家的理解,这里没有贴出垂直方向的,水平方向的整个流程是完整的 。后面会贴出来垂直方向的绘制代码。
其实也比较简单,在onDraw里面判断方向,这里以水平为例:遍历所有的子View,如果发现需要在其前绘制divider的,则算出divider的开始的位置(child.getLeft() - lp.leftMargin),然后调用drawVerticalDivider(),设置divider范围,紧接着绘制出来。
垂直方向同理,就不赘述了,贴上代码:
- private void drawDividersVertical(Canvas canvas)
- {
- final int count = getChildCount();
- for (int i = 0; i < count; i++)
- {
- final View child = getChildAt(i);
- if (child != null && child.getVisibility() != GONE)
- {
- if (hasDividerBeforeChildAt(i))
- {
- final android.widget.LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child
- .getLayoutParams();
- final int top = child.getTop() - lp.topMargin/*
- * -
- * mDividerHeight
- */;
- drawHorizontalDivider(canvas, top);
- }
- }
- }
- }
- private void drawHorizontalDivider(Canvas canvas, int top)
- {
- mDivider.setBounds(getPaddingLeft() + mDividerPadding, top, getWidth()
- - getPaddingRight() - mDividerPadding, top + mDividerHeight);
- mDivider.draw(canvas);
- }
代码说完了,下面干嘛呢?当然是测试了~~不测试怎么知道结果~~
4、测试
首先我们把布局文件中包含Button的Linelayout换成我们的com.zhy.view.IcsLinearLayout
在3.0以下机子上运行:
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="20dp"
- android:layout_margin="10dp"
- android:background="#22444444"
- android:orientation="vertical" >
- <TextView
- android:layout_width="match_parent"
- android:layout_height="128dp"
- android:background="@android:color/darker_gray"
- android:gravity="center"
- android:text="application_logo" />
- <com.zhy.view.IcsLinearLayout
- android:id="@+id/buttons_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:divider="@drawable/divider"
- android:orientation="horizontal"
- android:showDividers="middle" >
- <Button
- android:id="@+id/btn_first"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#ff0000"
- android:text="button_1" />
- <Button
- android:id="@+id/btn_second"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#00ff00"
- android:text="button_2" />
- <Button
- android:id="@+id/btn_third"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="#0000ff"
- android:text="button_3" />
- </com.zhy.view.IcsLinearLayout>
- </LinearLayout>
效果图:
久违了~~我们的分隔~~可以看到在3.0以下机器完美实现~~~
but,别高兴太早,我们这么改,3.0以上机器是什么样子呢?
哈哈,是不是完美实现了间隔~~~
现在可以高兴了~~~
大家现在肯定有困惑,我擦,你在构造里面获取divider,然后在onDraw里面自己绘制了divider,大家都知道3.0以上是支持的呀,肯定也会绘制呀,你说没冲突谁信呀~~~!!!
5、答疑
1、为什么和3.0以上没有发生一些该有的冲突?
嗯,是的,3.0以上是支持的,为什么我们在onDraw里面自己绘制,然后调用super.onDraw竟然没有发生什么冲突?
原因很简单:我们看4.4LinearLayout的源码:
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDivider == null) {
- return;
- }
其实,源码中也是在onDraw里面去绘制divider,但是如果mDivider为null,就会return。之所以没有冲突,是因为我们前面的某个操作让其mDivider成员变量为null了~~
现在去LinearLayout的构造方法:
- public LinearLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- ...
- setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));
- ...
- }
- public void setDividerDrawable(Drawable divider) {
- if (divider == mDivider) {
- return;
- }
- mDivider = divider;
- if (divider != null) {
- mDividerWidth = divider.getIntrinsicWidth();
- mDividerHeight = divider.getIntrinsicHeight();
- } else {
- mDividerWidth = 0;
- mDividerHeight = 0;
- }
- setWillNotDraw(divider == null);
- requestLayout();
- }
可以看到它在其构造中调用setDividerDrawable为其mDivider赋值,关键来了~~~~我们的自定义的LinearLayout复写了这个方法,也就是说,setDividerDrawable会调用子类的方法,这个父类的setDividerDrawable根本不会调用,从而导致mDivider为null了~~~
为null就对应了onDraw里面的绘制~~ok~解答完毕。
2、这篇博客怎么想到的?你咋知道代码这么写?
我相信这样的问题,很多人感兴趣,其实也算巧合,之前知道有divider这个属性;然后前段时间写Android 教你打造炫酷的ViewPagerIndicator 不仅仅是高仿MIUI 这篇博客的时候,特意去看了ViewPagerIndicator那个开源项目源码,发现了一个IcsLinearLayout这样的一个类,类似我们上面实现的,当然了,我做了一定的修改;于是乎,仔细研究了这了类,觉得很有必要写成博客,达到文章开头所叙述的的目的~其实大家有心的话,根据我们上述的代码,去看看LinearLayout源码中如何去绘制divider,你会发现代码基本是一样的(ps:你没发现问题1中的LinearLayout源码的setDividerDrawable和我们写的一模一样么~);
好了,到此整篇文章就结束了,还是那句话:”这样的情况在Android中肯定会很多,希望可以以此进行抛砖引玉,大家遇到类似的情况,提供一定的思路。这才是这篇博客的真正目的!“ 不要偷懒花点时间去敲一敲,看一看,想一想,你会发现里面还藏着很多东西,别怕浪费时间,我研究和写这篇博客的时间绝对超出你所学习这篇博客的时间~~
最后,欧来来~~
Android 自定义控件 优雅实现元素间的分割线 (支持3.0以下)的更多相关文章
- Android _优雅实现元素间的分割线 (支持3.0以下)
转:http://blog.csdn.net/lmj623565791/article/details/42407923 1.概述 话说,随着Android SDK版本的升级,很多控件增加了新的属性方 ...
- Android自定义控件 开源组件SlidingMenu的项目集成
在实际项目开发中,定制一个菜单,能让用户得到更好的用户体验,诚然菜单的样式各种各样,但是有一种菜单——滑动菜单,是被众多应用广泛使用的.关于这种滑动菜单的实现,我在前面的博文中也介绍了如何自定义去实现 ...
- 错觉-Info:视错觉与UI元素间的可能
ylbtech-错觉-Info:视错觉与UI元素间的可能 1.返回顶部 1. 视觉原理在当下红火的机械视觉中是必不可少的,那在我们日常工作的UI产品设计中又有什么可能性的呢?今天,我从“视错觉”这个角 ...
- Android自定义控件之自定义ViewGroup实现标签云
前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...
- Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)
最近一直在学习自定义控件,搜了许多大牛们Blog里分享的小教程,也上GitHub找了一些类似的控件进行学习.发现读起来都不太好懂,就想写这么一篇东西作为学习笔记吧. 一.控件介绍: 进度条在App中非 ...
- [转]Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)
最近一直在学习自定义控件,搜了许多大牛们Blog里分享的小教程,也上GitHub找了一些类似的控件进行学习.发现读起来都不太好懂,就想写这么一篇东西作为学习笔记吧. 一.控件介绍: 进度条在App中非 ...
- Android自定义控件之自定义组合控件
前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...
- Android自定义控件之自定义属性
前言: 上篇介绍了自定义控件的基本要求以及绘制的基本原理,本篇文章主要介绍如何给自定义控件自定义一些属性.本篇文章将继续以上篇文章自定义圆形百分比为例进行讲解.有关原理知识请参考Android自定义控 ...
- Android自定义控件之基本原理
前言: 在日常的Android开发中会经常和控件打交道,有时Android提供的控件未必能满足业务的需求,这个时候就需要我们实现自定义一些控件,今天先大致了解一下自定义控件的要求和实现的基本原理. 自 ...
随机推荐
- Linux系统中C&Cpp程序开发(一)
之前一直在Windows系统下进行程序的设计,近期开始学习使用Linux系统,因而打算将程序开发也转移到Linux系统下.今天先简单介绍一下该系统下的C程序开发步骤. 首先要预先安装vim和gcc工具 ...
- 关于获得本机Mac Address的方法
网络上有讲获得Mac address的方法有如下: 1. 发送ARP命令,利用返回的Mac Address缓冲区得到 2. 用NetworkInterface.GetAllNetworkInterfa ...
- SpringMVC学习简单HelloWorld实例
首先还是从一个简单的Hello World项目说起: 我机器的开发环境为: Ubuntu12.04(不同操作系统对本系列项目没有影响): 开发工具:Eclipse For JavaEE: 数据库:My ...
- 关于textbox.attributes["value"]的问题
在“修改”时,出现这个问题,后台点击修改时,应该是文本框出现一些初始值 BLL.manager bll = new BLL.manager(); Model.manager model = bll.G ...
- IIS上部署网站404错误
新装的系统上部署.net网站遇到403.404错误,可能原因记录: 1.应用程序池选择错误,一般选择4.0的 2.ASP.NET4.0应用程序池未安装(一般先安装了framework4.0,后安装ii ...
- spring 配置文件 引入外部的property文件的两种方法
spring 的配置文件 引入外部的property文件的两种方法 <!-- 引入jdbc配置文件 方法一 --> <bean id="propertyConfig ...
- c笔试题(1)
1.sizeof和strlen的区别 #include<stdio.h> #include<string.h> int main() { char a[10] = " ...
- h5connect.js 腾讯云视频点播使用指南
http://video.qcloud.com/download/docs/QVOD_Player_Web_SDK_Developer_Guide.pdf 腾讯云视频点播服务 Web播放器SDK开发指 ...
- 移动平台中 meta 标签的使用
一.meta 标签分两大部分:HTTP 标题信息(http-equiv)和页面描述信息(name). 1.http-equiv 属性的 Content-Type 值(显示字符集的设定) 说明:设定页面 ...
- 比如在vi中按ctrl+z
比如在vi中按ctrl+z有个命令可以恢复会话,我忘了,大家谁记得? 分享到: 对我有用[0] 丢个板砖[0] 引用 | 举报| 编辑 删除 管理 回复次数:8 hellwolf hellwo ...