Android联系人中联系人列表页的ListView做得用户体验非常好的,于是想把它从源码中提取出来,以便日后使用。写了一个简单的例子,一方面算是给自己备忘,另一方面跟大家分享一下。
好了,先来看看效果图:
向上挤压的动画
选择右边的导航栏
好了,废话不多说,直接上代码
右侧导航栏 BladeView.java
- package com.example.pinnedheaderlistviewdemo.view;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.os.Handler;
- import android.util.AttributeSet;
- import android.view.Gravity;
- import android.view.MotionEvent;
- import android.view.View;
- import android.widget.PopupWindow;
- import android.widget.TextView;
- import com.example.pinnedheaderlistviewdemo.R;
- public class BladeView extends View {
- private OnItemClickListener mOnItemClickListener;
- String[] b = { "#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K",
- "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X",
- "Y", "Z" };
- int choose = -1;
- Paint paint = new Paint();
- boolean showBkg = false;
- private PopupWindow mPopupWindow;
- private TextView mPopupText;
- private Handler handler = new Handler();
- public BladeView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
- public BladeView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public BladeView(Context context) {
- super(context);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (showBkg) {
- canvas.drawColor(Color.parseColor("#AAAAAA"));
- }
- int height = getHeight();
- int width = getWidth();
- int singleHeight = height / b.length;
- for (int i = 0; i < b.length; i++) {
- paint.setColor(Color.parseColor("#ff2f2f2f"));
- // paint.setTypeface(Typeface.DEFAULT_BOLD); //加粗
- paint.setTextSize(getResources().getDimensionPixelSize(R.dimen.bladeview_fontsize));//设置字体的大小
- paint.setFakeBoldText(true);
- paint.setAntiAlias(true);
- if (i == choose) {
- paint.setColor(Color.parseColor("#3399ff"));
- }
- float xPos = width / 2 - paint.measureText(b[i]) / 2;
- float yPos = singleHeight * i + singleHeight;
- canvas.drawText(b[i], xPos, yPos, paint);
- paint.reset();
- }
- }
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- final int action = event.getAction();
- final float y = event.getY();
- final int oldChoose = choose;
- final int c = (int) (y / getHeight() * b.length);
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- showBkg = true;
- if (oldChoose != c) {
- if (c >= 0 && c < b.length) { //让第一个字母响应点击事件
- performItemClicked(c);
- choose = c;
- invalidate();
- }
- }
- break;
- case MotionEvent.ACTION_MOVE:
- if (oldChoose != c) {
- if (c >= 0 && c < b.length) { //让第一个字母响应点击事件
- performItemClicked(c);
- choose = c;
- invalidate();
- }
- }
- break;
- case MotionEvent.ACTION_UP:
- showBkg = false;
- choose = -1;
- dismissPopup();
- invalidate();
- break;
- }
- return true;
- }
- private void showPopup(int item) {
- if (mPopupWindow == null) {
- handler.removeCallbacks(dismissRunnable);
- mPopupText = new TextView(getContext());
- mPopupText.setBackgroundColor(Color.GRAY);
- mPopupText.setTextColor(Color.WHITE);
- mPopupText.setTextSize(getResources().getDimensionPixelSize(R.dimen.bladeview_popup_fontsize));
- mPopupText.setGravity(Gravity.CENTER_HORIZONTAL
- | Gravity.CENTER_VERTICAL);
- int height = getResources().getDimensionPixelSize(R.dimen.bladeview_popup_height);
- mPopupWindow = new PopupWindow(mPopupText, height, height);
- }
- String text = "";
- if (item == 0) {
- text = "#";
- } else {
- text = Character.toString((char) ('A' + item - 1));
- }
- mPopupText.setText(text);
- if (mPopupWindow.isShowing()) {
- mPopupWindow.update();
- } else {
- mPopupWindow.showAtLocation(getRootView(),
- Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL, 0, 0);
- }
- }
- private void dismissPopup() {
- handler.postDelayed(dismissRunnable, 1500);
- }
- Runnable dismissRunnable = new Runnable() {
- @Override
- public void run() {
- // TODO Auto-generated method stub
- if (mPopupWindow != null) {
- mPopupWindow.dismiss();
- }
- }
- };
- public boolean onTouchEvent(MotionEvent event) {
- return super.onTouchEvent(event);
- }
- public void setOnItemClickListener(OnItemClickListener listener) {
- mOnItemClickListener = listener;
- }
- private void performItemClicked(int item) {
- if (mOnItemClickListener != null) {
- mOnItemClickListener.onItemClick(b[item]);
- showPopup(item);
- }
- }
- public interface OnItemClickListener {
- void onItemClick(String s);
- }
- }
PinnedHeaderListView.java
- /*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.example.pinnedheaderlistviewdemo.view;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.util.AttributeSet;
- import android.view.View;
- import android.widget.ListAdapter;
- import android.widget.ListView;
- /**
- * A ListView that maintains a header pinned at the top of the list. The
- * pinned header can be pushed up and dissolved as needed.
- */
- public class PinnedHeaderListView extends ListView {
- /**
- * Adapter interface. The list adapter must implement this interface.
- */
- public interface PinnedHeaderAdapter {
- /**
- * Pinned header state: don't show the header.
- */
- public static final int PINNED_HEADER_GONE = 0;
- /**
- * Pinned header state: show the header at the top of the list.
- */
- public static final int PINNED_HEADER_VISIBLE = 1;
- /**
- * Pinned header state: show the header. If the header extends beyond
- * the bottom of the first shown element, push it up and clip.
- */
- public static final int PINNED_HEADER_PUSHED_UP = 2;
- /**
- * Computes the desired state of the pinned header for the given
- * position of the first visible list item. Allowed return values are
- * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
- * {@link #PINNED_HEADER_PUSHED_UP}.
- */
- int getPinnedHeaderState(int position);
- /**
- * Configures the pinned header view to match the first visible list item.
- *
- * @param header pinned header view.
- * @param position position of the first visible list item.
- * @param alpha fading of the header view, between 0 and 255.
- */
- void configurePinnedHeader(View header, int position, int alpha);
- }
- private static final int MAX_ALPHA = 255;
- private PinnedHeaderAdapter mAdapter;
- private View mHeaderView;
- private boolean mHeaderViewVisible;
- private int mHeaderViewWidth;
- private int mHeaderViewHeight;
- public PinnedHeaderListView(Context context) {
- super(context);
- }
- public PinnedHeaderListView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
- public void setPinnedHeaderView(View view) {
- mHeaderView = view;
- // Disable vertical fading when the pinned header is present
- // TODO change ListView to allow separate measures for top and bottom fading edge;
- // in this particular case we would like to disable the top, but not the bottom edge.
- if (mHeaderView != null) {
- setFadingEdgeLength(0);
- }
- requestLayout();
- }
- @Override
- public void setAdapter(ListAdapter adapter) {
- super.setAdapter(adapter);
- mAdapter = (PinnedHeaderAdapter)adapter;
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- if (mHeaderView != null) {
- measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
- mHeaderViewWidth = mHeaderView.getMeasuredWidth();
- mHeaderViewHeight = mHeaderView.getMeasuredHeight();
- }
- }
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (mHeaderView != null) {
- mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
- configureHeaderView(getFirstVisiblePosition());
- }
- }
- public void configureHeaderView(int position) {
- if (mHeaderView == null) {
- return;
- }
- int state = mAdapter.getPinnedHeaderState(position);
- switch (state) {
- case PinnedHeaderAdapter.PINNED_HEADER_GONE: {
- mHeaderViewVisible = false;
- break;
- }
- case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {
- mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);
- if (mHeaderView.getTop() != 0) {
- mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
- }
- mHeaderViewVisible = true;
- break;
- }
- case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {
- View firstView = getChildAt(0);
- int bottom = firstView.getBottom();
- int itemHeight = firstView.getHeight();
- int headerHeight = mHeaderView.getHeight();
- int y;
- int alpha;
- if (bottom < headerHeight) {
- y = (bottom - headerHeight);
- alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;
- } else {
- y = 0;
- alpha = MAX_ALPHA;
- }
- mAdapter.configurePinnedHeader(mHeaderView, position, alpha);
- if (mHeaderView.getTop() != y) {
- mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
- }
- mHeaderViewVisible = true;
- break;
- }
- }
- }
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
- if (mHeaderViewVisible) {
- drawChild(canvas, mHeaderView, getDrawingTime());
- }
- }
- }
MySectionIndexer.java
- package com.example.pinnedheaderlistviewdemo;
- import java.util.Arrays;
- import android.util.Log;
- import android.widget.SectionIndexer;
- public class MySectionIndexer implements SectionIndexer{
- private final String[] mSections;//
- private final int[] mPositions;
- private final int mCount;
- /**
- * @param sections
- * @param counts
- */
- public MySectionIndexer(String[] sections, int[] counts) {
- if (sections == null || counts == null) {
- throw new NullPointerException();
- }
- if (sections.length != counts.length) {
- throw new IllegalArgumentException(
- "The sections and counts arrays must have the same length");
- }
- this.mSections = sections;
- mPositions = new int[counts.length];
- int position = 0;
- for (int i = 0; i < counts.length; i++) {
- if(mSections[i] == null) {
- mSections[i] = "";
- } else {
- mSections[i] = mSections[i].trim();
- }
- mPositions[i] = position;
- position += counts[i];
- Log.i("MySectionIndexer", "counts["+i+"]:"+counts[i]);
- }
- mCount = position;
- }
- @Override
- public Object[] getSections() {
- // TODO Auto-generated method stub
- return mSections;
- }
- @Override
- public int getPositionForSection(int section) {
- //change by lcq 2012-10-12 section > mSections.length以为>=
- if (section < 0 || section >= mSections.length) {
- return -1;
- }
- return mPositions[section];
- }
- @Override
- public int getSectionForPosition(int position) {
- if (position < 0 || position >= mCount) {
- return -1;
- }
- //注意这个方法的返回值,它就是index<0时,返回-index-2的原因
- //解释Arrays.binarySearch,如果搜索结果在数组中,刚返回它在数组中的索引,如果不在,刚返回第一个比它大的索引的负数-1
- //如果没弄明白,请自己想查看api
- int index = Arrays.binarySearch(mPositions, position);
- return index >= 0 ? index : -index - 2; //当index小于0时,返回-index-2,
- }
- }
CityListAdapter.java
- package com.example.pinnedheaderlistviewdemo.adapter;
- import java.util.List;
- import android.content.Context;
- import android.view.LayoutInflater;
- import android.view.View;
- import android.view.ViewGroup;
- import android.widget.AbsListView;
- import android.widget.AbsListView.OnScrollListener;
- import android.widget.BaseAdapter;
- import android.widget.TextView;
- import com.example.pinnedheaderlistviewdemo.City;
- import com.example.pinnedheaderlistviewdemo.MySectionIndexer;
- import com.example.pinnedheaderlistviewdemo.R;
- import com.example.pinnedheaderlistviewdemo.view.PinnedHeaderListView;
- import com.example.pinnedheaderlistviewdemo.view.PinnedHeaderListView.PinnedHeaderAdapter;
- public class CityListAdapter extends BaseAdapter implements
- PinnedHeaderAdapter, OnScrollListener {
- private List<City> mList;
- private MySectionIndexer mIndexer;
- private Context mContext;
- private int mLocationPosition = -1;
- private LayoutInflater mInflater;
- public CityListAdapter(List<City> mList, MySectionIndexer mIndexer,
- Context mContext) {
- this.mList = mList;
- this.mIndexer = mIndexer;
- this.mContext = mContext;
- mInflater = LayoutInflater.from(mContext);
- }
- @Override
- public int getCount() {
- // TODO Auto-generated method stub
- return mList == null ? 0 : mList.size();
- }
- @Override
- public Object getItem(int position) {
- // TODO Auto-generated method stub
- return mList.get(position);
- }
- @Override
- public long getItemId(int position) {
- // TODO Auto-generated method stub
- return position;
- }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- View view;
- ViewHolder holder;
- if (convertView == null) {
- view = mInflater.inflate(R.layout.select_city_item, null);
- holder = new ViewHolder();
- holder.group_title = (TextView) view.findViewById(R.id.group_title);
- holder.city_name = (TextView) view.findViewById(R.id.city_name);
- view.setTag(holder);
- } else {
- view = convertView;
- holder = (ViewHolder) view.getTag();
- }
- City city = mList.get(position);
- int section = mIndexer.getSectionForPosition(position);
- if (mIndexer.getPositionForSection(section) == position) {
- holder.group_title.setVisibility(View.VISIBLE);
- holder.group_title.setText(city.getSortKey());
- } else {
- holder.group_title.setVisibility(View.GONE);
- }
- holder.city_name.setText(city.getName());
- return view;
- }
- public static class ViewHolder {
- public TextView group_title;
- public TextView city_name;
- }
- @Override
- public int getPinnedHeaderState(int position) {
- int realPosition = position;
- if (realPosition < 0
- || (mLocationPosition != -1 && mLocationPosition == realPosition)) {
- return PINNED_HEADER_GONE;
- }
- mLocationPosition = -1;
- int section = mIndexer.getSectionForPosition(realPosition);
- int nextSectionPosition = mIndexer.getPositionForSection(section + 1);
- if (nextSectionPosition != -1
- && realPosition == nextSectionPosition - 1) {
- return PINNED_HEADER_PUSHED_UP;
- }
- return PINNED_HEADER_VISIBLE;
- }
- @Override
- public void configurePinnedHeader(View header, int position, int alpha) {
- // TODO Auto-generated method stub
- int realPosition = position;
- int section = mIndexer.getSectionForPosition(realPosition);
- String title = (String) mIndexer.getSections()[section];
- ((TextView) header.findViewById(R.id.group_title)).setText(title);
- }
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {
- // TODO Auto-generated method stub
- }
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem,
- int visibleItemCount, int totalItemCount) {
- // TODO Auto-generated method stub
- if (view instanceof PinnedHeaderListView) {
- ((PinnedHeaderListView) view).configureHeaderView(firstVisibleItem);
- }
- }
- }
select_city_item.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" >
- <TextView
- android:id="@+id/group_title"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:background="@color/gray"
- android:gravity="left|center"
- android:paddingBottom="5.0dip"
- android:paddingLeft="@dimen/selectcity_group_item_padding"
- android:paddingRight="@dimen/selectcity_group_item_padding"
- android:paddingTop="5.0dip"
- android:text="S"
- android:textColor="@color/white"
- android:textStyle="bold" />
- <TextView
- android:id="@+id/city_name"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:minHeight="40.0dip"
- android:paddingLeft="@dimen/selectcity_group_item_padding"
- android:text="深圳"
- android:textColor="@color/black"
- android:textSize="15sp" />
- </LinearLayout>
主界面布局文件 activity_main.xml
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MainActivity" >
- <com.example.pinnedheaderlistviewdemo.view.PinnedHeaderListView
- android:id="@+id/mListView"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:cacheColorHint="@android:color/transparent"
- android:footerDividersEnabled="false"
- android:headerDividersEnabled="false" />
- <com.example.pinnedheaderlistviewdemo.view.BladeView
- android:id="@+id/mLetterListView"
- android:layout_width="30dp"
- android:layout_height="fill_parent"
- android:layout_alignParentRight="true"
- android:background="#00000000" />
- </RelativeLayout>
MainActivity.java
- package com.example.pinnedheaderlistviewdemo;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.InputStream;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.Comparator;
- import java.util.List;
- import android.app.Activity;
- import android.os.Bundle;
- import android.os.Environment;
- import android.os.Handler;
- import android.util.Log;
- import android.view.LayoutInflater;
- import com.example.pinnedheaderlistviewdemo.adapter.CityListAdapter;
- import com.example.pinnedheaderlistviewdemo.db.CityDao;
- import com.example.pinnedheaderlistviewdemo.db.DBHelper;
- import com.example.pinnedheaderlistviewdemo.view.BladeView;
- import com.example.pinnedheaderlistviewdemo.view.BladeView.OnItemClickListener;
- import com.example.pinnedheaderlistviewdemo.view.PinnedHeaderListView;
- public class MainActivity extends Activity {
- private static final int COPY_DB_SUCCESS = 10;
- private static final int COPY_DB_FAILED = 11;
- protected static final int QUERY_CITY_FINISH = 12;
- private MySectionIndexer mIndexer;
- private List<City> cityList = new ArrayList<City>();
- public static String APP_DIR = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test/";
- private Handler handler = new Handler(){
- public void handleMessage(android.os.Message msg) {
- switch (msg.what) {
- case QUERY_CITY_FINISH:
- if(mAdapter==null){
- mIndexer = new MySectionIndexer(sections, counts);
- mAdapter = new CityListAdapter(cityList, mIndexer, getApplicationContext());
- mListView.setAdapter(mAdapter);
- mListView.setOnScrollListener(mAdapter);
- //設置頂部固定頭部
- mListView.setPinnedHeaderView(LayoutInflater.from(getApplicationContext()).inflate(
- R.layout.list_group_item, mListView, false));
- }else if(mAdapter!=null){
- mAdapter.notifyDataSetChanged();
- }
- break;
- case COPY_DB_SUCCESS:
- requestData();
- break;
- default:
- break;
- }
- };
- };
- private DBHelper helper;
- private CityListAdapter mAdapter;
- private static final String ALL_CHARACTER = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;
- protected static final String TAG = null;
- private String[] sections = { "#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K",
- "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X",
- "Y", "Z" };
- private int[] counts;
- private PinnedHeaderListView mListView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- helper = new DBHelper();
- copyDBFile();
- findView();
- }
- private void copyDBFile() {
- File file = new File(APP_DIR+"/city.db");
- if(file.exists()){
- requestData();
- }else{ //拷贝文件
- Runnable task = new Runnable() {
- @Override
- public void run() {
- copyAssetsFile2SDCard("city.db");
- }
- };
- new Thread(task).start();
- }
- }
- /**
- * 拷贝资产目录下的文件到 手机
- */
- private void copyAssetsFile2SDCard(String fileName) {
- File desDir = new File(APP_DIR);
- if (!desDir.exists()) {
- desDir.mkdirs();
- }
- // 拷贝文件
- File file = new File(APP_DIR + fileName);
- if (file.exists()) {
- file.delete();
- }
- try {
- InputStream in = getAssets().open(fileName);
- FileOutputStream fos = new FileOutputStream(file);
- int len = -1;
- byte[] buf = new byte[1024];
- while ((len = in.read(buf)) > 0) {
- fos.write(buf, 0, len);
- }
- fos.flush();
- fos.close();
- handler.sendEmptyMessage(COPY_DB_SUCCESS);
- } catch (Exception e) {
- e.printStackTrace();
- handler.sendEmptyMessage(COPY_DB_FAILED);
- }
- }
- private void requestData() {
- Runnable task = new Runnable() {
- @Override
- public void run() {
- CityDao dao = new CityDao(helper);
- List<City> hot = dao.getHotCities(); //热门城市
- List<City> all = dao.getAllCities(); //全部城市
- if(all!=null){
- Collections.sort(all, new MyComparator()); //排序
- cityList.addAll(hot);
- cityList.addAll(all);
- //初始化每个字母有多少个item
- counts = new int[sections.length];
- counts[0] = hot.size(); //热门城市 个数
- for(City city : all){ //计算全部城市
- String firstCharacter = city.getSortKey();
- int index = ALL_CHARACTER.indexOf(firstCharacter);
- counts[index]++;
- }
- handler.sendEmptyMessage(QUERY_CITY_FINISH);
- }
- }
- };
- new Thread(task).start();
- }
- public class MyComparator implements Comparator<City> {
- @Override
- public int compare(City c1, City c2) {
- return c1.getSortKey().compareTo(c2.getSortKey());
- }
- }
- private void findView() {
- mListView = (PinnedHeaderListView) findViewById(R.id.mListView);
- BladeView mLetterListView = (BladeView) findViewById(R.id.mLetterListView);
- mLetterListView.setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(String s) {
- if(s!=null){
- int section = ALL_CHARACTER.indexOf(s);
- int position = mIndexer.getPositionForSection(section);
- Log.i(TAG, "s:"+s+",section:"+section+",position:"+position);
- if(position!=-1){
- mListView.setSelection(position);
- }else{
- }
- }
- }
- });
- }
- }
OK,就这么多了,另附工程源码下载地址:http://download.csdn.net/detail/fx_sky/5995355