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

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

  • RecyclerView通用适配所有的特效

  • 顶部悬浮标题栏

  • 按字母索引

  • 隐藏,展开字母列表项

  • 快速定位

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

基于RecyclerView通用适配打造城市,成员导航列表_第1张图片

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

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 = 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

来看看效果图:

基于RecyclerView通用适配打造城市,成员导航列表_第2张图片

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

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

基于RecyclerView通用适配打造城市,成员导航列表_第3张图片

5、NavigationActivity(导航控件)

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

运行的效果图如下:

基于RecyclerView通用适配打造城市,成员导航列表_第4张图片

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

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

基于RecyclerView通用适配打造城市,成员导航列表_第5张图片

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

效果图:

基于RecyclerView通用适配打造城市,成员导航列表_第6张图片

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

基于RecyclerView通用适配打造城市,成员导航列表_第7张图片

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

基于RecyclerView通用适配打造城市,成员导航列表_第8张图片

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

你可能感兴趣的:(Android,android,城市导航,索引,recycler,字母索引)