手机屏幕毕竟有限,当我们要显示较多数据时便不得不舍去一些次要信息,将主要信息优先显示,也使显示效果更加简洁美观。遇到类似的需求,我们使用最多的就是 ListView ,而如果每次点击一个 Item 都要跳转到下一页查看详情,查看另一个还要返回列表重新进入另一条详情,使得操作繁琐体验降低。此时可隐藏和展开 Item 的 ListView 便应运而生,这不是一个新的控件,只是我们灵活使用造出来的用法,下边我就来实现 ListView 点击 Item 展开隐藏项,包括列表单项展开、多项展开、复杂布局展开的实现。
一、效果图
单项展开:
多项展开:
复杂布局时的实现:
二、单项展开
直接上代码,没有难度,OneExpandActivity 中就是模拟一些数据,使用 OneExpandAdapter 适配器加载:
public class OneExpandActivity extends Activity {Activity 的布局文件就不看了,只有一个 ListView。我们看 OneExpandAdapter.java:
@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);
}
}
先看 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>
大概就是这个样子:
看 Adapter 的代码(总体都还是常规用法,本次用到的逻辑都有注释):
/**我们只是 给特定组件 setTag(int position),根据点击的 View 记录下这个 position,在 getView() 中判断当前加载 View 是 position 是不是和记录的 position 相等来进行特定组件的可见性设置即可。
* 点击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;
}
}
这里我们需要明白:我们平时对 ListView 做的最多的操作就是 setOnItemClickListener,这个操作一般都是在 Activity 中进行的,此时响应区域是 Item 整体,不管你点击 Item 的哪个角落都会响应。而对于每个 Item 中子控件的事件监听(区别于整个Item,比如说 Item 中的按钮、输入框等等)都是在适配器类中添加,此时只有点击添加监听的子控件区域才会响应,相当于每个 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 中使用的布局比较简单(简单是指比较容易区分控制),如果对于一个比较麻烦一点的布局怎么办呢?比如我们几个例子:
<?xml version="1.0" encoding="utf-8"?>表格布局是一个整体,要隐藏哪些部分需要一一对 xxxView 设置,不是不能实现,只是会降低我们的"水平",这代码看起来太低级了,怎么办?但这样 有两种方法:(1)改造布局,改成和上边类似的布局,但这工作量相对较大而且会造成其他地方的改动;(2)就是我们下边的方法(其实只是个技巧,算不得方法):
<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>
以上代码结构如图
红色虚线框所示,将要隐藏的组件包装一下,打包处理,看 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下载。