标题是基于RecyclerView通用适配打造城市,成员导航列表,这里的通用适配是我发表的上一篇博客RecyclerView 之通用适配,导航列表具有以下特点:
- RecyclerView通用适配所有的特效
- 顶部悬浮标题栏
- 按字母索引
- 隐藏,展开字母列表项
- 快速定位
来张效果图,来帮助我们理解:

以上的需求基本可以满足城市,成员等导航列表,事先我了解了一下市面上导航列表,总感觉功能不是很齐全,大部分都是基于ListView的,今天我带给大家基于RecyclerView简单易懂的导航列表,心动就跟我一起行动。
依赖
请在 build.gradle文件的 dependencies节点中添加:
compile 'com.github.baserecycleradapter:library:1.1.0'
compile 'com.github.promeg:tinypinyin:1.0.0' // ~80KB
导航列表
1、构建实体类
package entity;
/**
* Created by Administrator on 8/10 0010.
*/
public class City {
//城市名称拼音
public String cityPinYin;
//城市名称
public String cityName;
//拼音首字母
public String firstPinYin;
//隐藏,展开字母列表项
public boolean hideEnable;
}
City
实体类的每个属性的含义我都中文标注了。
2、中文转拼音
中文转换拼音使用的是
TinyPinyin
,适用于Java和Android的快速、低内存占用的汉字转拼音库。 TinyPinyin的特点有:
- 生成的拼音不包含声调,也不处理多音字,默认一个汉字对应一个拼音;
- 拼音均为大写;
- 无需初始化,执行效率很高(Pinyin4J的4倍);
- 很低的内存占用(小于30KB)。
/**
* 如果c为汉字,则返回大写拼音;如果c不是汉字,则返回String.valueOf(c)
*/
String Pinyin.toPinyin(char c)
/**
* c为汉字,则返回true,否则返回false
*/
boolean Pinyin.isChinese(char c)
这里主要是用到
Pinyin.toPinyin方法
:
public static String transformPinYin(String character) {
StringBuffer buffer = new StringBuffer();
for (int i = ; i < character.length(); i++) {
buffer.append(Pinyin.toPinyin(character.charAt(i)));
}
return buffer.toString();
}
3、根据拼音进行排序
我们看一下
Collections
类中关于
sort
方法的
API
文档说明:
public static <T extends Comparable<? super T>> void sort(List<T> list)
该方法要说明的就是要调用Collections的sort()方法,则必须让集合中的元素实现Comparable接口:
public class PinYinComparator implements Comparator<City> {
@Override
public int compare(City city, City t1) {
return city.cityPinYin.compareTo(t1.cityPinYin);
}
}
mDatas
为排序的集合,使用如下:
Collections.sort(mDatas, new PinYinComparator());
到这里准备工作就做得差不多了,通过分析效果图,最右边的字母导航栏,最开始我的想法是也用
recyclerView
来实现,但是在触摸移动会频繁的调用适配刷新
notifyDataSetChanged();
,最后我放弃了使用
recyclerView
去实现快速导航,采用了自定义
View
的方式去实现。
4、LetterNavigationView(快速导航栏)
这里自定义
View
的基础知识我都不再讲解了,不懂的同学请点击以下链接:
http://blog.csdn.net/u012551350/article/details/51323986
我们先来看看
onSizeChanged
方法:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
if (!mDatas.isEmpty()) {
mTextHeight = (mHeight / mDatas.size());
}
}
mDatas
是个字符串集合,这里指的是字母集合。
mWidth
表示的是整个
View
的宽度,同理
mHeight
为高度。
mTextHeight
表示每个字母所占矩形的高度。命名可能不是很规范,还请见解。
接着来看
onDraw
方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = ; i < mDatas.size(); i++) {
if (i == selectorPosition) {
mPaint.setColor(Color.GREEN);
canvas.drawCircle(mWidth / , i * mTextHeight + mTextHeight / - dip2px(), dip2px(), mCirclePaint);
} else {
mPaint.setColor(Color.WHITE);
}
mPaint.setTextSize(dip2px());
mFontMetrics = mPaint.getFontMetrics();
canvas.drawText(mDatas.get(i), mWidth / , i * mTextHeight + mTextHeight / + mFontMetrics.bottom, mPaint);
}
}
onDraw
的方法也比较简单,
selectorPosition
表示当前字母索引。首先对字母集合的一个遍历,判断当前的索引,更换画笔颜色,绘制索引字母圆形背景,最后绘制字母。记得添加
mPaint.setTextAlign(Paint.Align.CENTER);
文本对齐方式。利用
baselineY=mHeight/2+fm.bottom
公式得到
baselineY
的坐标值,不理解的请点击以下链接:
http://blog.csdn.net/u012551350/article/details/51361778
来看看效果图:
我们还有个功能就是通过触摸来动态改变字母的索引,这个功能我们又怎么来实现呢?
最后我们重写
onTouchEvent
方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
changePosition(y);
break;
case MotionEvent.ACTION_MOVE:
changePosition(y);
break;
case MotionEvent.ACTION_UP:
if (mOnTouchListener != null) {
mOnTouchListener.onTouchListener(mDatas.get(selectorPosition), true);
}
break;
}
return true;
我们在
ACTION_DOWN
,
ACTION_MOVE
根据当前
y
来获取索引值
selectorPosition
,并且刷新
View
,来看看
changePosition
方法:
private void changePosition(int y) {
selectorPosition = y / (mHeight / mDatas.size());
if (selectorPosition >= mDatas.size()) {
selectorPosition = mDatas.size() - ;
} else if (selectorPosition <= ) {
selectorPosition = ;
}
if (mOnTouchListener != null) {
mOnTouchListener.onTouchListener(mDatas.get(selectorPosition), false);
}
invalidate();
}
if
语句是防止触摸到控件以外的点,造成数据越界异常。这里不是调用了一个接口,干什么用的?
mOnTouchListener
接口主要是控制视图中间大写字母的显示和隐藏的。最后来看一看快速导航的效果图:
5、NavigationActivity(导航控件)
先来看
xml
布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:background="#c3c9ce">
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.design.widget.CoordinatorLayout>
<include
layout="@layout/rv_letter_header"></include>
<widget.LetterNavigationView
android:id="@+id/navigation"
android:layout_width="48dp"
android:layout_height="match_parent"
android:layout_gravity="end"/>
<TextView
android:id="@+id/tv_letter_hide"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center"
android:background="@drawable/letter_circle_bg"
android:gravity="center"
android:text="A"
android:textColor="#FFF"
android:textSize="32sp"
android:visibility="gone"/>
</FrameLayout>
rv_letter_header
悬浮的头部控件。目前导航是展示在
NavigationActivity
中的,后期我会封装成一个控件以方便大家使用,通用适配的使用方式我在这里也就不再讲解了,不了解的请点击以下链接:
http://blog.csdn.net/u012551350/article/details/52026740
mRecyclerView加载数据:
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setLayoutManager(mLinearLayoutManager = new LinearLayoutManager(this));
mRecyclerView.setAdapter(mAdapter = new BaseRecyclerAdapter<City>(this, mDatas, R.layout.rv_item_city) {
@Override
protected void convert(BaseViewHolder helper, final City item) {
}
});
运行的效果图如下:
a、去重
不用我说,接下来就是去重。
if (helper.getAdapterPosition() == ) {
helper.setVisible(R.id.tv_letter_header, true);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - ).firstPinYin)) {
helper.setVisible(R.id.tv_letter_header, false);
} else {
helper.setVisible(R.id.tv_letter_header, true);
}
}
采用的是集合上一条数据和下一条数据比较,如果相同则隐藏,反正显示。当然你也可以在实体类添加字段处理,这种方式交给你们自己去实现。
b、列表项隐藏,显示
接着处理点击字母列表项,实现隐藏和显示该字母项下所有的
item
。这里就是实体添加字段来处理的,具体看代码:
helper.setOnClickListener(R.id.tv_letter_header, new View.OnClickListener() {
@Override
public void onClick(View view) {
for (City city : mDatas) {
if (city.firstPinYin.equals(item.firstPinYin)) {
city.hideEnable = !city.hideEnable;
}
}
notifyDataSetChanged();
}
});
点击字母项就改变属于该字母项下面的所有的
hideEnable
值,然后刷新适配器
notifyDataSetChanged
。
在
convert
,实现显示与隐藏:
if (item.hideEnable) {
helper.setVisible(R.id.tv_city, false);
} else {
helper.setVisible(R.id.tv_city, true);
}
效果图:
c、顶部悬浮字母
在讲解滑动悬浮功能的时候,需要事先了解下
recyclerView.findChildViewUnde
方法,来看看这个方法的一个实现:
public View findChildViewUnder(float x, float y) {
final int count = mChildHelper.getChildCount();
for (int i = count - ; i >= ; i--) {
final View child = mChildHelper.getChildAt(i);
final float translationX = ViewCompat.getTranslationX(child);
final float translationY = ViewCompat.getTranslationY(child);
if (x >= child.getLeft() + translationX &&
x <= child.getRight() + translationX &&
y >= child.getTop() + translationY &&
y <= child.getBottom() + translationY) {
return child;
}
}
return null;
}
大概的一个意思是说,返回(x,y)点以下的子视图,如果没有就返回 null ,有了这个方法,我们实现悬浮的字母列随着滑动动态变化了:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//获取tvLetterHeader下的子视图
View transView = recyclerView.findChildViewUnder(
tvLetterHeader.getMeasuredWidth(), tvLetterHeader.getMeasuredHeight() - );
//判断必须加上,防止返回null
if (transView != null) {
TextView tvLetter = (TextView) transView.findViewById(R.id.tv_letter_header);
if (tvLetter != null) {
String tvLetterStr = tvLetter.getText().toString().trim();
String tvHeaderStr = tvLetterHeader.getText().toString().trim();
tvLetterHeader.setText(tvLetterStr);
}
}
}
怎么才能实现下一个字母栏顶掉上一个字母栏呢?那就要用到平移了
setTranslationY
,具体来看看是怎么实现的:
if (helper.getAdapterPosition() == ) {
helper.itemView.setTag(HEADER_FIRST_VIEW);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - ).firstPinYin)) {
helper.itemView.setTag(HEADER_NONE_VIEW);
} else {
helper.itemView.setTag(HEADER_VISIBLE_VIEW);
}
}
分别给第一项,带字母栏的列,与不带字母栏的列设置
Tag
,接着我们在
addOnScrollListener
滚动监听中处理:
if (transView.getTag() != null) {
int headerMoveY = transView.getTop() - tvLetterHeader.getMeasuredHeight();
int tag = (int) transView.getTag();
if (tag == HEADER_VISIBLE_VIEW) {
if (transView.getTop() > ) {
tvLetterHeader.setTranslationY(headerMoveY);
} else {
tvLetterHeader.setTranslationY();
}
} else {
tvLetterHeader.setTranslationY();
}
}
动态的获取顶部悬浮字母栏向上平移的距离
transView.getTop() - tvLetterHeader.getMeasuredHeight();
,
if (tag == HEADER_VISIBLE_VIEW)
判断何时平移。
d、触摸字母导航栏,定位这一项,将它显示在顶部。
RecyclerView
用于控制移动的方法有如下几个:
- scrollToPosition 显示指定项,就是把你想置顶的项显示出来,但是在屏幕的什么位置是不管的,只要那一项现在看得到了,那它就罢工了。
- scrollBy 控制移动的距离,单位像素
- smoothScrollToPosition ,smoothScrollBy 多了滑动效果
这几个方法都不能很好解决问题,但是当
scrollToPosition
+
scrollBy
结合使用的时候,我们的问题就变的好解决了,思路是:先用scrollToPosition,将要置顶的项先移动显示出来,然后计算这一项离顶部的距离,用scrollBy完成最后的移动。
先传入要置顶第几项,然后区分情况处理:
private void moveToPosition(int n) {
//先从RecyclerView的LayoutManager中获取第一项和最后一项的Position
int firstItem = mLinearLayoutManager.findFirstVisibleItemPosition();
int lastItem = mLinearLayoutManager.findLastVisibleItemPosition();
//然后区分情况
if (n <= firstItem) {
//当要置顶的项在当前显示的第一个项的前面时
mRecyclerView.scrollToPosition(n);
} else if (n <= lastItem) {
//当要置顶的项已经在屏幕上显示时
int top = mRecyclerView.getChildAt(n - firstItem).getTop();
mRecyclerView.scrollBy(, top);
} else {
//当要置顶的项在当前显示的最后一项的后面时
mRecyclerView.scrollToPosition(n);
//这里这个变量是用在RecyclerView滚动监听里面的
move = true;
}
然后在
RecyclerView
滚动监听:
if (move) {
move = false;
//获取要置顶的项在当前屏幕的位置,selectPosition 是记录的要置顶项在RecyclerView中的位置
int n = selectPosition - mLinearLayoutManager.findFirstVisibleItemPosition();
if ( <= n && n < mRecyclerView.getChildCount()) {
//获取要置顶的项顶部离RecyclerView顶部的距离
int top = mRecyclerView.getChildAt(n).getTop();
//最后的移动
mRecyclerView.scrollBy(, top);
}
}
最后在
mNavigationView
的接口
setOnTouchListener
当中调用该方法:
mNavigationView.setOnTouchListener(new LetterNavigationView.OnTouchListener() {
@Override
public void onTouchListener(String str, boolean hideEnable) {
for (int i = ; i < mDatas.size(); i++) {
if (mDatas.get(i).firstPinYin.equals(str)) {
selectPosition = i;
break;
}
}
moveToPosition(selectPosition);
}
});
e、滑动改变字母导航栏的索引
我们在
addOnScrollListener
,获取当前的索引值来动态刷新字母导航栏:
String tvLetterStr = tvLetter.getText().toString().trim();
String tvHeaderStr = tvLetterHeader.getText().toString().trim();
if (!tvHeaderStr.equals(tvLetterStr)) {
for (int i = ; i < mLetterDatas.size(); i++) {
if (tvLetterStr.equals(mLetterDatas.get(i))) {
mNavigationView.setSelectorPosition(i);
break;
}
}
}
功能齐全的城市,成员导航就实现了。如果对你有所帮助,还望 github 给 star 。