一:ExpandableListView
 ExpandableListView,是可展开的列表组件的ExpandableListView的用法和ListView非常像,只是其所显示的列表项应该由ExpandableListAdapter提供,下面是它的xml属性及说明:
- childDivider:指定各组内子项列表项之间的分割条
 - childIndicator:显示在子列表项旁边的Drawable对象
 - groupIndicator:显示在组列表项旁边的Drawable对象
 
二:效果

 三:使用
 第一步:新建包,列表中显示的数据是从网络请求过来,并且插入到数据库中
 
 1.Android开发中使用Bean类最多的场景是从网络获取数据,将数据以Bean类组织,Bean类中的数据用于填充UI界面中的控件。此处使用Bean类主要是组织数据方便,便于将其中的数据填充到控件中。
2.biz是Business的缩写,实际上就是控制层,控制层的主要作用就是协调model层和view层直接的调用和转换。能够有效的避免请求直接进行数据库内容调用,而忽略了逻辑处理的部分。实际上biz就起到了一个server服务的角色,很好的沟通了上层和下层直接的转换,避免在model层进行业务处理(代码太混乱,不利于维护)
第二步:代码编写
1.Chapter.javapackage com.hanjie.expandablelistview2.bean;
import java.util.ArrayList;
import java.util.List;
public class Chapter {
private int id;private String name;
public static final String TABLE_NAME = "tb_chapter";public static final String COL_ID = "_id";public static final String COL_NAME = "name";
private List<ChapterItem> children = new ArrayList<>();
public Chapter() {}
public Chapter(int id, String name) {this.name = name;this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public List<ChapterItem> getChildren() {return children;}
public void addChild(ChapterItem child) {children.add(child);child.setPid(getId());}
public void addChild(int id, String childName) {ChapterItem chapterItem = new ChapterItem(id, childName);chapterItem.setPid(getId());children.add(chapterItem);}
public int getId() {return id;}
public void setId(int id) {this.id = id;}
}
2.ChapterItem.javapackage com.hanjie.expandablelistview2.bean;
public class ChapterItem {private int id;private String name;private int pid;
public static final String TABLE_NAME = "tb_chapter_item";public static final String COL_ID = "_id";public static final String COL_PID = "pid";public static final String COL_NAME = "name";
public ChapterItem() {}
public ChapterItem(int id, String name) {this.name = name;this.id = id;}
public int getId() {return id;}
public int getPid() {return pid;}
public void setPid(int pId) {this.pid = pId;}
public void setId(int id) {this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
}
3.ChapterLab.javapackage com.hanjie.expandablelistview2.bean;
import java.util.ArrayList;
import java.util.List;
public class ChapterLab {
public static List<Chapter> generateDatas() {
List<Chapter> chapters = new ArrayList<>();
Chapter root1 = new Chapter(1, "Android");Chapter root2 = new Chapter(2, "IOS");Chapter root3 = new Chapter(3, "Unity 3D");Chapter root4 = new Chapter(4, "Cocos2d-x");
root1.addChild(1, "PullToRefresh");root1.addChild(2, "Android 8.0通知栏解决方案");root1.addChild(4, "Android 与WebView的js交互");root1.addChild(8, "Android UiAutomator 2.0 入门实战");root1.addChild(10, "移动端音频视频入门");
root2.addChild(11, "iOS开发之LeanCloud");root2.addChild(12, "iOS开发之传感器");root2.addChild(13, "iOS开发之网络协议");root2.addChild(14, "iOS之分享集成");root2.addChild(15, "iOS之FTP上传");
root3.addChild(16, "Unity 3D 翻牌游戏开发");root3.addChild(17, "Unity 3D基础之变体Transform");root3.addChild(20, "带你开发类似Pokemon Go的AR游戏");root3.addChild(21, "Unity 3D游戏开发之脚本系统");root3.addChild(22, "Unity 3D地形编辑器");
root4.addChild(25, "Cocos2d-x游戏之七夕女神抓捕计划");root4.addChild(26, "Cocos2d-x游戏开发初体验-C++篇");root4.addChild(27, "Cocos2d-x全民俄罗斯");root4.addChild(28, "Cocos2d-x坦克大战");root4.addChild(30, "新春特辑-Cocos抢红包");
chapters.add(root1);chapters.add(root2);chapters.add(root3);chapters.add(root4);
return chapters;}
}
4.ChapterBiz.javapackage com.hanjie.expandablelistview2.biz;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.hanjie.expandablelistview2.bean.Chapter;
import com.hanjie.expandablelistview2.bean.ChapterItem;
import com.hanjie.expandablelistview2.dao.ChapterDao;
import com.hanjie.expandablelistview2.utils.HttpUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE;
/*** Created by baidu on 2018/6/10.*/
public class ChapterBiz {
private ChapterDao chapterDao = new ChapterDao();
public void loadDatas(final Context context, final CallBack callBack,final boolean useCache) {AsyncTask<Boolean, Void, List<Chapter>> asyncTask= new AsyncTask<Boolean, Void, List<Chapter>>() {
private Exception ex;
@Overrideprotected void onPostExecute(List<Chapter> chapters) {if (ex != null) {callBack.loadFailed(ex);} else {callBack.loadSuccess(chapters);}
}
@Overrideprotected List<Chapter> doInBackground(Boolean... booleans) {final List<Chapter> chapters = new ArrayList<>();
try {// 从缓存中取if (booleans[0]) {chapters.addAll(chapterDao.loadFromDb(context));}Log.d("hanjie", "loadFromDb -> " + chapters);
if (chapters.isEmpty()) {// 从网络获取final List<Chapter> chaptersFromNet = loadFromNet(context);// 缓存在数据库chapterDao.insertToDb(context, chaptersFromNet);Log.d("hanjie", "loadFromNet -> " + chaptersFromNet);chapters.addAll(chaptersFromNet);}} catch (final Exception e) {e.printStackTrace();ex = e;}return chapters;}};asyncTask.execute(useCache);}
public static interface CallBack {void loadSuccess(List<Chapter> chapterList);
void loadFailed(Exception ex);}
public List<Chapter> loadFromNet(Context context) {
// final String url = "https://www.wanandroid.com/tools/mockapi/2/mooc-expandablelistview";final String url = "https://www.imooc.com/api/expandablelistview";
String content = HttpUtils.doGet(url);final List<Chapter> chapterList = parseContent(content);// 缓存到数据库chapterDao.insertToDb(context, chapterList);
return chapterList;}
private List<Chapter> parseContent(String content) {
List<Chapter> chapters = new ArrayList<>();try {JSONObject jsonObject = new JSONObject(content);int errorCode = jsonObject.optInt("errorCode");if (errorCode == 0) {JSONArray jsonArray = jsonObject.optJSONArray("data");for (int i = 0; i < jsonArray.length(); i++) {JSONObject chapterJson = jsonArray.getJSONObject(i);int id = chapterJson.optInt("id");String name = chapterJson.optString("name");Chapter chapter = new Chapter(id, name);chapters.add(chapter);
JSONArray chapterItems = chapterJson.optJSONArray("children");for (int j = 0; j < chapterItems.length(); j++) {JSONObject chapterItemJson = chapterItems.getJSONObject(j);id = chapterItemJson.optInt("id");name = chapterItemJson.optString("name");ChapterItem chapterItem = new ChapterItem(id, name);chapter.addChild(chapterItem);}
}}
} catch (JSONException e) {e.printStackTrace();}return chapters;}
}
5.ChapterDao.javapackage com.hanjie.expandablelistview2.dao;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.hanjie.expandablelistview2.bean.Chapter;
import com.hanjie.expandablelistview2.bean.ChapterItem;
import com.hanjie.expandablelistview2.db.ChapterDbHelper;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE;
/*** Created by baidu on 2018/6/15.*/
public class ChapterDao {
public List<Chapter> loadFromDb(Context context) {ChapterDbHelper dbHelper = ChapterDbHelper.getInstance(context);SQLiteDatabase db = dbHelper.getWritableDatabase();
List<Chapter> chapterList = new ArrayList<>();Chapter chapter = null;Cursor cursor = db.rawQuery("select * from " + Chapter.TABLE_NAME, null);while (cursor.moveToNext()) {chapter = new Chapter();int id = cursor.getInt(cursor.getColumnIndex(Chapter.COL_ID));String name = cursor.getString(cursor.getColumnIndex(Chapter.COL_NAME));chapter.setId(id);chapter.setName(name);chapterList.add(chapter);}cursor.close();
ChapterItem chapterItem = null;for (Chapter tmpChapter : chapterList) {int pid = tmpChapter.getId();cursor = db.rawQuery("select * from " + ChapterItem.TABLE_NAME + " where " + ChapterItem.COL_PID + " = ? ", new String[]{pid + ""});while (cursor.moveToNext()) {chapterItem = new ChapterItem();int id = cursor.getInt(cursor.getColumnIndex(ChapterItem.COL_ID));String name = cursor.getString(cursor.getColumnIndex(ChapterItem.COL_NAME));chapterItem.setId(id);chapterItem.setName(name);chapterItem.setPid(pid);tmpChapter.addChild(chapterItem);}cursor.close();}
return chapterList;}
public void insertToDb(Context context, List<Chapter> chapters) {
if (chapters == null || chapters.isEmpty()) {return;}ChapterDbHelper dbHelper = ChapterDbHelper.getInstance(context);SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
ContentValues cv = null;for (Chapter chapter : chapters) {cv = new ContentValues();cv.put(Chapter.COL_ID, chapter.getId());cv.put(Chapter.COL_NAME, chapter.getName());db.insertWithOnConflict(Chapter.TABLE_NAME, null, cv, CONFLICT_REPLACE);
List<ChapterItem> chapterItems = chapter.getChildren();for (ChapterItem chapterItem : chapterItems) {cv = new ContentValues();cv.put(ChapterItem.COL_ID, chapterItem.getId());cv.put(ChapterItem.COL_NAME, chapterItem.getName());cv.put(ChapterItem.COL_PID, chapter.getId());db.insertWithOnConflict(ChapterItem.TABLE_NAME, null, cv, CONFLICT_REPLACE);}}db.setTransactionSuccessful();db.endTransaction();
}
}
6.ChapterDbHelper.javapackage com.hanjie.expandablelistview2.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.hanjie.expandablelistview2.bean.Chapter;
import com.hanjie.expandablelistview2.bean.ChapterItem;
public class ChapterDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "db_chapter.db";private static final int VERSION = 1;
private static ChapterDbHelper sInstance;
public ChapterDbHelper(Context context) {super(context, DB_NAME, null, VERSION);}
public static synchronized ChapterDbHelper getInstance(Context context) {if (sInstance == null) {sInstance = new ChapterDbHelper(context.getApplicationContext());}return sInstance;}
@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL("CREATE TABLE IF NOT EXISTS " + Chapter.TABLE_NAME + " ("+ Chapter.COL_ID + " INTEGER PRIMARY KEY, "+ Chapter.COL_NAME + " VARCHAR"+ ")");
db.execSQL("CREATE TABLE IF NOT EXISTS " + ChapterItem.TABLE_NAME + " ("+ ChapterItem.COL_ID + " INTEGER PRIMARY KEY, "+ ChapterItem.COL_NAME + " VARCHAR, "+ ChapterItem.COL_PID + " INTEGER"+ ")");}
@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
7.HttpUtils.javapackage com.hanjie.expandablelistview2.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/*** Http请求的工具类*/
public class HttpUtils {
private static final int TIMEOUT_IN_MILLIONS = 5000;
public interface CallBack {void onRequestComplete(String result);}
/*** 异步的Get请求** @param urlStr* @param callBack*/public static void doGetAsyn(final String urlStr, final CallBack callBack) {new Thread() {public void run() {try {String result = doGet(urlStr);if (callBack != null) {callBack.onRequestComplete(result);}} catch (Exception e) {e.printStackTrace();}
}
;}.start();}
/*** Get请求,获得返回数据** @param urlStr* @return* @throws Exception*/public static String doGet(String urlStr) {URL url = null;HttpURLConnection conn = null;InputStream is = null;ByteArrayOutputStream baos = null;try {url = new URL(urlStr);conn = (HttpURLConnection) url.openConnection();conn.setReadTimeout(TIMEOUT_IN_MILLIONS);conn.setConnectTimeout(TIMEOUT_IN_MILLIONS);conn.setRequestMethod("GET");conn.setRequestProperty("accept", "*/*");conn.setRequestProperty("connection", "Keep-Alive");if (conn.getResponseCode() == 200) {is = conn.getInputStream();baos = new ByteArrayOutputStream();int len = -1;byte[] buf = new byte[128];
while ((len = is.read(buf)) != -1) {baos.write(buf, 0, len);}baos.flush();return baos.toString();}
} catch (Exception e) {e.printStackTrace();} finally {try {if (is != null)is.close();} catch (IOException e) {}try {if (baos != null)baos.close();} catch (IOException e) {}conn.disconnect();}
return null;
}
}
8.ChapterAdapter.javapackage com.hanjie.expandablelistview2;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.hanjie.expandablelistview2.bean.Chapter;
import java.util.List;
public class ChapterAdapter extends BaseExpandableListAdapter {
private Context mContext;private List<Chapter> mDatas;private LayoutInflater mInflater;
public ChapterAdapter(Context context, List<Chapter> chapters) {mContext = context;mDatas = chapters;mInflater = LayoutInflater.from(context);}
@Overridepublic int getGroupCount() {return mDatas.size();}
@Overridepublic int getChildrenCount(int groupPosition) {return mDatas.get(groupPosition).getChildren().size();}
@Overridepublic Object getGroup(int groupPosition) {return mDatas.get(groupPosition);}
@Overridepublic Object getChild(int groupPosition, int childPosition) {return mDatas.get(groupPosition).getChildren().get(childPosition);}
@Overridepublic long getGroupId(int groupPosition) {return groupPosition;}
@Overridepublic long getChildId(int groupPosition, int childPosition) {return childPosition;}
// TODO@Overridepublic boolean hasStableIds() {return false;}
@Overridepublic View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
ParentViewHolder vh;if (convertView == null) {// 修改item height即可演示,第二个参数作用convertView = mInflater.inflate(R.layout.item_parent_chapter, parent, false);vh = new ParentViewHolder();vh.tv = convertView.findViewById(R.id.id_tv_parent);vh.iv = convertView.findViewById(R.id.id_indicator_group);convertView.setTag(vh);
} else {vh = (ParentViewHolder) convertView.getTag();}vh.tv.setText(mDatas.get(groupPosition).getName());vh.iv.setSelected(isExpanded);
return convertView;}
@Overridepublic View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
ChildViewHolder vh;if (convertView == null) {convertView = mInflater.inflate(R.layout.item_child_chapter, parent, false);vh = new ChildViewHolder();vh.tv = convertView.findViewById(R.id.id_tv_child);
convertView.setTag(vh);
} else {vh = (ChildViewHolder) convertView.getTag();}vh.tv.setText(mDatas.get(groupPosition).getChildren().get(childPosition).getName());return convertView;}
// 控制child item不可点击@Overridepublic boolean isChildSelectable(int groupPosition, int childPosition) {
return true;}
public static class ParentViewHolder {TextView tv;ImageView iv;}
public static class ChildViewHolder {TextView tv;}
}
9.MainActivity.javapackage com.hanjie.expandablelistview2;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ExpandableListView;
import androidx.appcompat.app.AppCompatActivity;
import com.hanjie.expandablelistview2.bean.Chapter;
import com.hanjie.expandablelistview2.biz.ChapterBiz;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private Button mBtnRefresh;
private ExpandableListView mExpandableListView;private ChapterAdapter mAdapter;private List<Chapter> mDatas = new ArrayList<>();
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
mExpandableListView = findViewById(R.id.id_expandable_listview);mBtnRefresh = findViewById(R.id.id_btn_refresh);
//        mDatas = ChapterLab.generateDatas();mAdapter = new ChapterAdapter(this, mDatas);mExpandableListView.setAdapter(mAdapter);
initEvents();loadDatas(true);
}
private ChapterBiz mChapterBiz = new ChapterBiz();
private void loadDatas(boolean useCache) {mChapterBiz.loadDatas(this, new ChapterBiz.CallBack() {@Overridepublic void loadSuccess(List<Chapter> chapterList) {Log.e("zhy", "loadSuccess  ");
mDatas.clear();mDatas.addAll(chapterList);mAdapter.notifyDataSetChanged();}
@Overridepublic void loadFailed(Exception ex) {ex.printStackTrace();Log.e("zhy", "loadFailed ex= " + ex.getMessage());}}, useCache);}
private void initEvents() {
mBtnRefresh.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {loadDatas(false);}});
mExpandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {@Overridepublic boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {Log.d("zhy", "onGroupClick groupPosition = " + groupPosition);return false;}});
mExpandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {@Overridepublic boolean onChildClick(ExpandableListView parent, View v,int groupPosition, int childPosition, long id) {Log.d("zhy", "onChildClick groupPosition = "+ groupPosition + " , childPosition = " + childPosition + " , id = " + id);
return false;}});
mExpandableListView.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {// 收回@Overridepublic void onGroupCollapse(int groupPosition) {Log.d("zhy", "onGroupCollapse groupPosition = " + groupPosition);
}});
mExpandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {// 展开@Overridepublic void onGroupExpand(int groupPosition) {Log.d("zhy", "onGroupExpand groupPosition = " + groupPosition);
}});
mExpandableListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {Log.d("zhy", "onItemClick position = " + position);
}});
}
}
10.activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent">
<ExpandableListViewandroid:id="@+id/id_expandable_listview"android:layout_width="match_parent"android:layout_height="match_parent"android:groupIndicator="@null"android:indicatorLeft="0dp"android:indicatorRight="56dp"tools:context="com.imooc.expandablelistview_imooc.MainActivity">
</ExpandableListView>
<Buttonandroid:id="@+id/id_btn_refresh"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="right|bottom"android:layout_margin="20dp"android:text="刷新" />
</FrameLayout>
11.item_child_chapter.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/id_tv_child"android:layout_width="match_parent"android:layout_height="40dp"android:gravity="center_vertical"android:textSize="16sp">
</TextView>
12.item_parent_chapter.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:background="#44337dd7"android:layout_height="56dp"android:orientation="horizontal">
<ImageViewandroid:id="@+id/id_indicator_group"android:layout_width="24dp"android:layout_height="24dp"android:layout_gravity="center_vertical"android:background="@drawable/indicator_group"/>
<TextViewandroid:id="@+id/id_tv_parent"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center_vertical"tools:text="Android"android:textSize="24dp"android:textStyle="bold">
</TextView>
</LinearLayout>
13.ic_launcher_foreground.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><!--<item android:drawable="@drawable/indicator_expand" android:state_expanded="true" />--><item android:drawable="@drawable/indicator_expand" android:state_selected="true" /><item android:drawable="@drawable/indicator_collapse" />
</selector>
图片可使用自己的















