基于RecyclerView通用适配打造城市,成员导航列表
来源:互联网 发布:淘宝规则重复开店 编辑:程序博客网 时间:2024/05/09 02:54
标题是基于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 = 0; 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
方法:
@Overrideprotected 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
方法:
@Overrideprotected 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
方法:
@Overridepublic 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
接口主要是控制视图中间大写字母的显示和隐藏的。最后来看一看快速导航的效果图:
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() == 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); }}
采用的是集合上一条数据和下一条数据比较,如果相同则隐藏,反正显示。当然你也可以在实体类添加字段处理,这种方式交给你们自己去实现。
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 - 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)
判断何时平移。
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(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); }});
e、滑动改变字母导航栏的索引
我们在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 。
- 基于RecyclerView通用适配打造城市,成员导航列表
- Android开发之基于RecyclerView通用适配打造城市导航列表
- RecyclerView学习(四)----城市导航列表的实现(上)
- RecyclerView学习(四)----ItemDecoration实现的城市导航列表(下)
- 基于RecyclerView通用适配自定义下拉刷新
- 打造RecyclerView的通用适配器
- 打造 RecyclerView的 通用Adapter
- 为RecyclerView打造通用adapter
- 为RecyclerView打造通用Adapter
- 为RecyclerView打造通用Adapter
- 【Android 仿微信通讯录 导航分组列表-下】自定义View为RecyclerView打造右侧索引导航栏IndexBar
- RecyclerView 之通用适配
- RecyclerView 通用适配 BaseQuickAdapter
- RecyclerView 通用适配 BaseQuickAdapter
- RecyclerView 之通用适配
- RecyclerView 之通用适配
- 【Android 仿微信通讯录 导航分组列表-上】使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
- Android 仿微信通讯录 导航分组列表-上】使用ItemDecoration为RecyclerView打造带悬停头部的分组列表
- Mysql索引优化
- ArrayList源代码的分析
- Effective C++_Item17笔记
- CodeForces 673A Bear and Game
- Android UI(Spinner)详解
- 基于RecyclerView通用适配打造城市,成员导航列表
- Debug和Release区别
- 学Java(安卓)的点滴
- Linux(Centos6.7)下安装NFS服务器
- jzoj 4690. 【GDOI2017模拟8.12】躲藏 网络流
- static_cast、dynamic_cast、reinterpret_cast、const_cast
- UVa 11235 频繁出现的数值(RMQ)
- node.js高清撸码
- 中国剩余定理