今天看啥  ›  专栏  ›  郭霖

Android系统联系人全特效实现(下),字母表快速滚动

郭霖  · 掘金  ·  · 2021-05-11 17:58
阅读 29

Android系统联系人全特效实现(下),字母表快速滚动

在上一篇文章中,我和大家一起实现了类似于 Android 系统联系人的分组导航和挤压动画功能,不过既然文章名叫做《Android 系统联系人全特效实现》,那么没有快速滚动功能显然是称不上 "全" 的。因此本篇文章我将带领大家在上篇文章的代码基础上改进,加入快速滚动功能。

如果还没有看过我上一篇文章,请抓紧去阅读一下 Android 系统联系人全特效实现 (上),分组导航和挤压动画 。

其实 ListView 本身是有一个快速滚动属性的,可以通过在 XML 中设置 android:fastScrollEnabled="true" 来启用。包括以前老版本的 Android 联系人中都是使用这种方式来进行快速滚动的。效果如下图所示:

不过这种快速滚动方式比较丑陋,到后来很多手机厂商在定制自己 ROM 的时候都将默认快速滚动改成了类似 iPhone 上 A-Z 字母表快速滚动的方式。这里我们怎么能落后于时代的潮流呢!我们的快速滚动也要使用 A-Z 字母表的方式!下面就来开始实现。

首先打开上次的 ContactsDemo 工程,修改 activity_main.xml 布局文件。由于我们要在界面上加入字母表,因此我们需要一个 Button,将这个 Button 的背景设为一张 A-Z 排序的图片,然后居右对齐。另外还需要一个 TextView,用于在弹出式分组布局上显示当前的分组,默认是 gone 掉的,只有手指在字母表上滑动时才让它显示出来。修改后的布局文件代码如下:

<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"
    android:orientation="vertical" >
 
    <ListView
        android:id="@+id/contacts_list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:scrollbars="none"
        android:fadingEdge="none" >
    </ListView>
 
    <LinearLayout
        android:id="@+id/title_layout"
        android:layout_width="fill_parent"
        android:layout_height="18dip"
        android:layout_alignParentTop="true"
        android:background="#303030" >
 
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginLeft="10dip"
            android:textColor="#ffffff"
            android:textSize="13sp" />
    </LinearLayout>
    
    <Button 
        android:id="@+id/alphabetButton"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_alignParentRight="true"
        android:background="@drawable/a_z"
        />
    
    <RelativeLayout 
        android:id="@+id/section_toast_layout"
        android:layout_width="70dip"
        android:layout_height="70dip"
        android:layout_centerInParent="true"
        android:background="@drawable/section_toast"
        android:visibility="gone"
        >
        <TextView 
            android:id="@+id/section_toast_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="30sp"
            />
    </RelativeLayout>
 
</RelativeLayout>
复制代码

然后打开 MainActivity 进行修改,毫无疑问,我们需要对字母表按钮的 touch 事件进行监听,于是在 MainActivity 中新增如下代码:

private void setAlpabetListener() {
	alphabetButton.setOnTouchListener(new OnTouchListener() {
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			float alphabetHeight = alphabetButton.getHeight();
			float y = event.getY();
			int sectionPosition = (int) ((y / alphabetHeight) / (1f / 27f));
			if (sectionPosition < 0) {
				sectionPosition = 0;
			} else if (sectionPosition > 26) {
				sectionPosition = 26;
			}
			String sectionLetter = String.valueOf(alphabet.charAt(sectionPosition));
			int position = indexer.getPositionForSection(sectionPosition);
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				alphabetButton.setBackgroundResource(R.drawable.a_z_click);
				sectionToastLayout.setVisibility(View.VISIBLE);
				sectionToastText.setText(sectionLetter);
				contactsListView.setSelection(position);
				break;
			case MotionEvent.ACTION_MOVE:
				sectionToastText.setText(sectionLetter);
				contactsListView.setSelection(position);
				break;
			default:
				alphabetButton.setBackgroundResource(R.drawable.a_z);
				sectionToastLayout.setVisibility(View.GONE);
			}
			return true;
		}
	});
}
复制代码

可以看到,在这个方法中我们注册了字母表按钮的 onTouch 事件,然后在 onTouch 方法里做了一些逻辑判断和处理,下面我来一一详细说明。首先通过字母表按钮的 getHeight 方法获取到字母表的总高度,然后用 event.getY 方法获取到目前手指在字母表上的纵坐标,用纵坐标除以总高度就可以得到一个用小数表示的当前手指所在位置 (0 表在 #端,1 表示在 Z 端)。由于我们的字母表中一共有 27 个字符,再用刚刚算出的小数再除以 1/27 就可以得到一个 0 到 27 范围内的浮点数,之后再把这个浮点数向下取整,就可以算出我们当前按在哪个字母上了。然后再对 event 的 action 进行判断,如果是 ACTION_DOWN 或 ACTION_MOVE,就在弹出式分组上显示当前手指所按的字母,并调用 ListView 的 setSelection 方法把列表滚动到相应的分组。如果是其它的 action,就将弹出式分组布局隐藏。

MainActivity 的完整代码如下:

public class MainActivity extends Activity {
 
	/**
	 * 分组的布局
	 */
	private LinearLayout titleLayout;
 
	/**
	 * 弹出式分组的布局
	 */
	private RelativeLayout sectionToastLayout;
 
	/**
	 * 右侧可滑动字母表
	 */
	private Button alphabetButton;
 
	/**
	 * 分组上显示的字母
	 */
	private TextView title;
 
	/**
	 * 弹出式分组上的文字
	 */
	private TextView sectionToastText;
 
	/**
	 * 联系人ListView
	 */
	private ListView contactsListView;
 
	/**
	 * 联系人列表适配器
	 */
	private ContactAdapter adapter;
 
	/**
	 * 用于进行字母表分组
	 */
	private AlphabetIndexer indexer;
 
	/**
	 * 存储所有手机中的联系人
	 */
	private List<Contact> contacts = new ArrayList<Contact>();
 
	/**
	 * 定义字母表的排序规则
	 */
	private String alphabet = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 
	/**
	 * 上次第一个可见元素,用于滚动时记录标识。
	 */
	private int lastFirstVisibleItem = -1;
 
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		adapter = new ContactAdapter(this, R.layout.contact_item, contacts);
		titleLayout = (LinearLayout) findViewById(R.id.title_layout);
		sectionToastLayout = (RelativeLayout) findViewById(R.id.section_toast_layout);
		title = (TextView) findViewById(R.id.title);
		sectionToastText = (TextView) findViewById(R.id.section_toast_text);
		alphabetButton = (Button) findViewById(R.id.alphabetButton);
		contactsListView = (ListView) findViewById(R.id.contacts_list_view);
		Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
		Cursor cursor = getContentResolver().query(uri,
				new String[] { "display_name", "sort_key" }, null, null, "sort_key");
		if (cursor.moveToFirst()) {
			do {
				String name = cursor.getString(0);
				String sortKey = getSortKey(cursor.getString(1));
				Contact contact = new Contact();
				contact.setName(name);
				contact.setSortKey(sortKey);
				contacts.add(contact);
			} while (cursor.moveToNext());
		}
		startManagingCursor(cursor);
		indexer = new AlphabetIndexer(cursor, 1, alphabet);
		adapter.setIndexer(indexer);
		if (contacts.size() > 0) {
			setupContactsListView();
			setAlpabetListener();
		}
	}
 
	/**
	 * 为联系人ListView设置监听事件,根据当前的滑动状态来改变分组的显示位置,从而实现挤压动画的效果。
	 */
	private void setupContactsListView() {
		contactsListView.setAdapter(adapter);
		contactsListView.setOnScrollListener(new OnScrollListener() {
			@Override
			public void onScrollStateChanged(AbsListView view, int scrollState) {
			}
 
			@Override
			public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
					int totalItemCount) {
				int section = indexer.getSectionForPosition(firstVisibleItem);
				int nextSecPosition = indexer.getPositionForSection(section + 1);
				if (firstVisibleItem != lastFirstVisibleItem) {
					MarginLayoutParams params = (MarginLayoutParams) titleLayout.getLayoutParams();
					params.topMargin = 0;
					titleLayout.setLayoutParams(params);
					title.setText(String.valueOf(alphabet.charAt(section)));
				}
				if (nextSecPosition == firstVisibleItem + 1) {
					View childView = view.getChildAt(0);
					if (childView != null) {
						int titleHeight = titleLayout.getHeight();
						int bottom = childView.getBottom();
						MarginLayoutParams params = (MarginLayoutParams) titleLayout
								.getLayoutParams();
						if (bottom < titleHeight) {
							float pushedDistance = bottom - titleHeight;
							params.topMargin = (int) pushedDistance;
							titleLayout.setLayoutParams(params);
						} else {
							if (params.topMargin != 0) {
								params.topMargin = 0;
								titleLayout.setLayoutParams(params);
							}
						}
					}
				}
				lastFirstVisibleItem = firstVisibleItem;
			}
		});
 
	}
 
	/**
	 * 设置字母表上的触摸事件,根据当前触摸的位置结合字母表的高度,计算出当前触摸在哪个字母上。
	 * 当手指按在字母表上时,展示弹出式分组。手指离开字母表时,将弹出式分组隐藏。
	 */
	private void setAlpabetListener() {
		alphabetButton.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				float alphabetHeight = alphabetButton.getHeight();
				float y = event.getY();
				int sectionPosition = (int) ((y / alphabetHeight) / (1f / 27f));
				if (sectionPosition < 0) {
					sectionPosition = 0;
				} else if (sectionPosition > 26) {
					sectionPosition = 26;
				}
				String sectionLetter = String.valueOf(alphabet.charAt(sectionPosition));
				int position = indexer.getPositionForSection(sectionPosition);
				switch (event.getAction()) {
				case MotionEvent.ACTION_DOWN:
					alphabetButton.setBackgroundResource(R.drawable.a_z_click);
					sectionToastLayout.setVisibility(View.VISIBLE);
					sectionToastText.setText(sectionLetter);
					contactsListView.setSelection(position);
					break;
				case MotionEvent.ACTION_MOVE:
					sectionToastText.setText(sectionLetter);
					contactsListView.setSelection(position);
					break;
				default:
					alphabetButton.setBackgroundResource(R.drawable.a_z);
					sectionToastLayout.setVisibility(View.GONE);
				}
				return true;
			}
		});
	}
 
	/**
	 * 获取sort key的首个字符,如果是英文字母就直接返回,否则返回#。
	 * 
	 * @param sortKeyString
	 *            数据库中读取出的sort key
	 * @return 英文字母或者#
	 */
	private String getSortKey(String sortKeyString) {
		alphabetButton.getHeight();
		String key = sortKeyString.substring(0, 1).toUpperCase();
		if (key.matches("[A-Z]")) {
			return key;
		}
		return "#";
	}
 
}
复制代码

好了,就改动了以上两处,其它文件都保持不变,让我们来运行一下看看效果:

非常不错!当你的手指在右侧字母表上滑动时,联系人的列表也跟着相应的变动,并在屏幕中央显示一个当前的分组。

现在让我们回数一下,分组导航、挤压动画、字母表快速滚动,Android 系统联系人全特效都实现了。好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里

关注我的技术公众号“郭霖”,优质技术文章推送。




原文地址:访问原文地址
快照地址: 访问文章快照