Android 自訂控制項 優雅實現元素間的分割線 (支援3.0以下)

來源:互聯網
上載者:User

標籤:android   divider   

轉載請標明出處: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

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){//將分隔元素的寬高轉化為對應的marginsetChildrenDivider();super.onMeasure(widthMeasureSpec, heightMeasureSpec);}/** * 將分隔元素的寬高轉化為對應的margin */protected void setChildrenDivider(){final int count = getChildCount();for (int i = 0; i < count; i++){//遍曆每個子ViewView 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把需要繪製的地方空出來了,那麼下面就是繪製了~~~

@Overrideprotected void onDraw(Canvas canvas){if (mDivider != null){if (getOrientation() == VERTICAL){//繪製垂直方向的dividerdrawDividersVertical(canvas);} else{//繪製水平方向的dividerdrawDividersHorizontal(canvas);}}super.onDraw(canvas);}/** * 繪製水平方向的divider * @param canvas */private void drawDividersHorizontal(Canvas canvas){final int count = getChildCount();//遍曆所有的子Viewfor (int i = 0; i < count; i++){final View child = getChildAt(i);if (child != null && child.getVisibility() != GONE){//如果需要繪製dividerif (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 */;//繪製dividerdrawVerticalDivider(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中肯定會很多,希望可以以此進行拋磚引玉,大家遇到類似的情況,提供一定的思路。這才是這篇部落格的真正目的!“ 不要偷懶花點時間去敲一敲,看一看,想一想,你會發現裡面還藏著很多東西,別怕浪費時間,我研究和寫這篇部落格的時間絕對超出你所學習這篇部落格的時間~~


最後,歐來來~~



源碼點擊下載



我建了一個QQ群,方便大家交流。群號:423372824

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

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

1、Android中百度地圖的使用

2、Android 自訂控制項實戰 電商活動中的刮刮卡

3、Android自訂控制項實戰  打造Android流式布局和熱門標籤

4、Android智能機器人“小慕”的實現

5、高仿QQ5.0側滑

6、高仿5.2.1主介面及訊息提醒







Android 自訂控制項 優雅實現元素間的分割線 (支援3.0以下)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.