ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

时间:2023-01-12 19:37:45

        手机屏幕毕竟有限,当我们要显示较多数据时便不得不舍去一些次要信息。将主要信息优先显示,也使显示效果更加简洁美观。遇到类似的需求,我们使用最多的就是 ListView ,而假设每次点击一个 Item 都要跳转到下一页查看详情,查看还有一个还要返回列表又一次进入还有一条详情。使得操作繁琐体验减少。此时可隐藏和展开 Item 的 ListView 便应运而生。这不是一个新的控件。仅仅是我们灵活使用造出来的使用方法。下边我就来实现 ListView 点击 Item 展开隐藏项,包含列表单项展开、多项展开、复杂布局展开的实现。

        一、效果图

        单项展开:

        ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

        多项展开:

        ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

        复杂布局时的实现:

        ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

        二、单项展开

        直接上代码,没有难度,OneExpandActivity 中就是模拟一些数据,使用 OneExpandAdapter 适配器载入:

public class OneExpandActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_one_expand);
requestData();
}

private void requestData() {
ArrayList<HashMap<String, String>> datas = new ArrayList<HashMap<String,String>>();
for(int i = 1; i <= 10; i++){
HashMap<String, String> item = new HashMap<String, String>();
item.put("phoneType", "HTC-M" + i + "");
item.put("discount", "9");
item.put("price", (2000 + i) + "");
item.put("time", "2016020" + i);
item.put("num", (300 - i) + "");
datas.add(item);
}

ListView lvProduct = (ListView) findViewById(R.id.lv_products);
OneExpandAdapter adapter = new OneExpandAdapter(this, datas);
lvProduct.setAdapter(adapter);
}
}
        Activity 的布局文件就不看了,仅仅有一个 ListView。

我们看 OneExpandAdapter.java:

        先看 Adapter 用到的布局样式:

<?xml version="1.0" encoding="utf-8"?

>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/shape2"
android:orientation="vertical" >

<LinearLayout
android:id="@+id/layout_showArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp" >

<TextView
android:id="@+id/tv_phoneType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HTC M8"
android:textColor="#162834"
android:textSize="25sp" />

<TextView
android:id="@+id/tv_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:text="9"
android:textColor="#F75252"
android:textSize="15sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="折"
android:textColor="#F75252"
android:textSize="15sp" />

<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="100dp"
android:text="2000"
android:textColor="#F75252"
android:textSize="20sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="¥"
android:textColor="#767171"
android:textSize="15sp" />
</LinearLayout>

<RelativeLayout
android:id="@+id/layout_hideArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp" >

<TextView
android:id="@+id/tv_timeNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/tv_time"
android:text="活动截止时间:"
android:textColor="#162834"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/tv_timeNote"
android:text="2016.02.10"
android:textColor="#F09BED"
android:textSize="15sp" />

<TextView
android:id="@+id/tv_numNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_timeNote"
android:layout_marginTop="10dp"
android:text="库存剩余:"
android:textColor="#162834"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/tv_numNote"
android:layout_below="@id/tv_time"
android:layout_toRightOf="@id/tv_numNote"
android:gravity="bottom"
android:text="888"
android:textColor="#F09BED"
android:textSize="15sp" />

<ImageView
android:id="@+id/img_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentRight="true"
android:src="@drawable/red_packet" />

<Button
android:id="@+id/btn_buy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_num"
android:padding="4dp"
android:textSize="24sp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="@drawable/select_btn"
android:text="马上抢购" />
</RelativeLayout>
</LinearLayout>

        大概就是这个样子:

        ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

        看 Adapter 的代码(整体都还是常规使用方法,本次用到的逻辑都有凝视):

/**
* 点击item展开隐藏部分,再次点击收起
* 仅仅可展开一条记录
*
* @author WangJ
* @date 2016.01.31
*/
public class OneExpandAdapter extends BaseAdapter {
private Context context;
private ArrayList<HashMap<String, String>> list;
private int currentItem = -1; //用于记录点击的 Item 的 position。是控制 item 展开的核心

public OneExpandAdapter(Context context,
ArrayList<HashMap<String, String>> list) {
super();
this.context = context;
this.list = list;
}

@Override
public int getCount() {
return list.size();
}

@Override
public Object getItem(int position) {
return list.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(
R.layout.item_2, parent, false);
holder = new ViewHolder();
holder.showArea = (LinearLayout) convertView.findViewById(R.id.layout_showArea);
holder.tvPhoneType = (TextView) convertView
.findViewById(R.id.tv_phoneType);
holder.tvDiscount = (TextView) convertView
.findViewById(R.id.tv_discount);
holder.tvPrice = (TextView) convertView
.findViewById(R.id.tv_price);
holder.tvTime = (TextView) convertView
.findViewById(R.id.tv_time);
holder.tvNum = (TextView) convertView
.findViewById(R.id.tv_num);
holder.btnBuy = (Button) convertView
.findViewById(R.id.btn_buy);
holder.hideArea = (RelativeLayout) convertView.findViewById(R.id.layout_hideArea);

convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}

HashMap<String, String> item = list.get(position);

// 注意:我们在此给响应点击事件的区域(我的样例里是 showArea 的线性布局)加入Tag。为了记录点击的 position。我们正好用 position 设置 Tag
holder.showArea.setTag(position);

holder.tvPhoneType.setText(item.get("phoneType"));
holder.tvDiscount.setText(item.get("discount"));
holder.tvPrice.setText(item.get("price"));
holder.tvTime.setText(item.get("time"));
holder.tvNum.setText(item.get("num"));

//依据 currentItem 记录的点击位置来设置"相应Item"的可见性(在list依次载入列表数据时,每载入一个时都看一下是不是需改变可见性的那一条)
if (currentItem == position) {
holder.hideArea.setVisibility(View.VISIBLE);
} else {
holder.hideArea.setVisibility(View.GONE);
}

holder.showArea.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View view) {
//用 currentItem 记录点击位置
int tag = (Integer) view.getTag();
if (tag == currentItem) { //再次点击
currentItem = -1; //给 currentItem 一个无效值
} else {
currentItem = tag;
}
//通知adapter数据改变须要又一次载入
notifyDataSetChanged(); //必须有的一步
}
});
holder.tvPhoneType.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View view) {
Toast.makeText(context, "hehe", Toast.LENGTH_SHORT).show();
}
});
return convertView;
}

private static class ViewHolder {
private LinearLayout showArea;

private TextView tvPhoneType;
private TextView tvDiscount;
private TextView tvPrice;
private TextView tvTime;
private TextView tvNum;
private Button btnBuy;

private RelativeLayout hideArea;
}
}
        我们仅仅是 给特定组件 setTag(int position),依据点击的 View 记录下这个 position,在 getView() 中推断当前载入 View 是 position 是不是和记录的 position 相等来进行特定组件的可见性设置就可以。

        这里我们须要明确:我们平时对 ListView 做的最多的操作就是 setOnItemClickListener。这个操作一般都是在 Activity 中进行的,此时响应区域是 Item 总体。无论你点击 Item 的哪个角落都会响应。而对于每一个 Item 中子控件的事件监听(差别于整个Item,比方说 Item 中的button、输入框等等)都是在适配器类中加入,此时仅仅有点击加入监听的子控件区域才会响应,相当于每一个 Item 中的该控件都加入了监听。OnClick 的响应优先级:子控件(元控件)> 父布局(可是不像 onTouch 事件有 Boolean 返回值那样,OnClick 事件是没有返回值的。即是“阻断式式响应”,不会再响应它所归属的上层控件)。

        搞定!

是不是比你想象中的简单呢?以下我们继续看多项 Item 展开咋实现。

        三、多项展开

        事实上和单项展开非常像,仅仅是在就点击位置和 getView() 中载入时决定可见性的推断有点小差别而已。所以 Activity 就不看了(使用的 adapter 改成下边这个就能够了)。adapter 用的布局还是上边那个,看 MultiExpandAdapter 代码:

/**
* 点击item展开隐藏部分,再次点击收起 可展开多条 Item
*
* @author WangJ
* @date 2016.02.01
*/
public class MultiExpandAdapter extends BaseAdapter {
private Context context;
private ArrayList<HashMap<String, String>> list;
private boolean[] showControl; // 用一个布尔数组记录list中每一个item是否要展开

public MultiExpandAdapter(Context context,
ArrayList<HashMap<String, String>> list) {
super();
this.context = context;
this.list = list;
showControl = new boolean[list.size()]; // 构造器中初始化布尔数组
}

/**
* 省略别的 @Override 方法
*/

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_2,
parent, false);
/**
* 省略 findView 方法
*/
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}

final HashMap<String, String> item = list.get(position);

// 注意:我们在此给响应点击事件的区域(我的样例里是 showArea 的线性布局)加入Tag,
// 为了记录点击的 position。我们正好用position 设置 Tag
holder.showArea.setTag(position);

holder.tvPhoneType.setText(item.get("phoneType"));
holder.tvDiscount.setText(item.get("discount"));
holder.tvPrice.setText(item.get("price"));
holder.tvTime.setText(item.get("time"));
holder.tvNum.setText(item.get("num"));

// list依次载入每一个item。载入的同一时候查看showControl控制数组中相应位置的true/false
// true显示隐藏部分
// false不显示
if (showControl[position]) {
holder.hideArea.setVisibility(View.VISIBLE);
} else {
holder.hideArea.setVisibility(View.GONE);
}

holder.showArea.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
// 依据点击位置改变控制数组中相应位置的布尔值
int tag = (Integer) view.getTag();
// 假设已经是true则改为false。反过来同理(即点击展开。再次点击收起)
if (showControl[tag]) {
showControl[tag] = false;
} else {
showControl[tag] = true;
}
//通知adapter数据改变须要又一次载入
notifyDataSetChanged(); //必须要有一步
}
});

// 对于 Item 中子控件的监听(差别于整个Item)都是在适配器类中加入,
// 不要和在Activity中给ListView加入setOnItemClickListener搞混了
holder.btnBuy.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
Toast.makeText(context, "快快下单!

剩余" + item.get("num") + "台",
Toast.LENGTH_SHORT).show();
}
});
return convertView;
}
/**
* 省略ViewHolder
*/
}

        改变不大吧?仅仅是(和上边的"单项展开"对照) 记录点击位置的方式和推断是否须要显示时的推断略微改一下就能够了。

        四、Item 布局较复杂情况下的实现

        上边两个 Adapter 中使用的布局比較简单(简单是指比較easy区分控制),假设对于一个比較麻烦一点的布局怎么办呢?比方我们几个样例:

<?

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

<!-- 明明 TableLayout 就能够作为根节点
为什么在 TableLayout 外再加一个 LinearLayout ?-->
<TableLayout
android:id="@+id/table"
style="@style/table"
android:background="@drawable/shape2"
android:layout_marginTop="0dp" >

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp" >

<TextView
style="@style/textNormal"
android:text="还款期数:" />

<TextView
android:id="@+id/tv_repayCycle"
style="@style/textNormal"
android:singleLine="true"
android:text="1" />
</TableRow>

<View style="@style/viewLine" />

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp" >

<TextView
style="@style/textNormal"
android:text="共计本息:" />

<TextView
android:id="@+id/tv_total"
style="@style/textNormal"
android:singleLine="true"
android:text="500" />
</TableRow>

<View
android:id="@+id/splitLine1"
style="@style/viewLine" />

<TableRow
android:id="@+id/rowRepayDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp" >

<TextView
style="@style/textNormal"
android:text="还款时间:" />

<TextView
android:id="@+id/tv_repayDate"
style="@style/textNormal"
android:singleLine="true"
android:text="20160121" />
</TableRow>

<View
android:id="@+id/splitLine2"
style="@style/viewLine" />

<TableRow
android:id="@+id/rowPrinciple"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp" >

<TextView
style="@style/textNormal"
android:text="剩余待还本金:" />

<TextView
android:id="@+id/tv_notRepayPrincipal"
style="@style/textNormal"
android:singleLine="true"
android:text="500" />
</TableRow>

<View
android:id="@+id/splitLine3"
style="@style/viewLine" />

<TableRow
android:id="@+id/rowInterest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp" >

<TextView
style="@style/textNormal"
android:text="剩余待还利息:" />

<TextView
android:id="@+id/tv_notRepayInterest"
style="@style/textNormal"
android:singleLine="true"
android:text="5.35" />
</TableRow>

<ImageView
android:id="@+id/img_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:src="@drawable/arrow_d" />
</TableLayout>
</LinearLayout>

        表格布局是一个总体,要隐藏哪些部分须要一一对 xxxView 设置,不是不能实现。仅仅是会减少我们的"水平",这代码看起来太低级了,怎么办?但这样 有两种方法:(1)改造布局,改成和上边类似的布局,但这工作量相对较大并且会造成其它地方的修改;(2)就是我们下边的方法(事实上仅仅是个技巧。算不得方法):

        以上代码结构如图

        ListView点击Item展开隐藏项(单项展开、多项展开、复杂布局时的展开处理)

        红色虚线框所看到的,将要隐藏的组件包装一下。打包处理。看 Adapter 代码:

/**
* 点击item展开隐藏部分,再次点击收起
* 仅仅可展开一条记录
*
* @author WangJ
*/
public class TableExpandAdapter extends BaseAdapter {
private Context context;
private ArrayList<HashMap<String, String>> list;
private int currentItem = -1; //用于记录点击的 Item 的 position

public TableExpandAdapter(Context context,
ArrayList<HashMap<String, String>> list) {
super();
this.context = context;
this.list = list;
}

@Override
public int getCount() {
return list.size();
}

@Override
public Object getItem(int position) {
return list.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(
R.layout.item_1, parent, false);
holder = new ViewHolder();
holder.table = (TableLayout) convertView.findViewById(R.id.table);
holder.tvRepayCycle = (TextView) convertView
.findViewById(R.id.tv_repayCycle);
holder.tvTotal = (TextView) convertView
.findViewById(R.id.tv_total);
holder.tvRepayDate = (TextView) convertView
.findViewById(R.id.tv_repayDate);
holder.tvNotRepayPrincipal = (TextView) convertView
.findViewById(R.id.tv_notRepayPrincipal);
holder.tvNotRepayInterest = (TextView) convertView
.findViewById(R.id.tv_notRepayInterest);
holder.imgMore = (ImageView) convertView.findViewById(R.id.img_more);

//** 把要隐藏的控件"装起来"——開始 **
holder.splitLine1 = convertView.findViewById(R.id.splitLine1);
holder.rowRepayDate = (TableRow) convertView
.findViewById(R.id.rowRepayDate);
holder.splitLine2 = convertView.findViewById(R.id.splitLine2);
holder.rowNotRepayPrincipal = (TableRow) convertView
.findViewById(R.id.rowPrinciple);
holder.splitLine3 = convertView.findViewById(R.id.splitLine3);
holder.rowNotRepayInterest = (TableRow) convertView
.findViewById(R.id.rowInterest);
holder.hideViews.add(holder.splitLine1);
holder.hideViews.add(holder.rowRepayDate);
holder.hideViews.add(holder.splitLine2);
holder.hideViews.add(holder.rowNotRepayPrincipal);
holder.hideViews.add(holder.splitLine3);
holder.hideViews.add(holder.rowNotRepayInterest);
//** 把要隐藏的控件"装起来"——结束 **

convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}

HashMap<String, String> item = list.get(position);

// 注意:我们在此给响应点击事件的区域(我的样例里是 table 布局)加入Tag,为了记录点击的 position,我们正好用 position 设置 Tag
holder.table.setTag(position);

holder.tvRepayCycle.setText(item.get("data1"));
holder.tvTotal.setText(item.get("data2"));
holder.tvRepayDate.setText(item.get("data3"));
holder.tvNotRepayPrincipal.setText(item.get("data4"));
holder.tvNotRepayInterest.setText(item.get("data5"));

//依据 currentItem 记录的点击位置设置"相应Item"的可见性
if (currentItem == position) {
setViewsVisibility(holder.hideViews, true);
holder.imgMore.setVisibility(View.GONE); //item展开时让箭头不可见
} else {
setViewsVisibility(holder.hideViews, false);
holder.imgMore.setVisibility(View.VISIBLE); //item收起时让箭头可见
}

holder.table.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View view) {
//用 currentItem 记录点击位置
int tag = (Integer) view.getTag();
if (tag == currentItem) {
currentItem = -1;
} else {
currentItem = tag;
}
//通知adapter数据改变须要又一次载入
notifyDataSetChanged();
}
});
return convertView;
}

/**
* 一次性设置一系列控件的可见性
*
* @param views
* ArrayList<View>类型,要设置可见性的控件封装
* @param visivility
* boolean类型。true表示可见,false表示不可见
*/
private void setViewsVisibility(ArrayList<View> views, boolean visivility) {
for (View view : views) {
view.setVisibility(visivility ? View.VISIBLE : View.GONE);
}
}

private static class ViewHolder {
private TableLayout table;

private TextView tvRepayCycle;
private TextView tvTotal;
private TextView tvRepayDate;
private TextView tvNotRepayPrincipal;
private TextView tvNotRepayInterest;

//** 须要隐藏控件——開始 **
private View splitLine1;
private TableRow rowRepayDate;
private View splitLine2;
private TableRow rowNotRepayPrincipal;
private View splitLine3;
private TableRow rowNotRepayInterest;
//** 须要隐藏控件——结束 **
private ArrayList<View> hideViews = new ArrayList<View>(); //用来封装隐藏的控件。使便于管理

private ImageView imgMore; //向下展开的箭头
}
}

        在 Activity 中也仅仅是简单模拟一些数据:

private void requestData() {

HashMap<String, String> accountInfo = new HashMap<String, String>();
accountInfo.put("type", "数码产品");
accountInfo.put("repayAccount", "622200******0000");

ArrayList<HashMap<String, String>> datas = new ArrayList<HashMap<String,String>>();
for(int i = 1; i <= 10; i++){
HashMap<String, String> item = new HashMap<String, String>();
item.put("data1", i + "");
item.put("data2", 510.50 + "");
item.put("data3", "20160" + i + "21");
item.put("data4", "500");
item.put("data5", "10.00");
datas.add(item);
}

showData(accountInfo);

ListView lvRepayInfo = (ListView) findViewById(R.id.lv_repayInfoList);
TableExpandAdapter adapter = new TableExpandAdapter(this, datas);
lvRepayInfo.setAdapter(adapter);
}

private void showData(HashMap<String, String> maps) {
TextView tvType = (TextView) findViewById(R.id.tv_type);
TextView tvRepayAccount = (TextView) findViewById(R.id.tv_repayAccount);

if(maps != null){
tvType.setText(maps.get("type"));
tvRepayAccount.setText(maps.get("repayAccount"));
}
}

        看完不要说“楼主。你 TMD 地在逗我!

”。楼主有言在先。这仅仅是一个技巧,算不得方法,让我的代码没有大段大段的在设置可见性看起来那么低级。事实上。那几个 holder.hideViews.add(View view) 放到 ViewHolder 类中更好一点。

        忘了说 TableLayout 外加 LinearLayout 作为根节点的问题:我们在 Adapter 的 getView() 方法中复用 convertView 时要 convertView.setTag(holder),而我们还要给 TableLayout 加入一个 int 类型的Tag。假设用 TableLayout 作为布局的根节点是无法完毕的。

        没什么技术难度,假设你没有时间,我把代码上传了,能够去下载——ListView Item点击展开隐藏项Demo下载