可展开和收起的LinearLayout

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,458评论 2 59
  • 95/96年的一个晚上,一个父亲骑车带着上幼儿园的女儿回家。 之后发生了车祸,更重要的是司机肇事逃逸。让他们本可以...
    惬意符号阅读 640评论 9 14
  • 先来说说网站SEO长尾关键词有哪些优势?流量较小,数量多,甚至大多数词还没有被挖掘出来。占有全网站流量比例...
    fatgk441阅读 579评论 0 1
  • 猪场母猪产死胎现象常有发生:有疾病造成的、高温时造成的、死胎一般包括母猪分娩时发生死亡的正常胎儿(白胎)和妊娠前、...
    5bbdb32c24aa阅读 1,055评论 0 2