技术交流1群:46523908
技术交流2群:46505645
NoHttp 源码及Demo托管在Github欢迎大家Star:https://github.com/Y0LANDA/NoHttp
NoHttp是专门做Android网络请求与下载的框架。
近日群里的小伙伴问我三级目录、Android
三级目录、ListView
单选/GridView
单选、ListView
多选/GridView
多选怎么做,我跟他讲了下原理和思路他还是有点迷糊,后来我就动手给他写了一个Demo
,这里也把这个Demo
分享给大家。当然文中用的都是ListView
,但是我相信当你看完了本博客,什么GridView
、ListView
什么单选多选都不在话下啊哈哈。
需求是,左侧一个列表,展示一级和二级目录,点击一级的Item,展开这个Item对应的二级目录,点击二级目录中的Item,选中当前点击二级目录的Item,反选其它二级的Item(这就是单选);点击二级目录Item的同时要展开当前点击的二级目录Item对应的三级目录,点击三级目录的Item可以选中,而不反选其它(这就是多选)。当然只能多选当前二级目录下的三级Item。下面一个图直观的解释一下:
首先看UI怎么设计,左侧我们首先想到的就是用ExpandableListView
实现,这个View有自动展开二级的功能,而且是一个List表。右侧第三级目录是一个ListView,点击二级目录的Item时用Adapter
回调到Activity
,由Activity
去刷新第三级目录。第三级目录就是一个ListView
啦,这个很简单啦。
接着是Item的选中变色,变色有两种,一种是文字变色,一种是背景变色,我们这里两个都做,那么怎么在选中和点击的时候设置字体颜色和View
背景色呢,很多想到了用Java代码动态的设置,我不得不说这样做真是很愚蠢,而且不一定能实现控制。所以我们想到了drawable
的selector
,没错就是这个货,当我们设置view.setSelected(true)
时它会自动显示selector
中的’selected=true’的颜色,不论是背景还是字体颜色。
其次左侧二级目录的单选功能怎么做?二级目录的数据肯定是一个List<JavaBean>
,我们想到用JavaJavaBean来记录每个Item的选中状态,当点击其中一个Item时先把所有的Item的JavaBean的选中状态setCheck(false)
,然后设置选中当前Item的Bean的选中状态setCheck(true)
,再刷新Adapter,这样就做到了单选。我们来看看代码:
// 使所有二级目录不选中
for (OneBean oneBean : oneBeans) {// 遍历平级的所有一级目录
List<TwoBean> twoBeans = oneBean.getOperation();
for (TwoBean twoBean : twoBeans)//遍历这个一级目录下的所有二级目录Item
twoBean.setChecked(false);
}
// 选中当前点击的二级目录
getChild(position, childPosition).setChecked(true);
notifyDataSetChanged();
// 通知外部刷新第三级
if (itemClickListener != null)
itemClickListener.onClick(position, childPosition);
最后右侧的三级目录的多选怎么做,看了二级目录的单选后多选不是更简单了,点击Item的时候这个Item是选中就设置为不选中,是不选中就设置为选中。我们来看看主要代码:
// 点击Item的时候选中或者反选当前Item,这里没有让其它item反选,说明就是多选
getItem(position).toggle();
notifyDataSetChanged();
下面我带大家一步步来实现这个分析。
界面就是两个View
,左边一个ExpandableListView
,右边一个ListView
:
<?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="horizontal">
<ExpandableListView android:id="@+id/elv_main" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:scrollbars="none" />
<ListView android:id="@+id/lv_main" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:scrollbars="none" />
</LinearLayout>
根据BaseExpandableListAdapter
一级目录的JavaBean
展开时立刻要拿到二级目录的数据,系统(暂且叫系统)会调用Adatper
中下面两个方法:
@Override
public OneBean getGroup(int groupPosition) {
return oneBeans.get(groupPosition);
}
@Override
public TwoBean getChild(int groupPosition, int childPosition) {
return getGroup(groupPosition).getOperation().get(childPosition);
}
由上可见,一级目录的JavaBean
肯定要包含二级目录,所以一级目录的JavaBean
代码如下:
public class OneBean {
/** * 第一级Item显示的文字 */
private String title;
/** * 第一级标题对应的第二级内容 */
private List<TwoBean> operation;
public OneBean() {
}
public OneBean(List<TwoBean> operation, String title) {
this.operation = operation;
this.title = title;
}
public List<TwoBean> getOperation() {
return operation;
}
public void setOperation(List<TwoBean> operation) {
this.operation = operation;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
二级目录要带上选中功能,所以我们实现系统的Checkable
接口:
public class TwoBean implements Checkable {
/** * 第二级Item显示的文字。 */
private String title;
/** * 第二级是否被选中。 */
private boolean isChecked;
public TwoBean() {
}
public TwoBean(boolean checked, String title) {
isChecked = checked;
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public void setChecked(boolean checked) {
isChecked = checked;
}
@Override
public boolean isChecked() {
return isChecked;
}
@Override
public void toggle() {
isChecked = !isChecked;
}
}
三级目录和二级目录一样,带有选中功能,我们还是实现Checkable
接口:
public class ThreeBean implements Checkable {
/** * 三级的Item的文字。 */
private String title;
/** * 是否选中。 */
private boolean isChecked;
/** * 在List中的位置 */
private int index;
public ThreeBean() {
}
public ThreeBean(boolean checked, String title, int index) {
isChecked = checked;
this.title = title;
this.index = index;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
@Override
public void setChecked(boolean checked) {
isChecked = checked;
}
@Override
public boolean isChecked() {
return isChecked;
}
@Override
public void toggle() {
isChecked = !isChecked;
}
@Override
public String toString() {
return Integer.toString(index);
}
}
因为一二级目录在左侧同在一个ExpandableListView
中,所以适配器肯定是一个,但是他们的Item的布局因为二级带选中功能而不一样。
一级和二级都是显示一个标题,单纯的就是一个TextView,加上点击效果,选中效果最多是TextView
加上一个TextColor=selector
和background=selector
的问题,所以:
<?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="@color/bg_two_normal"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title_one"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:background="@drawable/selector_tv_two_bg"
android:gravity="center_vertical"
android:paddingEnd="@dimen/tv_left_20"
android:paddingLeft="@dimen/tv_left_20"
android:paddingRight="@dimen/zero"
android:paddingStart="@dimen/zero"
android:textColor="@color/selector_tv_color" />
</LinearLayout>
主要就是设置数据的地方要注意一下,我们把每个ViewHolder
看作一个Item,可以把所有的逻辑放在ViewHolder
中,在getView
时根据position设置数据,所以ViewHolder
需要提供一个setPosition
的方法来接受getView
的position
.
第一级目录的getView
和ViewHolder
:
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
OneViewHolder oneViewHolder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_list_one_title, parent, false);
oneViewHolder = new OneViewHolder(convertView);
convertView.setTag(oneViewHolder);
} else
oneViewHolder = (OneViewHolder) convertView.getTag();
oneViewHolder.setPosition(groupPosition);
return convertView;
}
/** * 一级的holder。 */
class OneViewHolder {
private TextView mTvTitle;
private OneViewHolder(View view) {
mTvTitle = (TextView) view.findViewById(R.id.tv_title_one);
}
public void setPosition(int position) {
OneBean oneBean = getGroup(position);
mTvTitle.setText(oneBean.getTitle());
}
}
二级目录的getView
和ViewHolder
,二级目录这里才是重点,怎么做单选,怎么回调都依靠它:
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
TwoViewHolder twoViewHolder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_list_two_title, parent, false);
twoViewHolder = new TwoViewHolder(convertView);
convertView.setTag(twoViewHolder);
} else
twoViewHolder = (TwoViewHolder) convertView.getTag();
twoViewHolder.setPosition(groupPosition, childPosition);
return convertView;
}
/** * 二级的holder。 */
class TwoViewHolder implements View.OnClickListener {
private TextView mTvTitle;
private int position;
private int childPosition;
private TwoViewHolder(View view) {
view.setOnClickListener(this);
mTvTitle = (TextView) view.findViewById(R.id.tv_title_two);
}
public void setPosition(int position, int childPosition) {
this.position = position;
this.childPosition = childPosition;
TwoBean twoBean = getChild(position, childPosition);
mTvTitle.setText(twoBean.getTitle());
mTvTitle.setSelected(twoBean.isChecked());
}
@Override
public void onClick(View v) {
// 使所有二级目录不选中
for (OneBean oneBean : oneBeans) {// 遍历平级的所有一级目录
List<TwoBean> twoBeans = oneBean.getOperation();
for (TwoBean twoBean : twoBeans)//遍历这个一级目录下的所有二级目录Item
twoBean.setChecked(false);
}
// 选中当前点击的二级目录
getChild(position, childPosition).setChecked(true);
notifyDataSetChanged();
// 通知外部刷新第三级
if (itemClickListener != null)
itemClickListener.onClick(position, childPosition);
}
}
看到这里大家可以回过头看看文章最前面的分析了,怎么设置二级目录的单选。最核心的选中效果的是setPosition
中的代码mTvTitle.setSelected(twoBean.isChecked());
。
public class OneTwoAdapter extends BaseExpandableListAdapter {
private Context mContext;
/** * 二级Item点击监听 */
private OnTwoItemClickListener itemClickListener;
/** * 一二级目录的数据 */
private List<OneBean> oneBeans = new ArrayList<>();
private LayoutInflater mLayoutInflater;
public OneTwoAdapter(Context context, OnTwoItemClickListener itemClickListener) {
this.mContext = context;
mLayoutInflater = LayoutInflater.from(mContext);
this.itemClickListener = itemClickListener;
}
public void notifyDataSetChanged(List<OneBean> oneBeans) {
this.oneBeans.clear();
if (oneBeans != null)
this.oneBeans.addAll(oneBeans);
super.notifyDataSetChanged();
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
OneViewHolder oneViewHolder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_list_one_title, parent, false);
oneViewHolder = new OneViewHolder(convertView);
convertView.setTag(oneViewHolder);
} else
oneViewHolder = (OneViewHolder) convertView.getTag();
oneViewHolder.setPosition(groupPosition);
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
TwoViewHolder twoViewHolder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_list_two_title, parent, false);
twoViewHolder = new TwoViewHolder(convertView);
convertView.setTag(twoViewHolder);
} else
twoViewHolder = (TwoViewHolder) convertView.getTag();
twoViewHolder.setPosition(groupPosition, childPosition);
return convertView;
}
/** * 一级的holder。 */
class OneViewHolder {
private TextView mTvTitle;
private OneViewHolder(View view) {
mTvTitle = (TextView) view.findViewById(R.id.tv_title_one);
}
public void setPosition(int position) {
OneBean oneBean = getGroup(position);
mTvTitle.setText(oneBean.getTitle());
}
}
/** * 二级的holder。 */
class TwoViewHolder implements View.OnClickListener {
private TextView mTvTitle;
private int position;
private int childPosition;
private TwoViewHolder(View view) {
view.setOnClickListener(this);
mTvTitle = (TextView) view.findViewById(R.id.tv_title_two);
}
public void setPosition(int position, int childPosition) {
this.position = position;
this.childPosition = childPosition;
TwoBean twoBean = getChild(position, childPosition);
mTvTitle.setText(twoBean.getTitle());
mTvTitle.setSelected(twoBean.isChecked());
}
@Override
public void onClick(View v) {
// 使所有二级目录不选中
for (OneBean oneBean : oneBeans) {// 遍历平级的所有一级目录
List<TwoBean> twoBeans = oneBean.getOperation();
for (TwoBean twoBean : twoBeans)//遍历这个一级目录下的所有二级目录Item
twoBean.setChecked(false);
}
// 选中当前点击的二级目录
getChild(position, childPosition).setChecked(true);
notifyDataSetChanged();
// 通知外部刷新第三级
if (itemClickListener != null)
// 第一个参数是一级的index,第二个参数是二级的index
itemClickListener.onClick(position, childPosition);
}
}
public interface OnTwoItemClickListener {
void onClick(int groupId, int childId);
}
@Override
public int getGroupCount() {
return oneBeans.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return oneBeans.get(groupPosition).getOperation().size();
}
@Override
public OneBean getGroup(int groupPosition) {
return oneBeans.get(groupPosition);
}
@Override
public TwoBean getChild(int groupPosition, int childPosition) {
return getGroup(groupPosition).getOperation().get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return childPosition;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
}
到这里一二级目录的就做完了。我们在Activity
中给这个适配器设置一个二级目录点击监听,当二级Item被点击时就会触发这里的代码:
// 通知外部刷新第三级
if (itemClickListener != null)
// 第一个参数是一级的index,第二个参数是二级的index
itemClickListener.onClick(position, childPosition);
这样我们在外部就能收到二级目录被点击的回调了,而且通知了是哪个一级目录,哪个二级目录,当点击二级目录的时候我们去刷新二级Item对应的三级目录数据。
那么根据我们的分析三级比二级还简单,不过使用的是ListView
而已,所以我们直接上代码:
public class ThreeAdapter extends BaseAdapter {
private Context mContext;
private List<ThreeBean> mThreeBeans = new ArrayList<>();
/** * 一级id */
private int groupId = -1;
/** * 二级id */
private int chilcId = -1;
public ThreeAdapter(Context context) {
this.mContext = context;
}
public void notifyDataSetChanged(List<ThreeBean> threeBeans, int groupId, int chilcId) {
this.groupId = groupId;
this.chilcId = chilcId;
mThreeBeans.clear();
if (threeBeans != null) {
mThreeBeans.addAll(threeBeans);
}
super.notifyDataSetChanged();
}
/** * 返回第一级选中的Item的Position,当没有选中时返回-1。 * * @return Position。 */
public int getOneItemSelect() {
return groupId;
}
/** * 返回第二级选中的Item的Position,当没有选中时返回-1。 * * @return Position。 */
public int getTwoItemSelect() {
return chilcId;
}
/** * 返回第三级选中的Item集合。 * * @return {@code List<Three>}。 */
public List<ThreeBean> getThreeSelect() {
List<ThreeBean> threeBeans = new ArrayList<>();
for (ThreeBean threeBean : mThreeBeans) {
if (threeBean.isChecked())
threeBeans.add(threeBean);
}
return threeBeans;
}
@Override
public int getCount() {
return mThreeBeans.size();
}
@Override
public ThreeBean getItem(int position) {
return mThreeBeans.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ThreeViewHolder threeViewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list_three_title, parent, false);
threeViewHolder = new ThreeViewHolder(convertView);
convertView.setTag(threeViewHolder);
} else
threeViewHolder = (ThreeViewHolder) convertView.getTag();
threeViewHolder.setPosition(position);
return convertView;
}
/** * 一级的holder。 */
class ThreeViewHolder implements View.OnClickListener {
private TextView mTvTitle;
private int position;
private ThreeViewHolder(View view) {
view.setOnClickListener(this);
mTvTitle = (TextView) view.findViewById(R.id.tv_title_three);
}
/** * 设置Item的数据。 * * @param position 第几个Item。 */
public void setPosition(int position) {
this.position = position;
ThreeBean threeBean = getItem(position);
mTvTitle.setText(threeBean.getTitle());
mTvTitle.setSelected(threeBean.isChecked());
}
@Override
public void onClick(View v) {
// 点击Item的时候选中或者反选当前Item,这里没有让其它item反选,说明就是多选
getItem(position).toggle();
notifyDataSetChanged();
}
}
}
最核心的选中效果的是setPosition
中的代码mTvTitle.setSelected(twoBean.isChecked());
。点击时切换相反状态的代码是getItem(position).toggle();
,这里大家可以看一下JavaBean
中的实现,结合设置选中和selector
就实现了选中状态的切换。
我在三级的Adapter
中加了获取一二级选中index的方法,和获取三级选中的JavaBean的方法,这样我们在点击确定的时候就可以获取到选中的一二三级分别是哪个了。
说那么多还是要看Activity中怎么玩这几个Adapter
,我们来看看Activity
的代码:
public class MainActivity extends Activity {
/** * 第三级适配器 */
private ThreeAdapter threeListAdapter;
/** * 第三级数据 */
private List<ThreeBean> threeBeans;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 第三级
ListView listView = (ListView) findViewById(R.id.lv_main);
threeListAdapter = new ThreeAdapter(this);
listView.setAdapter(threeListAdapter);
ExpandableListView expandableListView = (ExpandableListView) findViewById(R.id.elv_main);
OneTwoAdapter expandCheckAdapter = new OneTwoAdapter(this, onTwoItemClickListener);
expandableListView.setAdapter(expandCheckAdapter);
// 第一二级
List<OneBean> oneBeans = new ArrayList<>();
for (int i = 0; i < 20; i++) {
List<TwoBean> twoBeans = new ArrayList<>();
for (int j = 0; j < 10; j++) {
twoBeans.add(new TwoBean(false, "第" + i + "的第" + j + "个"));
}
oneBeans.add(new OneBean(twoBeans, "第一级:" + i));
}
// 这里刷新就数据,假设是从服务器拿来的数据
expandCheckAdapter.notifyDataSetChanged(oneBeans);
}
private OneTwoAdapter.OnTwoItemClickListener onTwoItemClickListener = new OneTwoAdapter.OnTwoItemClickListener() {
@Override
public void onClick(int groupId, int childId) {
if (threeBeans == null)
threeBeans = new ArrayList<>();
threeBeans.clear();
// 这里模拟请求第三级的数据
for (int i = 0; i < 15; i++) {
threeBeans.add(new ThreeBean(false, "第" + groupId + "个的第" + childId + "的数据" + i, i));
}
threeListAdapter.notifyDataSetChanged(threeBeans, groupId, childId);
}
};
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (item.getItemId() == R.id.menu_sure) {
String message = "第一级选中的是第" + threeListAdapter.getOneItemSelect() + ",第二级选中的是第" + threeListAdapter.getTwoItemSelect();
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
// 拿到第三级选中的列表,这里可以这样拿,也可以直接从我们数据源中拿
List<ThreeBean> threeSelect = threeListAdapter.getThreeSelect();
if (threeSelect.size() > 0) {
String messageThree = "第三级选中了" + TextUtils.join(", ", threeSelect);
Toast.makeText(this, messageThree, Toast.LENGTH_LONG).show();
}
}
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
Activity
中无非就是给两个ListView
设置了Adapter
,然后加了菜单,点击的时候打印出来选中的Item的消息。
最后的最后打个广告,我做了一个Android开源网络框架,在文章的开头和末尾有连接,我其它博客也介绍了怎么用,缓存、Cookie、自定义请求等,上传文件、下载文件等,欢迎大家关注。
NoHttp 源码及Demo托管在Github欢迎大家Star:https://github.com/Y0LANDA/NoHttp