ExpandableLinearLayout介绍
场景介绍
开发的过程中,有时我们需要使用到这样一个功能,在展示一些商品的时候,默认只显示前几个,例如先显示前三个,这样子不会一进入页面就被商品列表占据了大部分,可以先让用户可以看到页面的大概,当用户需要查看更多的商品时,点击“展开”,就可以看到被隐藏的商品,点击“收起”,则又回到一开始的状态,只显示前几个,其他的收起来了。就拿美团外卖的订单详情页的布局作为例子,请看以下图片:
订单详情页面一开始只显示购买的前三样菜,当点击“点击展开”时,则将购买的所有外卖都展示出来,当点击“点击收起”时,则将除了前三样菜以外的都隐藏起来。其实要完成这样的功能并不难,为了方便自己和大家以后的开发,我将其封装成一个控件,取名为ExpandableLinearLayout,下面开始介绍它如何使用以及源码解析。
使用方式
一、使用默认展开和收起的底部
在布局文件中,使用ExpandableLinearLayout,代码如下:
<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为待收起时的文字提示。
在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());
}
}
效果如下:
1.支持修改默认显示的个数
可以修改默认显示的个数,比如将其修改为3,即defaultItemCount="3"
效果如下:
2.支持修改待展开和待收起状态下的文字提示
可以修改待展开状态和待收起状态下的文字提示,比如修改expandText="查看更多",hideText="收起更多"
效果如下:
3.支持修改提示文字的大小、颜色
可以修改提示文字的大小和颜色,对应的属性分别是tipTextSize,tipTextColor。比如修改tipTextSize="16sp",tipTextColor="#ff7300"
效果如下:
4.支持更换箭头的图标
可以修改箭头的图标,只需配置arrowDownImg属性,引用对应的图标,这里的箭头图标需要是向下的箭头,这样当展开和收起时,箭头会做相应的旋转动画。设置arrowDownImg="@mipmap/arrow_down_grey",修改为灰色的向下图标。
效果如下:
二、使用自定义底部
布局文件中,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