标题是基于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
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
实体类的每个属性的含义我都中文标注了。
中文转换拼音使用的是TinyPinyin
,适用于Java和Android的快速、低内存占用的汉字转拼音库。 TinyPinyin的特点有:
/**
* 如果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 = 0; i < character.length(); i++) {
buffer.append(Pinyin.toPinyin(character.charAt(i)));
}
return buffer.toString();
}
我们看一下Collections
类中关于sort
方法的API
文档说明:
public static super T>> void sort(List 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
的方式去实现。
这里自定义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 = 0; i < mDatas.size(); i++) {
if (i == selectorPosition) {
mPaint.setColor(Color.GREEN);
canvas.drawCircle(mWidth / 2, i * mTextHeight + mTextHeight / 2 - dip2px(1), dip2px(8), mCirclePaint);
} else {
mPaint.setColor(Color.WHITE);
}
mPaint.setTextSize(dip2px(15));
mFontMetrics = mPaint.getFontMetrics();
canvas.drawText(mDatas.get(i), mWidth / 2, i * mTextHeight + mTextHeight / 2 + 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() - 1;
} else if (selectorPosition <= 0) {
selectorPosition = 0;
}
if (mOnTouchListener != null) {
mOnTouchListener.onTouchListener(mDatas.get(selectorPosition), false);
}
invalidate();
}
if
语句是防止触摸到控件以外的点,造成数据越界异常。这里不是调用了一个接口,干什么用的?mOnTouchListener
接口主要是控制视图中间大写字母的显示和隐藏的。最后来看一看快速导航的效果图:
先来看xml
布局:
<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(this, mDatas, R.layout.rv_item_city) {
@Override
protected void convert(BaseViewHolder helper, final City item) {
}
});
运行的效果图如下:
不用我说,接下来就是去重。
if (helper.getAdapterPosition() == 0) {
helper.setVisible(R.id.tv_letter_header, true);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - 1).firstPinYin)) {
helper.setVisible(R.id.tv_letter_header, false);
} else {
helper.setVisible(R.id.tv_letter_header, true);
}
}
采用的是集合上一条数据和下一条数据比较,如果相同则隐藏,反正显示。当然你也可以在实体类添加字段处理,这种方式交给你们自己去实现。
接着处理点击字母列表项,实现隐藏和显示该字母项下所有的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);
}
效果图:
在讲解滑动悬浮功能的时候,需要事先了解下recyclerView.findChildViewUnde
方法,来看看这个方法的一个实现:
public View findChildViewUnder(float x, float y) {
final int count = mChildHelper.getChildCount();
for (int i = count - 1; i >= 0; 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() - 1);
//判断必须加上,防止返回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() == 0) {
helper.itemView.setTag(HEADER_FIRST_VIEW);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - 1).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() > 0) {
tvLetterHeader.setTranslationY(headerMoveY);
} else {
tvLetterHeader.setTranslationY(0);
}
} else {
tvLetterHeader.setTranslationY(0);
}
}
动态的获取顶部悬浮字母栏向上平移的距离transView.getTop() - tvLetterHeader.getMeasuredHeight();
,if (tag == HEADER_VISIBLE_VIEW)
判断何时平移。
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(0, top);
} else {
//当要置顶的项在当前显示的最后一项的后面时
mRecyclerView.scrollToPosition(n);
//这里这个变量是用在RecyclerView滚动监听里面的
move = true;
}
然后在RecyclerView
滚动监听:
if (move) {
move = false;
//获取要置顶的项在当前屏幕的位置,selectPosition 是记录的要置顶项在RecyclerView中的位置
int n = selectPosition - mLinearLayoutManager.findFirstVisibleItemPosition();
if (0 <= n && n < mRecyclerView.getChildCount()) {
//获取要置顶的项顶部离RecyclerView顶部的距离
int top = mRecyclerView.getChildAt(n).getTop();
//最后的移动
mRecyclerView.scrollBy(0, top);
}
}
最后在mNavigationView
的接口setOnTouchListener
当中调用该方法:
mNavigationView.setOnTouchListener(new LetterNavigationView.OnTouchListener() {
@Override
public void onTouchListener(String str, boolean hideEnable) {
for (int i = 0; i < mDatas.size(); i++) {
if (mDatas.get(i).firstPinYin.equals(str)) {
selectPosition = i;
break;
}
}
moveToPosition(selectPosition);
}
});
我们在addOnScrollListener
,获取当前的索引值来动态刷新字母导航栏:
String tvLetterStr = tvLetter.getText().toString().trim();
String tvHeaderStr = tvLetterHeader.getText().toString().trim();
if (!tvHeaderStr.equals(tvLetterStr)) {
for (int i = 0; i < mLetterDatas.size(); i++) {
if (tvLetterStr.equals(mLetterDatas.get(i))) {
mNavigationView.setSelectorPosition(i);
break;
}
}
}
功能齐全的城市,成员导航就实现了。如果对你有所帮助,还望 github 给 star 。