本篇文章已受权微信公众号 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