Android 自定義控件 優雅實現元素間的分割線 (支持3.0如下)

轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/42407923 ,本文出自:【張鴻洋的博客】
html

一、概述

話說,隨着Android SDK版本的升級,不少控件增長了新的屬性方便咱們的使用,好比LinearLayout中多了:divider、showDividers等,用於爲其內部元素添加分隔;可是呢,這樣的屬性在較低版本的SDK中不能被支持,那麼,咱們在開發過程當中,可能會出現這樣的需求:將這個新的特性想辦法作到儘量的向下兼容。有人說,能夠本身寫個新的控件去實現,這樣的確能夠,可是會不會太霸氣了點。難道就沒有接地氣一點的方式麼?嗯,本文就是這樣的一個目的,以一種較爲接地氣的方式,實現新的屬性的向下兼容。java

這樣的狀況在Android中確定會不少,但願能夠以此進行拋磚引玉,你們遇到相似的狀況,提供必定的思路。這纔是這篇博客的真正目的!android


二、divider相關用法

爲了保證簡介性,這裏就不討論divider有多麼多麼好用神馬的,由於不是咱們的重點。固然了這裏提供一篇divider的參考:grid-spacing-on-android (基本就是引出divider的用處,有興趣的看下,本文的demo樣子也將參考本連接)。canvas

你們先看一個效果圖:數組


若是要實現,這樣的效果圖,對於這3個Button你們會怎麼作(主要看button):微信

簡單嘛:一個水平的線性佈局,內部三個Button的weight都爲1,而後第二個Button設置leftMargin,rightMargin就能夠了。app

嗯,沒問題,假設如今我有一個需求:通過某個操做Button3隱藏,而後讓Button1和Button2按以下佈局:ide


這樣的感受是否是不錯,雖然少了一個,徹底不影響美觀;可是,若是按照上述的答案佈局

「一個水平的線性佈局,內部三個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一下。

三、自定義LinearLayout

看了標題,你們認爲又是自定義LinearLayout麼~~

嗯,繼承LinearLayout是確定的,咱們沒有辦法改變它的源碼,可是能夠經過繼承去改變一些特性。

注意下:如今的目的是兼容至3.0如下:

首先看一個3.0如下的效果圖,否則你說我騙你:


上面的佈局文件在3.0如下顯示就是這麼個樣子,徹底無視間隔。

首先考慮一個問題,對於divider、showDividers 3.0如下的LinearLayout確定無視呀,咋辦呢?

咱們實現個LinearLayout的子類,讓它認識divider和showDividers~~~重視一下這裏,這裏就是咱們向前邁進的一大步,之後遇到相似問題,都這麼幹。

一、識別高版本的屬性

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而不是你自定義的,本質沒區別。

好了,如今你們應該知道怎麼獲取高版本的屬性了~~

二、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。

三、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);
	}

代碼說完了,下面幹嗎呢?固然是測試了~~不測試怎麼知道結果~~

四、測試

首先咱們把佈局文件中包含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以上是支持的呀,確定也會繪製呀,你說沒衝突誰信呀~~~!!!

五、答疑

一、爲何和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~解答完畢。

二、這篇博客怎麼想到的?你咋知道代碼這麼寫?

我相信這樣的問題,不少人感興趣,其實也算巧合,以前知道有divider這個屬性;而後前段時間寫Android 教你打造炫酷的ViewPagerIndicator 不只僅是高仿MIUI 這篇博客的時候,特地去看了ViewPagerIndicator那個開源項目源碼,發現了一個IcsLinearLayout這樣的一個類,相似咱們上面實現的,固然了,我作了必定的修改;因而乎,仔細研究了這了類,以爲頗有必要寫成博客,達到文章開頭所敘述的的目的~其實你們有心的話,根據咱們上述的代碼,去看看LinearLayout源碼中如何去繪製divider,你會發現代碼基本是同樣的(ps:你沒發現問題1中的LinearLayout源碼的setDividerDrawable和咱們寫的如出一轍麼~);


好了,到此整篇文章就結束了,仍是那句話:」這樣的狀況在Android中確定會不少,但願能夠以此進行拋磚引玉,你們遇到相似的狀況,提供必定的思路。這纔是這篇博客的真正目的!「 不要偷懶花點時間去敲一敲,看一看,想想,你會發現裏面還藏着不少東西,別怕浪費時間,我研究和寫這篇博客的時間絕對超出你所學習這篇博客的時間~~


最後,歐來來~~



源碼點擊下載



建了一個QQ羣,方便你們交流。羣號:423372824

----------------------------------------------------------------------------------------------------------

博主部分視頻已經上線,若是你不喜歡枯燥的文本,請猛戳(初錄,期待您的支持):

一、Android中百度地圖的使用

二、Android 自定義控件實戰 電商活動中的刮刮卡

三、Android自定義控件實戰  打造Android流式佈局和熱門標籤

四、Android智能機器人「小慕」的實現

五、高仿QQ5.0側滑

六、高仿微信5.2.1主界面及消息提醒

相關文章
相關標籤/搜索