基于RecyclerView通用适配打造城市,成员导航列表

来源:互联网 发布:淘宝规则重复开店 编辑:程序博客网 时间:2024/05/09 02:54

标题是基于RecyclerView通用适配打造城市,成员导航列表,这里的通用适配是我发表的上一篇博客RecyclerView 之通用适配,导航列表具有以下特点:

  • RecyclerView通用适配所有的特效

  • 顶部悬浮标题栏

  • 按字母索引

  • 隐藏,展开字母列表项

  • 快速定位

来张效果图,来帮助我们理解:

recycler

以上的需求基本可以满足城市,成员等导航列表,事先我了解了一下市面上导航列表,总感觉功能不是很齐全,大部分都是基于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

来看看效果图:

recycler

我们还有个功能就是通过触摸来动态改变字母的索引,这个功能我们又怎么来实现呢?

最后我们重写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_DOWNACTION_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接口主要是控制视图中间大写字母的显示和隐藏的。最后来看一看快速导航的效果图:

recycler

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) {     } });

运行的效果图如下:

recycler

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);    }}

采用的是集合上一条数据和下一条数据比较,如果相同则隐藏,反正显示。当然你也可以在实体类添加字段处理,这种方式交给你们自己去实现。

recycler

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);}

效果图:

recycler

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)判断何时平移。

recycler

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);    }});

recycler

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

8 0
原创粉丝点击