Android項目:手機安全衛士(16)—— 複雜 ListView淺析
Android項目:手機安全衛士(16)—— 複雜 ListView1 介紹
接著昨天的內容,今天繼續完善應用列表,首先,應用分為系統應用和使用者應用,安裝位置分為手機記憶體和 sdcard,所以,我們在 ListView 中添加一個分類,分為系統應用和使用者應用,每一個 item 顯示安裝的位置,最終效果如下所示:
2 判斷應用類型和安裝位置
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("手機記憶體"); }
3 ListView 添加兩種布局
接下來的就是今天的重點了,給 ListView 添加第二種 item 布局,將應用分為兩類:系統應用和使用者應用,並且用兩個列表來儲存,代碼如下:
ArrayList installedApp = getInstalledApp(); ArrayList userList = new ArrayList(); ArrayList systemList = new ArrayList(); //拆分成兩個列表 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 mUserList; private final ArrayList mSystemList; private final Context mContext; public AppManagerAdapter(Context ctx, ArrayList userList, ArrayList 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。最好的效果
4 ListView 標題列懸浮
每當標題列滑動到頂端時,都會有一個懸浮效果,表示以下 item 都是屬於該標題類型,其實這隻是一種障眼法,那個標題列一直都在,只是在適當的時候修改它的顯示文字即可,需要修改 AppManagerActivity 的布局檔案,將 ListView 放在一個 FrameLayout 布局中,然後添加一個 TextView 覆蓋在上面,同時增加一個載入資料提示進度條,代碼如下:
<framelayout android:layout_height="match_parent" android:layout_width="match_parent"> </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() + ")"); } } } });
效果如下:
5 線上程中載入資料
擷取手機所有安裝的應用是一個耗時操作,所以我們需要線上程中載入資料,為了使用者體驗,防止白屏出現,所以加上了一個進度條,代碼如下:
@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 installedApp = getInstalledApp(); mUserList = new ArrayList(); mSystemList = new ArrayList(); 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() 方法,它是在主線程中啟動並執行,所以可以放心使用。
6 總結
今天的內容還是比較充實的,學到了兩個知識點:
ListView 多種 item 的實現線程中方便的更新 UI