Android — 長按ListView 利用上下文菜單(ActionMode) 進行批量事件處理

好久沒寫博客拉```````

最近終於稍微閒一點了```````

無聊拿手機清理短信,發現批量事件的處理還是挺管用的``````

那麼自己也來山寨一記看看效果吧`````


閒話少說,首先,我們來看下手機自帶的短信功能裏執行批量刪除時的效果:



然後  是我們自己簡單山寨的效果:

     


模擬的操作過程很簡單,但也很有代表性。

我們假定我們所處的場景爲,進入一個存放聯繫人列表的界面。


於是,首先我們定義了一個進度框,模擬提示正在從網絡上下載數據。

接着,當網絡數據成功下載到移動設備上後,將數據綁定顯示到對應的ListView之中。

然後,就是我們這篇博客提到的:長按該聯繫人列表的ListView觸發事件,

彈出使用ActionMode的上下文菜單,並讓該ListView中的列表項支持復現,實現批量操作。

最後,就是當用戶選擇了一定數量的選項後,點擊菜單中的Item進行某項批量操作後,執行對應的操作,並刷新ListView。


理清了我們想要實現的大致效果,接着我們要做的

就是整理一下思路,然後逐步的去編寫代碼,完成實現工作。let's do it !


首先,我們已經知道了自己 想要以一個聯繫人列表作爲場景。

那麼,自然我們會需要一個ListView來綁定和存放這些聯繫人數據。

於是,我們先將存放ListView以及定義該ListView的Item的細節的佈局文件搞出來,分別爲:


context_menu_action_mode.xml

<?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="vertical" >

    <ListView
        android:id="@+id/context_menu_listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

context_menu_action_mode_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin" >

    <ImageView
        android:id="@+id/user_head"
        android:layout_width="55dp"
        android:layout_height="55dp"
        android:contentDescription="@string/user_head_description"
        android:src="@drawable/headimage_default" />

    <TextView
        android:id="@+id/user_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginStart="20dp"
        android:layout_toEndOf="@id/user_head"
        android:layout_toRightOf="@id/user_head"
        android:textSize="25sp" />

    <TextView
        android:id="@+id/phone_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/user_head"
        android:layout_marginLeft="20dp"
        android:layout_marginStart="20dp"
        android:layout_toEndOf="@id/user_head"
        android:layout_toRightOf="@id/user_head" />

    <CheckBox
        android:id="@+id/contact_selected_checkbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:clickable="false"
        android:focusable="false" />

</RelativeLayout>

關於佈局的定義,並沒有什麼難點。

唯一需要注意的是,我們爲了更加友好的交互體驗,所以在用戶長按ListView進入可複選的模式後,

在每個列表的最右側添加顯示了一個CheckBox,以提示用戶是否成功選擇到了想要操作的列表項。

CheckBox只有在用戶進入複選模式後,才顯示,所以我們需要在後面注意在代碼中動態的控制其顯示情況。

並且!更需要注意的是,記得將CheckBox的clickable與focusable兩個屬性的值設置爲false!

這樣做的原因是因爲CheckBox(定義在作爲ListView的Item文件當中)自身的響應焦點及點擊事件的優先級高於ListView自身。

所以,如果忘記設置的話,焦點及響應事件將被攔截在CheckBox,無法到達ListView。


第二步,當我們定義好了ListView的相關程序之後,自然忘不了它的好基友:適配器Adapter

MyContactAdapter.java:

package com.example.android_menu_test_demo.adapter;

import java.util.ArrayList;
import com.example.android_menu_test_demo.R;
import com.example.android_menu_test_demo.domain.Contact;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

public class MyContactAdapter extends BaseAdapter {

	private Context mContext;
	private ArrayList<Contact> contacts;
	private ViewHolder mViewHolder;
	private ArrayList<Contact> selected_contacts = new ArrayList<Contact>();

	private boolean itemMultiCheckable;

	public MyContactAdapter(Context mContext, ArrayList<Contact> contacts) {
		this.mContext = mContext;
		this.contacts = contacts;
	}

	@Override
	public int getCount() {
		return contacts.size();
	}

	@Override
	public Object getItem(int position) {
		return contacts.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		if (convertView == null) {
			convertView = LayoutInflater.from(mContext).inflate(R.layout.contact_listview_item, null);

			mViewHolder = new ViewHolder();

			mViewHolder.user_head = (ImageView) convertView.findViewById(R.id.user_head);
			mViewHolder.user_name_text = (TextView) convertView.findViewById(R.id.user_name);
			mViewHolder.phone_number_text = (TextView) convertView.findViewById(R.id.phone_number);
			mViewHolder.item_seleted = (CheckBox) convertView.findViewById(R.id.contact_selected_checkbox);

			convertView.setTag(mViewHolder);
		} else {
			mViewHolder = (ViewHolder) convertView.getTag();
		}

		// ************對於控件的具體處理****************

		// 設置checkbox是否可見
		if (itemMultiCheckable) {
			mViewHolder.item_seleted.setVisibility(View.VISIBLE);
			// 如果checkbox可見,證明當前處於可多選操作情況下,則根據用戶選擇情況設置checkbox被選中狀態
			if (selected_contacts.contains(contacts.get(position))) {
				mViewHolder.item_seleted.setChecked(true);
			} else {
				mViewHolder.item_seleted.setChecked(false);
			}

		} else {
			mViewHolder.item_seleted.setVisibility(View.GONE);
		}

		// 控件賦值
		Contact contact = contacts.get(position);
		mViewHolder.user_name_text.setText(contact.getUserName());
		mViewHolder.phone_number_text.setText(contact.getPhoneNumber());

		return convertView;
	}

	public void setItemMultiCheckable(boolean flag) {
		itemMultiCheckable = flag;
	}

	public void addSelectedContact(int position) {
		selected_contacts.add(contacts.get(position));
	}

	public void cancelSeletedContact(int position) {
		selected_contacts.remove(contacts.get(position));
	}

	public void clearSeletedContacts() {
		selected_contacts = new ArrayList<Contact>();
	}

	public void deleteSeletedContacts() {
		for (Contact contact : selected_contacts) {
			contacts.remove(contact);
		}
	}

	static class ViewHolder {
		ImageView user_head;
		TextView user_name_text, phone_number_text;
		CheckBox item_seleted;
	}
}

適配器類的定義與我們開發中最常見的定義並沒有太多區別。

值得注意的的代碼,無非就是前面談到的,做好動態控制CheckBox顯示狀態的工作。

另外,我們在適配器的定義中,爲了讓listview要顯示的數據,更便於裝載和傳遞,

通常會定義封裝數據的實體類,正如上面的Contact類,不過這個太簡單,就沒貼代碼的必要了。


接下來,就是我們想要實現的功能的重點了,

我們說到希望通過ListView的長點擊事件,來觸發一個上下文菜單來進行事件處理。

在Android 3.0之後添加的ActionMode相對於之前的普通上下文菜單,

顯然更適合對於批量事件的處理,有着更好的交互體驗。


所以說,既然將要使用到上下文 菜單,那麼,廢話少說。

先定義一個我們需要的簡單的菜單文件:

multi_acitonmode_menu.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/menu_cancle"
        android:showAsAction="always"
        android:title="@string/item_cancle"/>
    
        <item
        android:id="@+id/menu_delete"
        android:showAsAction="always"
        android:title="@string/item_delete"/>

</menu>

緊接着,一切準備 工作我們都已經基本就緒,

那麼接下來要做的,自然就是Activity的代碼編寫工作了。

ContextMenuActionModeActivity.java

package com.example.android_menu_test_demo;

import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AbsListView.MultiChoiceModeListener;
import java.util.ArrayList;

import com.example.android_menu_test_demo.adapter.MyContactAdapter;
import com.example.android_menu_test_demo.domain.Contact;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ContextMenuActionModeActivity extends Activity {

	private ListView contact_list_view;
	private ProgressDialog mDialog;

	private MyContactAdapter mAdpater;
	private MultiModeCallback mCallback;

	// 模擬數據
	private ArrayList<Contact> contacts;
	private String[] userNames = new String[] { "Jack", "Rose", "Matt", "Adam", "Xtina", "Blake", "Tupac", "Biggie",
			"T.I", "Eminem" };
	private String[] phoneNumbers = new String[] { "138-0000-0001", "138-0000-0002", "138-0000-0003", "138-0000-0004",
			"138-0000-0005", "138-0000-0006", "138-0000-0007", "138-0000-0008", "138-0000-0009", "138-0000-0010" };

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.context_menu_action_mode);
		
		initView();

		new ContactsDownloadTask().execute();

	}

	private void initView() {
		contact_list_view = (ListView) this.findViewById(R.id.context_menu_listView);

		mDialog = new ProgressDialog(this);

		mDialog.setTitle("提示信息");
		mDialog.setMessage("下載聯繫人列表中...");
		mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);

		mCallback = new MultiModeCallback();
		contact_list_view.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
		contact_list_view.setMultiChoiceModeListener(mCallback);
	}

	private void downloadContactsFromServer() {
		if (contacts == null) {
			contacts = new ArrayList<Contact>();
		}

		for (int i = 0; i < userNames.length; i++) {
			contacts.add(new Contact(userNames[i], phoneNumbers[i]));
		}
	}

	private class ContactsDownloadTask extends AsyncTask<Void, Integer, Void> {

		private int currentlyProgressValue;

		@Override
		protected void onPreExecute() {
			mDialog.show();
			super.onPreExecute();
		}

		@Override
		protected Void doInBackground(Void... params) {

			while (currentlyProgressValue < 100) {
				publishProgress(++currentlyProgressValue);
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			// download data from server
			downloadContactsFromServer();
			return null;
		}

		@Override
		protected void onProgressUpdate(Integer... values) {
			super.onProgressUpdate(values);
			mDialog.setProgress(values[0]);
		}

		@Override
		protected void onPostExecute(Void result) {
			super.onPostExecute(result);

			mAdpater = new MyContactAdapter(ContextMenuActionModeActivity.this, contacts);
			contact_list_view.setAdapter(mAdpater);

			mDialog.dismiss();
		}

	}

	private class MultiModeCallback implements MultiChoiceModeListener {
		private View mMultiSelectActionBarView;
		private TextView mSelectedCount;

		@Override
		public boolean onCreateActionMode(ActionMode mode, Menu menu) {
			mode.getMenuInflater().inflate(R.menu.multi_acitonmode_menu, menu);

			mAdpater.setItemMultiCheckable(true);
			mAdpater.notifyDataSetChanged();

			if (mMultiSelectActionBarView == null) {
				mMultiSelectActionBarView = LayoutInflater.from(ContextMenuActionModeActivity.this)
						.inflate(R.layout.list_multi_select_actionbar, null);
				mSelectedCount = (TextView) mMultiSelectActionBarView.findViewById(R.id.selected_conv_count);
			}
			mode.setCustomView(mMultiSelectActionBarView);
			((TextView) mMultiSelectActionBarView.findViewById(R.id.title)).setText(R.string.select_item);
			return true;
		}

		@Override
		public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
			// TODO Auto-generated method stub
			return false;
		}

		@Override
		public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
			switch (item.getItemId()) {
			case R.id.menu_cancle:
				mAdpater.setItemMultiCheckable(false);
				mAdpater.clearSeletedContacts();
				mAdpater.notifyDataSetChanged();
				mode.finish();
				break;
			case R.id.menu_delete:
				mAdpater.deleteSeletedContacts();
				mAdpater.notifyDataSetChanged();
				mode.invalidate();
				mode.finish();
				break;

			default:
				break;
			}
			return false;
		}

		@Override
		public void onDestroyActionMode(ActionMode mode) {
			mAdpater.setItemMultiCheckable(false);
			mAdpater.clearSeletedContacts();
			mAdpater.notifyDataSetChanged();

		}

		@Override
		public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
			if (checked) {
				mAdpater.addSelectedContact(position);
			} else {
				mAdpater.cancelSeletedContact(position);
			}

			mAdpater.notifyDataSetChanged();

			updateSeletedCount();
			mode.invalidate();

		}

		public void updateSeletedCount() {
			mSelectedCount.setText(Integer.toString(contact_list_view.getCheckedItemCount()) + "條");
		}

	}

}

對於我們這樣的菜鳥來說,上面activity代碼中值得注意的可能是:

1、基本上,我們首先會定義一個異步任務類,模擬從網絡下載數據的過程。有助於Adapter的API的使用的掌握。

2、我們在上面代碼中定義的實現了MultiChoiceModeListener接口的內部類,MultiModeCallback就是幫助我們實現長按ListView(也試用於GridView),並且監聽處理MultiChoice事件的關鍵。

     —  簡單來說,可以看到,我們在該內部類的回調方法onCreateActionMode中,處理長按ListView後,ActionMode菜單相關的創建工作。並且在此控制ListView中的CheckBox顯示,告知用戶,我們已經進入到了可以進行批量操作的模式下。

     —  onActionItemClicked方法 用於監聽和響應菜單上對應的選項的點擊事件,你可以在此根據自己的需求,爲對應的菜單選項編寫響應代碼。

     —  onDestroyActionMode方法 用於處理菜單銷燬時,所要執行的動作。

     —  而onItemCheckedStateChanged方法 則就是用於監聽處理ListView中每個列表項的選中狀態改變時的回調了,我們會在這裏根據需求完成對應的編碼工作。

3、到了這裏,我們對於我們想要的功能的實現,可以說已經是基本搞定了。但是,你可能已經在上面的MultiModeCallback類的某些代碼中注意點到:

爲了更加友善的交互感受,我們 還可以以ActionBar的形式,在菜單欄上,添加一段內容,正如 短信功能裏所使用的那樣,用以提示用戶類似於“您當前已經選擇了XX條內容”的信息。所以我們還會定義一個類似ActionBar的佈局文件,如下:

list_multi_select_actionbar.xml:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/custom_title_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/title"
        android:layout_gravity="center_vertical"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:textColor="#ffffff" />

    <TextView
        android:id="@+id/selected_conv_count"
        android:layout_gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:textColor="#ffffff"/>

</LinearLayout>

在上面的代碼中,你可以看到,我們同樣是在onCreateActionMode方法中完成ActionMode上下文菜單的裝載工作的同時,也會進行對於該作爲ActionBar使用的View的裝載與顯示控制工作。

在該View裝載和顯示工作完成之後,我們要做的就很簡單了,只需要在onItemCheckedStateChanged中進行監聽,當用戶選中某個列表項時,對用於顯示提示信息的TextView的顯示內容進行更新,則OK了。


走到這一步,我們可以說是已經山寨完畢了,下一步要做的則可以將Demo編譯到模擬器或者手機上,看看效果了~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章