之前我在『ArkUI AlphabetIndexer 真香』一文中提到,Android 平台也自带了一个 AlphabetIndexer
,不少 Android 开发者都表示没见过这玩意儿。
Android 中的字母索引器的 UI 其实应该叫 FastScroller
,即「快速滑动块」,而 AlphabetIndexer
这个类也确实存在,只不过它是作为辅助类来处理逻辑,FastScroller
才是处理滑块的显示。 因为使用起来不方便又或者是样式不符合国人使用习惯等原因,在国内的 App 中都不常见,我们可以先看看它的效果:
今天就来聊聊怎么实现,虽然日常基本不会使用,就当涨涨见识。
首先来看看布局,列表的子项就一个 TextView
,没什么好说的。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="16sp"
android:textStyle="bold" />
页面则使用一个 ListView
来做展示。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_purple"
app:title="Contacts" />
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fastScrollEnabled="true"
tools:listitem="@layout/item_contact" />
</LinearLayout>
注意这里要配置 android:fastScrollEnabled
属性。
不使用 RecyclerView
的原因是它还需要配置其他属性,相对来说更加复杂,后续后机会再单独介绍。
接下来编写适配器:
class ContactAdapter(
context: Context,
cursor: Cursor,
data: List<Map<String, String>>,
@LayoutRes resource: Int,
from: Array<String>,
@IdRes to: IntArray
) : SimpleAdapter(context, data, resource, from, to), SectionIndexer {
private val indexer = AlphabetIndexer(cursor, 0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
override fun getSections(): Array<Any> {
return indexer.sections
}
override fun getPositionForSection(sectionIndex: Int): Int {
return indexer.getPositionForSection(sectionIndex)
}
override fun getSectionForPosition(position: Int): Int {
return indexer.getSectionForPosition(position)
}
}
前面的布局中可以看到样式十分简单,所以这里直接继承 SimpleAdapter
,同时实现 SectionIndexer
,这里十分关键。
构建 AlphabetIndexer
,需要用到 Cursor
,我们从上一层传入,并将 26 个字母作为索引。
AlphabetIndexer
本身也是 SectionIndexer
的子类,所以在我们实现的 SectionIndexer
的接口中调用 AlphabetIndexer
的对应方法返回。
提到 Cursor
,我们最先想到可以直接查询本地通讯录的数据,它会给我们返回一个 Cursor
对象,就不用我们手动封装数据源了:
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
arrayOf(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY),
null,
null,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY
)
startManagingCursor(cursor)
cursor?.let {
val list = getData(it)
val adapter = ContactAdapter(
this,
it,
list,
R.layout.item_contact,
arrayOf(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY),
intArrayOf(R.id.tv_contact)
)
binding.listView.adapter = adapter
}
}
private fun getData(cursor: Cursor): List<Map<String, String>> {
val list = mutableListOf<Map<String, String>>()
if (cursor.moveToFirst()) {
do {
val column = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
val name = cursor.getString(column)
list.add(mapOf(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY to name))
} while (cursor.moveToNext())
}
return list
}
}
Cursor
数据取出 ContactsContract.Contacts.DISPLAY_NAME_PRIMARY
这一列用于组装子项,再将 Cursor
传入刚刚的 ContactAdapter
中构建 AlphabetIndexer
。
获取联系人还需要动态申请权限,这部分代码没有给出,自行处理即可。
有些朋友敲完之后可能会发现侧边的索引器出不来,其实不是代码的问题,只是数据量不够导致的,你可以配置 android:fastScrollAlwaysVisible
属性让它保持显示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout ...>
...
<ListView ...
android:fastScrollAlwaysVisible="true"
android:fastScrollEnabled="true" />
</LinearLayout>
如果不配置该属性,默认情况下只有当内容大于 4 页时,ListView
才会显示索引器,我们可以在源码中得出这个结论。
ListView
的父类 AbsListView
的构造方法中有 android:fastScrollEnabled
这个属性的配置:
public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
ViewTreeObserver.OnTouchModeChangeListener,
RemoteViewsAdapter.RemoteAdapterConnectionCallback {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768941)
private FastScroller mFastScroll;
public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
...
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AbsListView, defStyleAttr, defStyleRes);
setFastScrollEnabled(a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false));
setFastScrollStyle(a.getResourceId(R.styleable.AbsListView_fastScrollStyle, 0));
setFastScrollAlwaysVisible(a.getBoolean(R.styleable.AbsListView_fastScrollAlwaysVisible, false));
...
}
public void setFastScrollEnabled(final boolean enabled) {
if (mFastScrollEnabled != enabled) {
mFastScrollEnabled = enabled;
if (isOwnerThread()) {
setFastScrollerEnabledUiThread(enabled);
} else {
post(new Runnable() {
@Override
public void run() {
setFastScrollerEnabledUiThread(enabled);
}
});
}
}
}
private void setFastScrollerEnabledUiThread(boolean enabled) {
if (mFastScroll != null) {
mFastScroll.setEnabled(enabled);
} else if (enabled) {
mFastScroll = new FastScroller(this, mFastScrollStyle);
mFastScroll.setEnabled(true);
}
resolvePadding();
if (mFastScroll != null) {
mFastScroll.updateLayout();
}
}
}
可以看到 ListView
的快速滑动块是通过 FastScroller
类实现的,接下来看看 FastScroller
的创建流程:
/**
* Helper class for AbsListView to draw and control the Fast Scroll thumb
*/
class FastScroller {
/** Minimum number of pages to justify showing a fast scroll thumb. */
private static final int MIN_PAGES = 4;
/**
* @return Whether the fast scroll thumb is enabled.
*/
public boolean isEnabled() {
return mEnabled && (mLongList || mAlwaysShow);
}
/**
* Called when one of the variables affecting enabled state changes.
*
* @param peekIfEnabled whether the thumb should peek, if enabled
*/
private void onStateDependencyChanged(boolean peekIfEnabled) {
if (isEnabled()) {
if (isAlwaysShowEnabled()) {
setState(STATE_VISIBLE);
} else if (mState == STATE_VISIBLE) {
postAutoHide();
} else if (peekIfEnabled) {
setState(STATE_VISIBLE);
postAutoHide();
}
} else {
stop();
}
mList.resolvePadding();
}
private void updateLongList(int childCount, int itemCount) {
final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
if (mLongList != longList) {
mLongList = longList;
onStateDependencyChanged(false);
}
}
...
}
不难理解,当开启 android:fastScrollEnabled
配置时,有两种条件可以显示快速滑动块,一是长列表,二是 android:fastScrollAlwaysVisible
也为 true
,长列表的判断条件是数据量大于等于 MIN_PAGES
,即 4
页。
以上就是 Android 字母索引的简单实现,关于自定义等其他用法就不展开介绍了,毕竟使用率不高,以后有机会再聊。