可展開和收起的LinearLayout

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈
轉載請註明出處: http://blog.csdn.net/Chay_Chan/article/details/72810770java

##ExpandableLinearLayout介紹
###場景介紹
  開發的過程當中,有時咱們須要使用到這樣一個功能,在展現一些商品的時候,默認只顯示前幾個,例如先顯示前三個,這樣子不會一進入頁面就被商品列表佔據了大部分,能夠先讓用戶能夠看到頁面的大概,當用戶須要查看更多的商品時,點擊「展開」,就能夠看到被隱藏的商品,點擊「收起」,則又回到一開始的狀態,只顯示前幾個,其餘的收起來了。就拿美團外賣的訂單詳情頁的佈局做爲例子,請看如下圖片:android

  訂單詳情頁面一開始只顯示購買的前三樣菜,當點擊「點擊展開」時,則將購買的全部外賣都展現出來,當點擊「點擊收起」時,則將除了前三樣菜之外的都隱藏起來。其實要完成這樣的功能並不難,爲了方便本身和你們之後的開發,我將其封裝成一個控件,取名爲ExpandableLinearLayout,下面開始介紹它如何使用以及源碼解析。git

##使用方式
###1、使用默認展開和收起的底部
在佈局文件中,使用ExpandableLinearLayout,代碼以下:github

<com.chaychan.viewlib.ExpandableLinearLayout
        android:id="@+id/ell_product"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:orientation="vertical"
		app:useDefaultBottom="true"
        app:defaultItemCount="2"
        app:expandText="點擊展開"
        app:hideText="點擊收起"
        ></com.chaychan.viewlib.ExpandableLinearLayout>

和LinearLayout的使用方法相似,若是是靜態數據,能夠在兩個標籤中間插入子條目佈局的代碼,也能夠在java文件中使用代碼動態插入。useDefaultBottom是指是否使用默認底部(默認爲true,若是須要使用默認底部,可不寫這個屬性),若是是自定義的底部,則設置爲false,下面會介紹自定義底部的用法,defaultItemCount=「2」,設置默認顯示的個數爲2,expandText爲待展開時的文字提示,hideText爲待收起時的文字提示。web

在java文件中,根據id找到控件,動態往ExpandableLinearLayout中插入子條目並設置數據便可,代碼以下:微信

@Bind(R.id.ell_product)
ExpandableLinearLayout ellProduct;    

  @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.page_ell_default_bottom_demo);
    ButterKnife.bind(this);

    ellProduct.removeAllViews();//清除全部的子View(避免從新刷新數據時重複添加)
    //添加數據
    for (int i = 0; i < 5; i++) {
        View view = View.inflate(this, R.layout.item_product, null);
        ProductBean productBean = new ProductBean(imgUrls[i], names[i], intros[i], "12.00");
        ViewHolder viewHolder = new ViewHolder(view, productBean);
        viewHolder.refreshUI();
        ellProduct.addItem(view);//添加子條目
    }
}


 class ViewHolder {
    @Bind(R.id.iv_img)
    ImageView ivImg;
    @Bind(R.id.tv_name)
    TextView tvName;
    @Bind(R.id.tv_intro)
    TextView tvIntro;
    @Bind(R.id.tv_price)
    TextView tvPrice;

    ProductBean productBean;

    public ViewHolder(View view, ProductBean productBean) {
        ButterKnife.bind(this, view);
        this.productBean = productBean;
    }

    private void refreshUI() {
        Glide.with(EllDefaultBottomDemoActivity.this)
                .load(productBean.getImg())
                .placeholder(R.mipmap.ic_default)
                .into(ivImg);
        tvName.setText(productBean.getName());
        tvIntro.setText(productBean.getIntro());
        tvPrice.setText("¥" + productBean.getPrice());
    }
}

效果以下:app

####1.支持修改默認顯示的個數
能夠修改默認顯示的個數,好比將其修改成3,即defaultItemCount=「3」maven

效果以下:ide

####2.支持修改待展開和待收起狀態下的文字提示
能夠修改待展開狀態和待收起狀態下的文字提示,好比修改expandText=「查看更多」,hideText=「收起更多」svg

效果以下:

####3.支持修改提示文字的大小、顏色

能夠修改提示文字的大小和顏色,對應的屬性分別是tipTextSize,tipTextColor。好比修改tipTextSize=「16sp」,tipTextColor="#ff7300"

效果以下:

####4.支持更換箭頭的圖標
能夠修改箭頭的圖標,只需配置arrowDownImg屬性,引用對應的圖標,這裏的箭頭圖標須要是向下的箭頭,這樣當展開和收起時,箭頭會作相應的旋轉動畫。設置arrowDownImg="@mipmap/arrow_down_grey",修改成灰色的向下圖標。

效果以下:

###2、使用自定義底部

佈局文件中,ExpandableLinearLayout配置useDefaultBottom=「false」,聲明不使用默認底部。本身定義底部的佈局。

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >

        <!--商品列表-->
        <com.chaychan.viewlib.ExpandableLinearLayout
            android:id="@+id/ell_product"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:orientation="vertical"
            app:defaultItemCount="2"
            app:useDefaultBottom="false"
            >

        </com.chaychan.viewlib.ExpandableLinearLayout>

        <!--自定義底部-->
        <RelativeLayout...>
          
		<!--優惠、實付款-->
        <RelativeLayout...>

    </LinearLayout>

</ScrollView>

java文件中,代碼以下:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
    setContentView(R.layout.page_ell_custom_bottom_demo);
    ButterKnife.bind(this);

  ...  //插入模擬數據的代碼,和上面演示使用默認底部的代碼同樣
 
  //設置狀態改變時的回調
  ellProduct.setOnStateChangeListener(new ExpandableLinearLayout.OnStateChangeListener() {
        @Override
        public void onStateChanged(boolean isExpanded) {
            doArrowAnim(isExpanded);//根據狀態箭頭旋轉
            //根據狀態更改文字提示
            if (isExpanded) {
                //展開
                tvTip.setText("點擊收起");
            } else {
                tvTip.setText("點擊展開");
            }
        }
    });

   //爲自定義的底部設置點擊事件
   rlBottom.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ellProduct.toggle();
        }
    });

}

  // 箭頭的動畫
  private void doArrowAnim(boolean isExpand) {
    if (isExpand) {
        // 當前是展開,箭頭由下變爲上
        ObjectAnimator.ofFloat(ivArrow, "rotation", 0, 180).start();
    } else {
        // 當前是收起,箭頭由上變爲下
        ObjectAnimator.ofFloat(ivArrow, "rotation", -180, 0).start();
    }
 }

主要的代碼是爲ExpandableLinearLayout設置狀態改變的回調,rlBottom爲自定義底部的根佈局RelativeLayout,爲其設置點擊事件,當點擊的時候調用ExpandableLinearLayout的toggle()方法,當收到回調時,根據狀態旋轉箭頭以及更改文字提示。

效果以下:

到這裏,ExpandableLinearLayout的使用就介紹完畢了,接下來是對源碼進行解析。

##源碼解析
  ExpandableLinearLayout的原理其實很簡單,當使用默認的底部時,若是子條目的個數小於或者等於默認顯示的個數,則不添加底部,若是子條目的個數大於默認顯示的個數,則往最後插入一個默認的底部,一開始的時候,將ExpandableLinearLayout除了默認顯示的條目和底部不隱藏之外,其餘的子條目都進行隱藏,當點擊「展開」的時候,將被隱藏的條目設置爲顯示狀態,當點擊「收起」的時候,將默認顯示條目如下的那些條目都隱藏。

首先介紹下ExpandableLinearLayout自定義的屬性:

<declare-styleable name="ExpandableLinearLayout">
    <!--默認顯示的條目數-->
    <attr name="defaultItemCount" format="integer" />
    <!--提示文字的大小-->
    <attr name="tipTextSize" format="dimension" />
    <!--字體顏色-->
    <attr name="tipTextColor" format="color"/>
    <!--待展開的文字提示-->
    <attr name="expandText" format="string" />
    <!--待收起時的文字提示-->
    <attr name="hideText" format="string" />
    <!--向下的箭頭的圖標-->
    <attr name="arrowDownImg" format="reference" />
    <!--是否使用默認的底部-->
    <attr name="useDefaultBottom" format="boolean" />
</declare-styleable>

ExpandableLinearLayout繼承於LinearLayout

public class ExpandableLinearLayout extends LinearLayout implements View.OnClickListener {

public ExpandableLinearLayout(Context context) {
    this(context, null);
}

public ExpandableLinearLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public ExpandableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    //獲取自定義屬性的值
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandableLinearLayout);
    defaultItemCount = ta.getInt(R.styleable.ExpandableLinearLayout_defaultItemCount, 2);
    expandText = ta.getString(R.styleable.ExpandableLinearLayout_expandText);
    hideText = ta.getString(R.styleable.ExpandableLinearLayout_hideText);
    fontSize = ta.getDimension(R.styleable.ExpandableLinearLayout_tipTextSize, UIUtils.sp2px(context, 14));
    textColor = ta.getColor(R.styleable.ExpandableLinearLayout_tipTextColor, Color.parseColor("#666666"));
    arrowResId = ta.getResourceId(R.styleable.ExpandableLinearLayout_arrowDownImg, R.mipmap.arrow_down);
    useDefaultBottom = ta.getBoolean(R.styleable.ExpandableLinearLayout_useDefaultBottom, true);
    ta.recycle();

    setOrientation(VERTICAL);
}

 /**
 * 渲染完成時初始化默認底部view
 */
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    findViews();
}

/**
 * 初始化底部view
 */
private void findViews() {
    bottomView = View.inflate(getContext(), R.layout.item_ell_bottom, null);
    ivArrow = (ImageView) bottomView.findViewById(R.id.iv_arrow);

    tvTip = (TextView) bottomView.findViewById(R.id.tv_tip);
    tvTip.getPaint().setTextSize(fontSize);
    tvTip.setTextColor(textColor);
    ivArrow.setImageResource(arrowResId);

    bottomView.setOnClickListener(this);
}

添加子條目的方法,addItem(View view):

public void addItem(View view) {
    int childCount = getChildCount();
    if (!useDefaultBottom){
        //若是不使用默認底部
        addView(view);
        if (childCount > defaultItemCount){
            hide();
        }
        return;
    }

    //使用默認底部
    if (!hasBottom) {
        //若是尚未底部
        addView(view);
    } else {
        addView(view, childCount - 2);//插在底部以前
    }
    refreshUI(view);
}

  當添加條目的時候,獲取全部子條目的個數,若是是不使用默認底部的話,則只是將View添加到ExpandableLinearLayout中,當數目超過默認顯示個數時,則調用hide()方法,收起除了默認顯示條目外的其餘條目,即將它們設置爲隱藏。若是是使用默認底部,hasBottom爲是否已經有底部的標誌,若是尚未底部則是直接往ExpandableLinearLayout中順序添加,若是已經有底部,則是往底部前一個的位置添加View。調用的相關方法代碼以下:

/**
 * 收起
 */
private void hide() {
    int endIndex = useDefaultBottom ? getChildCount() - 1 : getChildCount();//若是是使用默認底部,則結束的下標是到底部以前,不然則所有子條目都隱藏
    for (int i = defaultItemCount; i < endIndex; i++) {
        //從默認顯示條目位置如下的都隱藏
        View view = getChildAt(i);
        view.setVisibility(GONE);
    }
}

/**
 * 刷新UI
 *
 * @param view
 */
private void refreshUI(View view) {
    int childCount = getChildCount();
    if (childCount > defaultItemCount) {
        if (childCount - defaultItemCount == 1) {
            //剛超過默認,判斷是否要添加底部
            justToAddBottom(childCount);
        }
        view.setVisibility(GONE);//大於默認數目的先隱藏
    }
}

/**
 * 判斷是否要添加底部
 * @param childCount
 */
private void justToAddBottom(int childCount) {
    if (childCount > defaultItemCount) {
        if (useDefaultBottom && !hasBottom) {
            //要使用默認底部,而且尚未底部
            addView(bottomView);//添加底部
            hide();
            hasBottom = true;
        }
    }
}

默認底部的點擊事件:

@Override
public void onClick(View v) {
    toggle();
}

public void toggle() {
    if (isExpand) {
        hide();
        tvTip.setText(expandText);
    } else {
        expand();
        tvTip.setText(hideText);
    }
    doArrowAnim();
    isExpand = !isExpand;

    //回調
    if (mListener != null){
        mListener.onStateChanged(isExpand);
    }
}

點擊的時候調用toggle()會根據當前狀態,進行展開或收起,若是當前是展開狀態,即isExpand爲true,則調用hide()方法收起,不然,當前是收起狀態時,調用 expand( )進行展開。這裏判斷若是有設置狀態改變的監聽,若是有則調用接口的方法將狀態傳遞出去,expand( )方法的代碼以下:

/**
 * 展開
 */
private void expand() {
    for (int i = defaultItemCount; i < getChildCount(); i++) {
        //從默認顯示條目位置如下的都顯示出來
        View view = getChildAt(i);
        view.setVisibility(VISIBLE);
    }
}

到這裏爲止,ExpandableLinearLayout的源碼解析就結束了,但願能夠這個控件能夠幫助到你們。

導入方式

在項目根目錄下的build.gradle中的allprojects{}中,添加jitpack倉庫地址,以下:

allprojects {
    repositories {
        jcenter()
        maven { url 'https://jitpack.io' }//添加jitpack倉庫地址
    }
}

打開app的module中的build.gradle,在dependencies{}中,添加依賴,以下:

dependencies {
       compile 'com.github.chaychan:ExpandableLinearLayout:1.0.0'
}

源碼github地址:https://github.com/chaychan/ExpandableLinearLayout

同時也收錄在PowfulViewLibrary中,若是想要在PowfulViewLibrary也有這個控件,更新下PowfulViewLibrary的版本。如下版本爲目前最新:

compile ‘com.github.chaychan:PowerfulViewLibrary:1.1.6’

PowerfulViewLibrary源碼地址: https://github.com/chaychan/PowerfulViewLibrary