接着昨天的内容,今天继续完善应用列表,首先,应用分为系统应用和用户应用,安装位置分为手机内存和 sdcard,所以,我们在 ListView 中添加一个分类,分为系统应用和用户应用,每一个 item 显示安装的位置,最终效果如下所示:
关于项目相关文章,请访问:
项目源码地址(实时更新):https://github.com/xwdoor/MobileSafe
ApplicationInfo 对象有个 flags 属性,它有很多个状态值,我们用它和 FLAG_EXTERNAL_STORAGE、FLAG_SYSTEM 进行与操作,从而判断是否具有该状态值,代码如下:
int flag = applicationInfo.flags;
if((flag&ApplicationInfo.FLAG_EXTERNAL_STORAGE)==ApplicationInfo.FLAG_EXTERNAL_STORAGE){
//安装在sdcard中
appInfo.isSdcardApp = true;
}else {
//安装在手机内存
appInfo.isSdcardApp = false;
}
if((flag & ApplicationInfo.FLAG_SYSTEM)==ApplicationInfo.FLAG_SYSTEM){
//系统应用
appInfo.isUserApp = false;
}else {
//用户应用
appInfo.isUserApp = true;
}
然后在 AppManagerAdapter 的 getView() 方法中进行判断并赋值,代码如下:
if (appInfo.isSdcardApp) {
holder.tvLocation.setText("外置存储器");
} else {
holder.tvLocation.setText("手机内存");
}
接下来的就是今天的重点了,给 ListView 添加第二种 item 布局,将应用分为两类:系统应用和用户应用,并且用两个列表来存储,代码如下:
ArrayList<AppInfo> installedApp = getInstalledApp();
ArrayList<AppInfo> userList = new ArrayList<AppInfo>();
ArrayList<AppInfo> systemList = new ArrayList<AppInfo>();
//拆分成两个列表
for (AppInfo info : installedApp) {
if (info.isUserApp) {
userList.add(info);
} else {
systemList.add(info);
}
}
给 ListView 添加两种及以上布局,需要重写 Adapter 的两个方法:getViewTypeCount()、getItemViewType(),分别返回布局类型的数量,以及当前 item 的类型,对 AppManagerAdapter 的代码修改如下:
/** * 应用列表适配器 * * Created by XWdoor on 2016/3/22 022 11:44. * 博客:http://blog.csdn.net/xwdoor */
public class AppManagerAdapter extends BaseAdapter {
private final ArrayList<AppInfo> mUserList;
private final ArrayList<AppInfo> mSystemList;
private final Context mContext;
public AppManagerAdapter(Context ctx, ArrayList<AppInfo> userList, ArrayList<AppInfo> systemList) {
this.mContext = ctx;
this.mUserList = userList;
this.mSystemList = systemList;
}
@Override
public int getCount() {
return mUserList.size() + mSystemList.size() + 2;//增加两个标题栏
}
@Override
public AppInfo getItem(int position) {
// 遇到标题栏,直接返回null
if (position == 0 || position == mUserList.size() + 1) {
return null;
}
if (position < mUserList.size() + 1) {
return mUserList.get(position - 1);//去掉标题栏的占位
} else {
return mSystemList.get(position - mUserList.size() - 2);//需要减掉两个标题栏的占位
}
}
@Override
public long getItemId(int position) {
return position;
}
// 返回布局类型的个数, 就会缓存两种convertView
@Override
public int getViewTypeCount() {
return 2;
}
// 根据当前位置,返回相应布局类型, 必须从0开始计算
@Override
public int getItemViewType(int position) {
if (position == 0 || position == mUserList.size() + 1) {// 遇到标题栏
return 0;
} else {
return 1;
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 首先要判断当前布局类型, 系统会缓存多种convertView, 然后根据当前布局类型,返回对应的convertView
// 根据类型,加载不同布局
switch (getItemViewType(position)){
case 0://标题
HeaderHolder headerHolder = null;
if(convertView==null){
convertView = View.inflate(mContext,R.layout.item_app_manager_header,null);
headerHolder = new HeaderHolder();
headerHolder.tvHeader = (TextView) convertView.findViewById(R.id.tv_header);
convertView.setTag(headerHolder);
}else {
headerHolder = (HeaderHolder) convertView.getTag();
}
if(position == 0){
headerHolder.tvHeader.setText("用户应用(" + mUserList.size() + ")");
}else {
headerHolder.tvHeader.setText("系统应用(" + mSystemList.size() + ")");
}
break;
case 1://应用item
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(mContext, R.layout.item_app_manager_adapter, null);
holder = new ViewHolder();
holder.tvName = (TextView) convertView.findViewById(R.id.tv_name);
holder.tvLocation = (TextView) convertView.findViewById(R.id.tv_location);
holder.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
AppInfo appInfo = getItem(position);
holder.tvName.setText(appInfo.appName);
holder.ivIcon.setImageDrawable(appInfo.icon);
if (appInfo.isSdcardApp) {
holder.tvLocation.setText("外置存储器");
} else {
holder.tvLocation.setText("手机内存");
}
break;
}
return convertView;
}
static class ViewHolder {
public TextView tvName;
public ImageView ivIcon;
public TextView tvLocation;
}
static class HeaderHolder{
public TextView tvHeader;
}
}
首先是构造函数需要传入两个应用列表,getCount() 方法需要返回两个列表总数再加上两个标题栏;getItem() 方法根据情况返回,若是标题栏,则返回 null,若是具体的某个 item,需要去掉标题栏的占位;同理,在 getView() 方法中,需要根据不同的类型,加载不同的布局,这里需要说一下关于 View 的复用问题,根据 getViewTypeCount() 方法的返回值,系统会保存多种 convertView, 然后根据当前布局类型,返回对应的 convertView。最好的效果如图:
每当标题栏滑动到顶端时,都会有一个悬浮效果,表示以下 item 都是属于该标题类型,其实这只是一种障眼法,那个标题栏一直都在,只是在适当的时候修改它的显示文字即可,需要修改 AppManagerActivity 的布局文件,将 ListView 放在一个 FrameLayout 布局中,然后添加一个 TextView 覆盖在上面,同时增加一个加载数据提示进度条,代码如下:
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" >
<ListView android:id="@+id/lv_list" android:layout_width="match_parent" android:layout_height="match_parent" />
<TextView android:id="@+id/tv_header" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_blue_dark" android:padding="5dp" android:text="用户应用(0)" android:textColor="#fff" android:textSize="16sp" />
<LinearLayout android:id="@+id/ll_loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" android:orientation="vertical" >
<ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="正在加载,请稍候..." />
</LinearLayout>
</FrameLayout>
然后添加 ListView 的滚动监听,用于更改标题栏的文字显示,代码如下:
final TextView tvHeader = (TextView) findViewById(R.id.tv_header);
lvList.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// firstVisibleItem 第一个可见元素位置
// visibleItemCount 可见元素数量
// totalItemCount 元素总数
if (mUserList != null && mSystemList != null) {
if (firstVisibleItem >= mUserList.size() + 1) {//算上标题栏占位
tvHeader.setText("系统应用(" + mSystemList.size() + ")");
} else {
tvHeader.setText("用户应用(" + mUserList.size() + ")");
}
}
}
});
效果如下:
获取手机所有安装的应用是一个耗时操作,所以我们需要在线程中加载数据,为了用户体验,防止白屏出现,所以加上了一个进度条,代码如下:
@Override
protected void loadData() {
llLoading.setVisibility(View.VISIBLE);
//在线程中加载数据
new Thread() {
@Override
public void run() {
String availRom = getAvailSpace(Environment.getExternalStorageDirectory().getAbsolutePath());
String availSdcard = getAvailSpace(Environment.getDataDirectory().getAbsolutePath());
tvRomAvail.setText("内部存储可用:" + availRom);
tvSdcardAvail.setText("sdcard可用:" + availSdcard);
ArrayList<AppInfo> installedApp = getInstalledApp();
mUserList = new ArrayList<AppInfo>();
mSystemList = new ArrayList<AppInfo>();
for (AppInfo info : installedApp) {
if (info.isUserApp) {
mUserList.add(info);
} else {
mSystemList.add(info);
}
}
//更新UI数据
runOnUiThread(new Runnable() {
@Override
public void run() {
lvList.setAdapter(new AppManagerAdapter(AppManagerActivity.this, mUserList, mSystemList));
llLoading.setVisibility(View.GONE);
}
});
}
}.start();
}
一般在线程中更新 UI,我们都会使用 Handler 来完成,但是只有一两行语句,感觉大材小用了,这里我们可以使用 runOnUiThread() 方法,它是在主线程中运行的,所以可以放心使用。
今天的内容还是比较充实的,学到了两个知识点:
关于项目相关文章,请访问:
项目源码地址(实时更新):https://github.com/xwdoor/MobileSafe