ExpandableListView的使用以及信息的高亮显示

ExpandableListView是ListView控件的延伸,它能够对数据进行分组显示和隐藏,并统计总数量。可进行滚动,对某一内容高亮显示。


<1>编写xml布局文件,用于获取ExpandableListView对象

sf_activity_contact_list.xml

<span style="font-family:Microsoft YaHei;"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/contacts_list_holder"
    style="@style/SF_ActivityBackground"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/sf_activity_background_color"
    android:overScrollMode="always"
    android:scrollbarStyle="outsideInset"
    android:scrollbars="vertical">

    <!-- style="@style/SF_ActivityBackground" -->

    <ExpandableListView
        android:id="@+id/contacts_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fastScrollEnabled="true" >
    </ExpandableListView>

</LinearLayout></span>

<2>编写Activity界面,获取ExpandableListView对象。并设置适配器和对应的属性、事件等等

package com.snapfish.ui;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;

import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.SearchView.OnQueryTextListener;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.EditText;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageView;
import android.widget.TextView;

import com.snapfish.R;
import com.snapfish.android.generated.bean.AddressType;
import com.snapfish.android.generated.bean.Entry;
import com.snapfish.android.generated.bean.MrchShippingOption;
import com.snapfish.android.generated.bean.OpenSocialResponseType;
import com.snapfish.android.generated.bean.Person;
import com.snapfish.android.generated.bean.PublisherOrder;
import com.snapfish.android.generated.bean.PublisherOrderAddress;
import com.snapfish.checkout.IUserData;
import com.snapfish.internal.core.SFConstants;
import com.snapfish.internal.database.SFContactsHolderDB;
import com.snapfish.internal.datamodel.SFContact;
import com.snapfish.internal.datamodel.SFContactDBManager;
import com.snapfish.internal.event.SFEventManager;
import com.snapfish.internal.event.SFIEventListener;
import com.snapfish.internal.event.SFLocaleContactsEvent;
import com.snapfish.internal.event.SFRemoteContactsEvent;
import com.snapfish.util.CShippingOptionsAdapter;
import com.snapfish.util.SFActivityUtils;
import com.snapfish.util.SFContactExpandableListAdapter;
import com.snapfish.util.SFLogger;

public class SFContactsListActivity extends ABaseActivity { // implements
															// LoaderManager.LoaderCallbacks<Cursor>

	private static final SFLogger sLogger = SFLogger
			.getInstance(SFContactsListActivity.class.getName());

	private Context m_ctx;

	private TextView m_contactNameOverlay;
	private boolean m_visible;

	// private static final int CONTACT_DATA_LOADER = 111;

	OnScrollListener mItemsListScrollListener = new OnScrollListener() {

		@Override
		public void onScrollStateChanged(AbsListView view, int scrollState) {
			m_visible = true;

			// when stop scrolling
			if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
				m_contactNameOverlay.setVisibility(View.INVISIBLE);
				m_visible = false;
			}
			// when scrolling and the finger is on the screen
			else if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {

			}
			// when scrolling and the finger perform an action above
			else if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {

			}
		}

		@Override
		public void onScroll(AbsListView view, int firstVisibleItem,
				int visibleItemCount, int totalItemCount) {
			if (visibleItemCount > 0 && m_visible) {
				SFContact contact = mPhoneContacts.get(firstVisibleItem);
				m_contactNameOverlay.setText(String.valueOf(contact.getName()
						.charAt(0)));
				m_contactNameOverlay.setVisibility(View.VISIBLE);
			}
		}
	};

	/**************************************************
	 * The event of getting locale contacts
	 *************************************************/
	private SFIEventListener<SFLocaleContactsEvent> m_localeContactListEvent = new SFIEventListener<SFLocaleContactsEvent>() {

		@Override
		public void onEvent(SFLocaleContactsEvent event) {
			// TODO The event of getting locale contacts
			sLogger.debug("****************The event of getting locale contacts*****************");
			hideProgressDialog();
			if (null != event && event.getContactsList() != null) {
				mPhoneContacts = event.getContactsList();

				showProgressDialog(getString(R.string.sf_please_wait),
						getString(R.string.sf_progress_remote_contact_list));
				// Get all remote contacts
				SFContactDBManager.asyncGetAllRemoteContacts(getSession());
			}
		}
	};

	/**************************************************
	 * The event of getting remote contacts
	 *************************************************/
	private SFIEventListener<SFRemoteContactsEvent> m_remoteContactListEvent = new SFIEventListener<SFRemoteContactsEvent>() {

		@Override
		public void onEvent(SFRemoteContactsEvent event) {
			// TODO The event of getting remote contacts
			sLogger.debug("****************The event of getting remote contacts*****************");
			hideProgressDialog();
			if (null != event && event.getOpenSocialResponseType() != null) {
				OpenSocialResponseType osrt = event.getOpenSocialResponseType();
				if (null != osrt) {
					List<Entry> entries = osrt.getEntryList();
					if (null != entries && !entries.isEmpty()) {
						int entrySize = entries.size();
						for (int i = 0; i < entrySize; i++) {
							try {
								Person person = Person.newFromJSON(entries.get(
										i).toJSON());
								if (null != person) {
									AddressType address = person.getAddress();
									String displayName = person
											.getDisplayName();
									if (isValidContact(address)
											&& !TextUtils.isEmpty(displayName)) {
										SFContact contact = new SFContact(
												IUserData.EUserDataType.SNAPFISH,
												person.getId(),
												displayName,
												address.getStreet1(),
												address.getCity(),
												address.getProvince(),
												address.getPostalCode(),
												SFContact.ContactAddressType.TYPE_OTHER);

										contact.setGuessedPrecision(SFContact.GuessedPrecision.ABSOLUTE);
										contact.setCountry(address.getCountry() == null ? getResources()
												.getConfiguration().locale
												.getCountry() : address
												.getCountry());
										contact.setAddressGuessed(false);
										contact.setIsConfirmed(true);
										contact.setPhone(address.getPhone());
										mPhoneContacts.add(contact);
									}
								}
							} catch (JSONException e) {
								sLogger.error(e.getMessage());
							}
						}
					}
				}
			}

			onContactsListCreated();
		}
	};

	/*
	 * carry over from order review activity in case if contact has to be edited
	 * or order update must happen on this screen
	 */
	private PublisherOrder m_publisherOrder;
	private int m_orderQuantity;
	private long m_mrchId;

	private SFContact mSelectedAddress;
	private List<SFContact> mPhoneContacts;
	private ExpandableListView mContactsListView;
	private SFContactExpandableListAdapter mContactsListAdapter;
	private List<MrchShippingOption> m_mrchShippingOptions = new ArrayList<MrchShippingOption>();

	private String m_shippingOptionDesc;
	private String m_shippingOptionType;

	/* search */
	private MenuItem searchItem;
	private SearchView mSearchView;
	private ImageView closeBtn;
	private EditText searchEditText;
	public static boolean isSearchQuery;
	private String mSearchString;

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

		m_ctx = this;

		// getSupportLoaderManager().initLoader(CONTACT_DATA_LOADER, null,
		// this);

		// Display home and finish current activity
		SFActivityUtils.displayHomeAsUp(this);

		getDataFromIntent();
		mContactsListView = (ExpandableListView) findViewById(R.id.contacts_list);
		/*
		 * on child click the action that happens will depend whether contact
		 * selected is confirmed or not, meaning that the end-user has validated
		 * address and order can be safely sent to selected address
		 */
		mContactsListView.setOnChildClickListener(new OnChildClickListener() {
			@Override
			public boolean onChildClick(ExpandableListView parent, View v,
					int groupPosition, int childPosition, long id) {
				mSelectedAddress = (SFContact) mContactsListAdapter.getChild(
						groupPosition, childPosition);
				/*
				 * in the event when address precision is below 100% it must be
				 * validated by the end-user so the CShippingAddressActivity is
				 * launched first
				 */
				Intent exitIntent = null;
				if (mSelectedAddress.getGuessedPrecision() != SFContact.GuessedPrecision.ABSOLUTE) {
					exitIntent = new Intent(SFContactsListActivity.this,
							SFShippingAddressActivity.class);
					/* pass contact _id */
					exitIntent.putExtra(
							SFContactsHolderDB.SFContactColumns._ID,
							mSelectedAddress.getId());
					/* selected shipping address for edit */
					exitIntent.putExtra(SFConstants.SF_SHIPPING_ADDRESS,
							handleLocalization(mSelectedAddress));
					/* shipping options */
					exitIntent.putExtra(
							SFConstants.MRCH_SHIP_OPTIONS,
							getIntent().getStringArrayExtra(
									SFConstants.MRCH_SHIP_OPTIONS));
					/* order ID ? */
					exitIntent.putExtra(SFConstants.ORDERID,
							m_publisherOrder.getOrderId());
					/* quantity ? */
					exitIntent.putExtra(SFConstants.ORDER_QUANTITY, getIntent()
							.getIntExtra(SFConstants.ORDER_QUANTITY, -1));
					/* mrchId */
					exitIntent.putExtra(SFConstants.MRCH_ID, getIntent()
							.getLongExtra(SFConstants.MRCH_ID, -1));
					/* and finally order */
					exitIntent.putExtra(SFConstants.SF_ORDER, orderAsString());

					startActivityForResult(exitIntent,
							SFConstants.SF_REQUEST_CODE_SHIPPING_ADDRESS);
				} else {/* adding selected address to order */
					m_publisherOrder.setShippingAddress(mSelectedAddress);

					resetShippingOptions();

					if (m_shippingOptionDesc != null
							&& m_shippingOptionType != null) {
						exitIntent = new Intent();
						exitIntent.putExtra(SFConstants.SF_ORDER,
								orderAsString());
						exitIntent.putExtra(
								SFConstants.SF_SHIPPING_OPTION_DESCRIPTION,
								m_shippingOptionDesc);
						exitIntent.putExtra(
								SFConstants.SF_SHIPPING_OPTION_SHIPPING_TYPE,
								m_shippingOptionType);
						setResult(RESULT_OK, exitIntent);
						finish();
					}
				}
				return false;
			}
		});

	}

	@Override
	protected void onResume() {
		// register the event of getting locale contacts
		SFEventManager.subscribe(m_ctx, SFLocaleContactsEvent.class,
				m_localeContactListEvent);

		// register the event of getting remote contacts
		SFEventManager.subscribe(m_ctx, SFRemoteContactsEvent.class,
				m_remoteContactListEvent);

		m_contactNameOverlay = (TextView) LayoutInflater.from(m_ctx).inflate(
				R.layout.sf_contact_pop_overlay, null);
		m_contactNameOverlay.setVisibility(View.INVISIBLE);
		WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
				LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
				WindowManager.LayoutParams.TYPE_APPLICATION,
				WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
						| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
				PixelFormat.TRANSLUCENT);
		WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
		windowManager.addView(m_contactNameOverlay, lp);

		showProgressDialog(getString(R.string.sf_please_wait),
				getString(R.string.sf_progress_locale_contact_list));

		// Get all local contacts
		SFContactDBManager.asyncGetAllLocalContacts(getSession());

		// initializeData();
		// ctx = this;
		// mContactsListView.setAdapter(new ContactExpandListAdapter(group,
		// child, ctx));

		super.onResume();
	}

	@Override
	protected void onPause() {
		// uninstall the event of getting locale contacts
		SFEventManager.unsubscribe(m_ctx, SFLocaleContactsEvent.class,
				m_localeContactListEvent);

		// uninstall the event of getting remote contacts
		SFEventManager.unsubscribe(m_ctx, SFRemoteContactsEvent.class,
				m_remoteContactListEvent);

		super.onPause();
	}

	/**
	 * simply copies street1 into street2 whenever street2 is a mandatory field
	 * in the address entry
	 * 
	 * @param contact
	 * @return
	 */
	private String handleLocalization(PublisherOrderAddress contact) {
		if (getResources().getBoolean(R.bool.sf_address_street2_required)) {
			sLogger.debug("not a localized address: " + contact.toString());
			if (contact.getStreet2() == null) {
				contact.setStreet2(contact.getStreet1());
				contact.setStreet1(null);
			}
		}

		String convertedAddr = "";
		try {
			convertedAddr = contact.toJSON().toString();
			sLogger.debug("localized address: " + convertedAddr);
		} catch (JSONException e) {
			sLogger.error("cannot convert selected address: " + e);
		}

		return convertedAddr;
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);

		if (requestCode == SFConstants.SF_REQUEST_CODE_SHIPPING_ADDRESS) {
			if (resultCode == RESULT_OK) {
				/*
				 * after a contact had been verified by end-user it's precision
				 * level is moved to ABSOLUTE
				 */
				mSelectedAddress
						.setGuessedPrecision(SFContact.GuessedPrecision.ABSOLUTE);
				setResult(RESULT_OK, data);
				finish();
			}
		}

	}

	/**
	 * carry over the data required by CShippingAddressActivity
	 */
	private void getDataFromIntent() {
		Intent intent = getIntent();
		String order = intent.getStringExtra(SFConstants.SF_ORDER);
		String[] mrchShippingOptions = getIntent().getStringArrayExtra(
				SFConstants.MRCH_SHIP_OPTIONS);

		if (order != null) {
			try {
				m_publisherOrder = PublisherOrder.newFromJSON(new JSONObject(
						order));
				if (null != mrchShippingOptions) {
					for (String mrchShippingOption : mrchShippingOptions) {
						m_mrchShippingOptions
								.add(MrchShippingOption
										.newFromJSON(new JSONObject(
												mrchShippingOption)));
					}
				}
			} catch (JSONException e) {
				sLogger.error("cannot parse order: " + order);
				sLogger.error("cannot parse order: " + e);
			}
		} else {
			showWarningDialog("Order failed", "order not found in intent");
		}
	}

	/**
	 * initializes the contacts list adapter
	 */
	private void onContactsListCreated() {
		/* set adapter */
		mContactsListAdapter = new SFContactExpandableListAdapter(this,
				mPhoneContacts);
		mContactsListView.setAdapter(mContactsListAdapter);
		/* hide the divider */
		mContactsListView.setDivider(null);
		/*
		 * hide indicator, by design it's the first letter of a contact that
		 * takes place of the indicator
		 */
		mContactsListView.setGroupIndicator(null);
		/*
		 * make list state expanded - the first time it should display expanded
		 * list of contacts
		 */
		for (int i = 0; i < mContactsListAdapter.getGroupCount(); i++) {
			mContactsListView.expandGroup(i);
		}

		/*
		 * onScrollListener
		 */
		mContactsListView.setOnScrollListener(mItemsListScrollListener);
	}

	// private List<String> group;// 组列表
	// private List<List<String>> child;// 子列表
	// private Context ctx;
	//
	// private void initializeData() {
	// group = new ArrayList<String>();
	// child = new ArrayList<List<String>>();
	//
	// addInfo("Andy", new String[] { "male", "138123***", "GuangZhou" });
	// addInfo("Fairy", new String[] { "female", "138123***", "GuangZhou" });
	// addInfo("Jerry", new String[] { "male", "138123***", "ShenZhen" });
	// addInfo("Tom", new String[] { "female", "138123***", "ShangHai" });
	// addInfo("Bill", new String[] { "male", "138231***", "ZhanJiang" });
	// }
	//
	// private void addInfo(String group, String[] child) {
	// this.group.add(group);
	// List<String> childItem = new ArrayList<String>();
	// for (int i = 0; i < child.length; i++) {
	// childItem.add(child[i]);
	// }
	//
	// this.child.add(childItem);
	// }

	/**
	 * converts order to string
	 * 
	 * @return
	 */
	private String orderAsString() {
		String orderAsString = "";
		try {
			orderAsString = m_publisherOrder.toJSON().toString();
		} catch (JSONException e) {
			e.printStackTrace();
		}
		return orderAsString;
	}

	private void resetShippingOptions() {
		PublisherOrder order = m_publisherOrder;
		PublisherOrderAddress cnt = null;
		try {
			cnt = PublisherOrderAddress.newFromJSON(new JSONObject(
					handleLocalization(order.getShippingAddress())));
			sLogger.debug("order address: " + cnt.toJSON());
		} catch (JSONException e) {
			e.printStackTrace();
		}

		if (m_mrchShippingOptions.size() > 0) {
			CShippingOptionsAdapter soa = new CShippingOptionsAdapter(
					getApplicationContext(), getSession().getAppCredentials()
							.getLocale(), m_mrchShippingOptions, m_mrchId,
					m_orderQuantity, cnt);

			String shipping = getApplicationContext().getResources().getString(
					R.string.sf_order_shipping_opt_no_colon);

			sLogger.debug("getSession().getAppCredentials().getLocale(): "
					+ getSession().getAppCredentials().getLocale());
			sLogger.debug("soa.getAvailableShipOptions().size(): "
					+ soa.getAvailableShipOptions().size());

			if (soa.getAvailableShipOptions().size() > 0) {
				MrchShippingOption item = soa.getAvailableShipOptions().get(0);
				String shippingDescription = item.getDescription();
				String shippingType = Character.toString(
						shippingDescription.charAt(0)).toUpperCase()
						+ shippingDescription.substring(1);
				order.setShippingOption(item.getShippingCode());
				order.setShipping(0.0f);
				m_shippingOptionDesc = soa.estimatedDeliveryDays(item
						.getShippingCode());
				m_shippingOptionType = shippingType + shipping;

				showProgressDialog(
						getResources().getString(R.string.sf_please_wait),
						getResources().getString(
								R.string.sf_progress_update_order));

			} else {
				showErrorAlertDialog();
			}
		}
	}

	private void showErrorAlertDialog() {
		AlertDialog.Builder alertDialog = new AlertDialog.Builder(
				SFContactsListActivity.this);
		alertDialog.setTitle(R.string.sf_warning_dialog_title);
		alertDialog.setMessage(R.string.sf_error_no_shipping_options)
				.setPositiveButton("OK", new DialogInterface.OnClickListener() {

					@Override
					public void onClick(DialogInterface dialog, int which) {
						dialog.dismiss();
					}
				}).show();
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.sf_menu_p2r, menu);
		searchItem = menu.findItem(R.id.p2r_action_search);
		mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);

		searchEditText = (EditText) mSearchView
				.findViewById(R.id.search_src_text);
		searchEditText.setHintTextColor(getResources().getColor(
				R.color.sf_button_normal));
		searchEditText.setHint(R.string.sf_contacts_search_hint);

		SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
		ComponentName searchComponent = new ComponentName(this,
				SFContactsListActivity.class);

		mSearchView.setSearchableInfo(searchManager
				.getSearchableInfo(searchComponent));
		mSearchView.setIconifiedByDefault(true);
		mSearchView.setSubmitButtonEnabled(false);

		closeBtn = (ImageView) mSearchView.findViewById(R.id.search_close_btn);

		MenuItemCompat.setOnActionExpandListener(searchItem,
				new MenuItemCompat.OnActionExpandListener() {

					@Override
					public boolean onMenuItemActionCollapse(MenuItem arg0) {
						sLogger.debug("search field collapsed");
						return true;
					}

					@Override
					public boolean onMenuItemActionExpand(MenuItem arg0) {
						sLogger.debug("search field expanded");
						return true;
					}

				});

		closeBtn.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				searchEditText.setText("");
			}
		});

		mSearchView.setOnQueryTextListener(new OnQueryTextListener() {

			@Override
			public boolean onQueryTextSubmit(String arg0) {
				return false;
			}

			@Override
			public boolean onQueryTextChange(String searchText) {
				if (searchText.length() > 0) {
					mSearchString = searchText;
					sLogger.debug("search text is: " + searchText);
				} else {
					mSearchString = null;
					sLogger.debug("reset list");
				}
				return true;
			}
		});

		return super.onCreateOptionsMenu(menu);
	}

	/**
	 * Invoking this method when the item option of menu selected
	 */
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// when the top|left icon selected,finish current activity
		if (item.getItemId() == android.R.id.home) {
			finish();
			overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
			return true;
		}
		return super.onOptionsItemSelected(item);
	}

	private boolean isValidContact(AddressType addressType) {
		if (addressType == null)
			return false;

		if (!TextUtils.isEmpty(addressType.getStreet1())
				&& !TextUtils.isEmpty(addressType.getCity())
				&& !TextUtils.isEmpty(addressType.getProvince())
				&& !TextUtils.isEmpty(addressType.getPhone()))
			return true;

		return false;
	}

}

<3>编写布局文件sf_view_contact_sort_header.xml。用于展示租名和记录数量。
<?

xml version="1.0" encoding="utf-8"?

> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:paddingTop="5dp" android:paddingBottom="5dp" android:paddingLeft="@dimen/sf_standard_layout_padding" android:paddingRight="@dimen/sf_standard_layout_padding" android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/sf_contact_sort_by_name" style="@style/SF_MediumBlueBoldTxt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:layout_marginLeft="5dp" android:text="@string/sf_contacts_sorting_txt" /> <TextView android:id="@+id/sf_contact_sort_counter" style="@style/SF_SmallLightGreyTxt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:gravity="right" android:text="@string/sf_contacts_counter_txt" android:visibility="gone" /> </RelativeLayout>


<4>编写布局文件sf_view_contact_item.xml,用于展示数据的内容:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/SF_SelectableSectionBackground"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/sf_activity_background_color"
    android:paddingBottom="5dp"
    android:paddingLeft="@dimen/sf_contact_item_layout_padding"
    android:paddingRight="@dimen/sf_contact_item_layout_padding" >

    <LinearLayout
        android:id="@+id/sf_ll_contact_item_divider"
        style="@style/SF_HorizontalDivider"
        android:layout_width="match_parent"
        android:layout_marginBottom="5dp"
        android:orientation="horizontal" />

    <include
        android:id="@+id/sf_include_contact_item_photo"
        android:layout_width="65dp"
        android:layout_height="65dp"
        android:layout_alignLeft="@+id/sf_ll_contact_item_divider"
        android:layout_alignTop="@+id/sf_ll_contact_item_divider"
        android:layout_centerHorizontal="true"
        android:layout_centerInParent="true"
        android:layout_centerVertical="true"
        android:layout_marginTop="5dp"
        layout="@layout/sf_view_contact_graphics"
        android:visibility="visible" />

    <LinearLayout
        android:id="@+id/sf_ll_contact_item_details"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="5dp"
        android:layout_toRightOf="@id/sf_include_contact_item_photo"
        android:gravity="left|center_horizontal"
        android:orientation="vertical" >

        <!-- name -->

        <TextView
            android:id="@+id/sf_tv_contact_item_name"
            style="@style/SF_MediumBlackTxt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:ellipsize="end"
            android:singleLine="true"
            android:text="@string/sf_name_placeholder" />
        <!-- name end -->


        <!-- address -->

        <TextView
            android:id="@+id/sf_tv_contact_item_address"
            style="@style/SF_SmallGreyTxt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="left"
            android:ellipsize="end"
            android:singleLine="false"
            android:text="@string/sf_address_placeholder"
            android:textAlignment="center" />
        <!-- address end -->

        <TextView
            android:id="@+id/sf_tv_contact_item_phone"
            style="@style/SF_MediumLightGreyTxt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:minLines="1"
            android:scrollHorizontally="false"
            android:singleLine="true"
            android:text="@string/sf_adress_type_txt"
            android:visibility="gone" />
    </LinearLayout>

</RelativeLayout>

<5>编写SFContactExpandableListAdapter.java。继承BaseExpandableListAdapter.java

package com.snapfish.util;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.snapfish.R;
import com.snapfish.checkout.IUserData;
import com.snapfish.internal.datamodel.SFContact;
import com.snapfish.ui.SFContactsListActivity;

public class SFContactExpandableListAdapter extends BaseExpandableListAdapter { // implements
	// LoaderManager.LoaderCallbacks<Cursor>
	private static final SFLogger sLogger = SFLogger
			.getInstance(SFContactsListActivity.class.getName());

	private Context m_ctx;
	private ViewHolder m_viewHolder;
	private List<SFContact> mCombinedContacts;
	private Map<String, List<SFContact>> mSortedContacts;

	public SFContactExpandableListAdapter(Context mContext,
			List<SFContact> mCombinedContacts) {
		this.m_ctx = mContext;
		this.mCombinedContacts = mCombinedContacts;
		this.mSortedContacts = getSortedContacts();
		m_viewHolder = new ViewHolder();
	}

	@Override
	public int getGroupCount() {
		return mSortedContacts.size();
	}

	@Override
	public int getChildrenCount(int groupPosition) {
		List<SFContact> children = mSortedContacts
				.get(getKeyByPosition(groupPosition));
		return children.size();
	}

	@Override
	public Object getGroup(int groupPosition) {
		List<SFContact> group = mSortedContacts
				.get(getKeyByPosition(groupPosition));
		return group;
	}

	@Override
	public Object getChild(int groupPosition, int childPosition) {
		@SuppressWarnings("unchecked")
		List<SFContact> selectedGroup = (List<SFContact>) getGroup(groupPosition);
		if (selectedGroup.size() < childPosition)
			return null;

		return selectedGroup.get(childPosition);
	}

	@Override
	public long getGroupId(int groupPosition) {
		return groupPosition;
	}

	@SuppressWarnings("unchecked")
	@Override
	public long getChildId(int groupPosition, int childPosition) {
		List<SFContact> childGroup = (List<SFContact>) getGroup(groupPosition);
		if (null != childGroup && childGroup.get(childPosition) != null)
			return childPosition;

		return 0;
	}

	@Override
	public boolean hasStableIds() {
		return false;
	}

	@Override
	public View getGroupView(int groupPosition, boolean isExpanded,
			View convertView, ViewGroup parent) {
		View row = LayoutInflater.from(m_ctx).inflate(
				R.layout.sf_view_contact_sort_header, parent, false);
		TextView sortTextView = (TextView) row
				.findViewById(R.id.sf_contact_sort_by_name);
		TextView countTextView = (TextView) row
				.findViewById(R.id.sf_contact_sort_counter);
		String letter = getKeyByPosition(groupPosition);

		if (groupPosition == 0) {
			countTextView.setVisibility(View.VISIBLE);
			countTextView.setText("" + mCombinedContacts.size() + " "
					+ m_ctx.getString(R.string.sf_contacts_counter_txt));
		}

		sortTextView.setText("  " + letter);
		return row;
	}

	@Override
	public View getChildView(int groupPosition, int childPosition,
			boolean isLastChild, View convertView, ViewGroup parent) {
		SFContact contact = (SFContact) getChild(groupPosition, childPosition);
		if (contact != null) {
			// SFContact.ContactAddressType displayType = contact
			// .getAddressType();
			String displayName = contact.getName();
			String displayAddress = getContactAddress(contact);

			View row = LayoutInflater.from(m_ctx).inflate(
					R.layout.sf_view_contact_item, parent, false);

			// contact name
			m_viewHolder.m_name = (TextView) row
					.findViewById(R.id.sf_tv_contact_item_name);
			m_viewHolder.m_name.setText(displayName);

			// contact address
			m_viewHolder.m_address = (TextView) row
					.findViewById(R.id.sf_tv_contact_item_address);
			m_viewHolder.m_address.setText(displayAddress);

			/* contact address type */
			m_viewHolder.m_phone = (TextView) row
					.findViewById(R.id.sf_tv_contact_item_phone);

			// contact photo
			setContactIcon(row, contact, getKeyByPosition(groupPosition));

			// if (isMultipleAddress(groupPosition, childPosition)
			// || contact.getGuessedPrecision() ==
			// SFContact.GuessedPrecision.LOW) {
			// showAddressType(m_viewHolder.m_phone,
			// contact.getGuessedPrecision(), displayType);
			// }

			row.setTag(m_viewHolder);
			return row;
		}

		return convertView;
	}

	/**
	 * to avoid displaying address type when we do not have matching contacts
	 * 
	 * @param groupPosition
	 * @param childPosition
	 * @return
	 */
	private boolean isMultipleAddress(int groupPosition, int childPosition) {
		@SuppressWarnings("unchecked")
		List<SFContact> group = (List<SFContact>) getGroup(groupPosition);
		SFContact reference = (SFContact) getChild(groupPosition, childPosition);
		String referenceName = reference.getName();
		int i = group.size();

		for (int a = 0; a < i; a++) {
			if (a != childPosition)
				if (referenceName.equalsIgnoreCase(group.get(a).getName()))
					return true;
		}
		return false;
	}

	@Override
	public boolean isChildSelectable(int groupPosition, int childPosition) {
		return true;
	}

	/**
	 * contact icons are of three kinds: image, icon, or warning message
	 * 
	 * @param aContact
	 * @param letter
	 * @param addrPositiveMatch
	 */
	private void setContactIcon(View row, SFContact aContact, String letter) {
		// TODO guessed precision was not set in the DB - set it up
		SFContact.GuessedPrecision addrPositiveMatch = aContact
				.getGuessedPrecision();

		ViewGroup contactImageView = (ViewGroup) row
				.findViewById(R.id.sf_include_contact_item_photo);

		if (contactImageView != null) {
			ImageView unverifiedAddress = (ImageView) contactImageView
					.findViewById(R.id.sf_iv_contact_need_confirmation);
			LinearLayout noCntPhoto = (LinearLayout) contactImageView
					.findViewById(R.id.sf_ll_contact_no_photo);
			TextView cntFirstLetter = (TextView) noCntPhoto
					.findViewById(R.id.sf_tv_contact_name_letter);
			ImageView defaultCntIcon = (ImageView) contactImageView
					.findViewById(R.id.sf_iv_contact_default_photo);
			defaultCntIcon.setVisibility(View.GONE);
			cntFirstLetter.setVisibility(View.GONE);
			noCntPhoto.setVisibility(View.GONE);

			/* get profile photo bitmap to load into ImageView */
			Bitmap bmp = getProfilePhoto(aContact);

			switch (addrPositiveMatch) {
			case ABSOLUTE:
				if (bmp != null) {
					defaultCntIcon.setVisibility(View.VISIBLE);
					defaultCntIcon.setImageBitmap(bmp);
				} else {
					noCntPhoto.setVisibility(View.VISIBLE);
					noCntPhoto.setBackgroundColor(Color
							.parseColor(getRandomColor()));
					cntFirstLetter.setVisibility(View.VISIBLE);
					cntFirstLetter.setText(letter);
				}
				break;
			case HIGH:
				unverifiedAddress.setVisibility(View.VISIBLE);
				break;
			case MEDIUM:
				unverifiedAddress.setVisibility(View.VISIBLE);
				break;
			case LOW:
				break;
			default:
				unverifiedAddress.setVisibility(View.VISIBLE);
				break;
			}
		}
	}

	private void showAddressType(TextView typeView,
			SFContact.GuessedPrecision precisionLevel,
			SFContact.ContactAddressType displayType) {
		Resources r = m_ctx.getResources();
		// typeView.setVisibility(View.VISIBLE);
		typeView.setVisibility(View.GONE);

		if (precisionLevel != SFContact.GuessedPrecision.ABSOLUTE) {// not
																	// verified
																	// (guessed)
																	// address
			typeView.setTextColor(r
					.getColor(R.color.sf_error_message_txt_color));
			typeView.setText(r.getString(R.string.sf_verify_address_label));
		} else if (displayType != null) { // verified address
			typeView.setText(getAddressType(displayType));
		}
	}

	private int colorsUsed;

	/**
	 * using preset swatches to set background colors in the layout
	 * 
	 * @return
	 */
	private String getRandomColor() {
		String[] colorChoises = m_ctx.getResources().getStringArray(
				R.array.sf_swatches_sp_set);

		if (colorsUsed == colorChoises.length)
			colorsUsed = 0;

		String colorOut = colorChoises[colorsUsed];

		colorsUsed++;
		return colorOut;
	}

	private Bitmap getProfilePhoto(SFContact aContact) {
		if (aContact.getContactSource().equals(
				IUserData.EUserDataType.PHONEBOOK)) {
			InputStream photoStream = getPhotoAsInputSteam(aContact);
			if (photoStream != null)
				return BitmapFactory.decodeStream(photoStream);
		}
		return null;
	}

	private String getKeyByPosition(int groupPosition) {
		int keyPosition = 0;
		Iterator<String> iter = mSortedContacts.keySet().iterator();
		while (iter.hasNext()) {
			String key = iter.next();
			if (keyPosition == groupPosition) {
				return key;
			}
			keyPosition++;
		}
		return null;
	}

	/**
	 * extracting first letter of display name
	 * 
	 * @return
	 */
	private Collection<String> extractFirstLetter() {
		Collection<String> sortByLetter = new HashSet<String>();
		int index = mCombinedContacts.size();
		Collections.sort(mCombinedContacts, new Comparator<SFContact>() {

			@Override
			public int compare(SFContact lhs, SFContact rhs) {
				return lhs.getName().compareTo(rhs.getName());
			}
		});
		for (int i = 0; i < index; i++) {
			char letter = mCombinedContacts.get(i).getName().charAt(0);
			sortByLetter.add(String.valueOf(letter).toUpperCase());
		}
		return sortByLetter;
	}

	private Map<String, List<SFContact>> getSortedContacts() {
		List<SFContact> selectedContact = null;
		mSortedContacts = new TreeMap<String, List<SFContact>>();

		Iterator<String> firstLetter = extractFirstLetter().iterator();
		while (firstLetter.hasNext()) {
			String s = firstLetter.next();
			int index = mCombinedContacts.size();
			selectedContact = new ArrayList<SFContact>();
			for (int i = 0; i < index; i++) {
				String initChar = String.valueOf(mCombinedContacts.get(i)
						.getName().charAt(0));
				if (s.equalsIgnoreCase(initChar)) {
					selectedContact.add(mCombinedContacts.get(i));
				}
			}
			mSortedContacts.put(s, selectedContact);
		}

		return mSortedContacts;
	}

	/**
	 * retrieves address type string from arrays.xml
	 * 
	 * @param addressType
	 * @return
	 */
	private String getAddressType(SFContact.ContactAddressType addressType) {
		String addrTypeOut = "";
		int arrSize = m_ctx.getResources().getStringArray(
				R.array.sf_address_type).length;
		if (arrSize != 0 && arrSize == 3) {
			switch (addressType) {
			case TYPE_HOME:
				addrTypeOut = m_ctx.getResources().getStringArray(
						R.array.sf_address_type)[0];
				break;
			case TYPE_WORK:
				addrTypeOut = m_ctx.getResources().getStringArray(
						R.array.sf_address_type)[1];
				break;
			case TYPE_OTHER:
			default:
				addrTypeOut = m_ctx.getResources().getStringArray(
						R.array.sf_address_type)[2];
				break;
			}
		} else
			addrTypeOut = "UNKNOWN";

		return addrTypeOut;
	}

	private InputStream getPhotoAsInputSteam(SFContact contact) {
		byte[] data = contact.getContactPhotoData();

		if (data != null) {
			return new ByteArrayInputStream(contact.getContactPhotoData());
		}

		return null;
	}

	@SuppressLint("InlinedApi")
	private InputStream openPhoto(long contactId) {
		Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
				contactId);
		Uri photoUri = Uri.withAppendedPath(contactUri,
				Contacts.Photo.CONTENT_DIRECTORY);
		Cursor cursor = m_ctx.getContentResolver().query(photoUri,
				new String[] { Contacts.Photo.PHOTO }, null, null, null);
		if (cursor == null) {
			return null;
		}
		try {
			if (cursor.moveToFirst()) {
				byte[] data = cursor.getBlob(0);
				if (data != null) {
					return new ByteArrayInputStream(data);
				}
			}
		} finally {
			cursor.close();
		}
		return null;
	}

	private String getContactAddress(SFContact contact) {
		StringBuilder sb = new StringBuilder();
		sb.append(contact.getCountry() + " ");
		sb.append(contact.getProvince() + " ");
		sb.append(contact.getCity() + " ");
		sb.append(TextUtils.isEmpty(contact.getStreet1()) ? (TextUtils
				.isEmpty(contact.getStreet2()) ? (TextUtils.isEmpty(contact
				.getStreet3()) ? "" : contact.getStreet3()) : contact
				.getStreet2()) : contact.getStreet1() + "  ");
		sb.append(contact.getPostalCode() + "
");
		sb.append(contact.getPhone());

		return sb.toString();
	}

	private static class ViewHolder {
		TextView m_name;
		TextView m_address;
		TextView m_phone;
	}

}


注意

继承BaseExpandableListAdapter需实现对应的方法:

<1>getGroupId():获取组号

<2>getChildId():获取子元素编号(位于组中)

<3>getGroup():获取指定组的组内容

<4>getChild():依据组和子元素的位置获取子元素内容

<5>getGroupCount():获取组的数目

<6>getChildrenCount():获取相应组的子元素的数目

<7>getGroupView():获取View对象用于显示组内容

<8>getChildView():依据组和子元素的位置获取View对象用于显示子元素的内容

<9>hasStableIds():组和子元素是否持有稳定的ID,也就是底层数据的改变不会影响到它们

<10>isChildSelectable():是否可选择指定位置的子元素



原文地址:https://www.cnblogs.com/gccbuaa/p/7182884.html