开发者社区> tangyangkai> 正文

RecyclerView学习(四)----城市导航列表的实现(上)

简介: 本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。 最近一个月实在是太忙了,博客也快一个月没更新了。。。刚好最近公司项目需要一个城市导航的列表,自己捣鼓两天之后实现的效果图如下: 左侧的列表根据拼音自动排序,支持头部悬停,点击Item会提示选择的城市;右侧是一个快速导航栏,点击字母会提示选择的字母,左侧列表会滑动到对应位置,支持导航栏快速滑动。
+关注继续查看

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发。

最近一个月实在是太忙了,博客也快一个月没更新了。。。刚好最近公司项目需要一个城市导航的列表,自己捣鼓两天之后实现的效果图如下:

这里写图片描述

左侧的列表根据拼音自动排序,支持头部悬停,点击Item会提示选择的城市;右侧是一个快速导航栏,点击字母会提示选择的字母,左侧列表会滑动到对应位置,支持导航栏快速滑动。

OK,整体效果就是这样,真机测试也挺流畅,一起看看怎么实现这个炫酷的城市导航列表。

1.数据准备

1.构建城市实体类
假如服务器返回的是一堆杂乱无章的城市数据,我们需要对这些数据根据拼音的先后顺序进行排序。对应的实体类如下:

/**
 * Created by tangyangkai on 16/7/26.
 */
public class City {
    private String cityPinyin;
    private String cityName;
    private String firstPinYin;

    public String getCityPinyin() {
        return cityPinyin;
    }

    public void setCityPinyin(String cityPinyin) {
        this.cityPinyin = cityPinyin;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }
    public String getFirstPinYin() {
        firstPinYin = cityPinyin.substring(0, 1);
        return firstPinYin;
    }
}

cityPinyin代表城市名称的拼音,cityName代表城市名称,firstPinYin则代表城市拼音的第一个字母,也就是索引。

2.将汉字转换为拼音
这里我用的是TinyPinyin,一个适用于Java和Android的快速、低内存占用的汉字转拼音库。TinyPinyin的特点有:生成的拼音不包含声调,也不处理多音字,默认一个汉字对应一个拼音;拼音均为大写;无需初始化,执行效率很高(Pinyin4J的4倍);很低的内存占用(小于30KB)。使用起来也很简单:

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

比如传入一个汉字“安庆”,返回的结果就是“ANQING”

3.根据拼音进行排序
这里用的是java中的compareto方法,返回参与比较的前后两个字符串的asc码的差值,举个栗子:
若a=”b”,b=”a”,输出1;
若a=”abcdef”,b=”a”输出5;
若a=”abcdef”,b=”ace”输出-1;
即参与比较的两个字符串如果首字符相同,则比较下一个字符,直到有不同的为止,返回该不同的字符的asc码差值。

    public class PinyinComparator implements Comparator<City> {
        @Override
        public int compare(City cityFirst, City citySecond) {
            return cityFirst.getCityPinyin().compareTo(citySecond.getCityPinyin());
        }
    }

使用的时候实现Comparator接口,传入需要比较的实体类,然后将返回值作为 Collections.sort(cityList, pinyinComparator)中的第二个参数,Collections.sort方法会根据这个传入的int值对cityList进行排序。

2.自定义快速导航栏

1.重写onDraw()方法
右侧快速导航栏是一个自定义View,这里重点说一下onDraw()方法。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(backgroundColor);
        canvas.drawRect(0, 0, (float) mWidth, mHeight, paint);
        for (int i = 0; i < CityActivity.pinyinList.size(); i++) {
            String textView = CityActivity.pinyinList.get(i);
            if (i == position - 1) {
                paint.setColor(getResources().getColor(R.color.error_color));
                selectTxt = CityActivity.pinyinList.get(i);
                listener.showTextView(selectTxt, false);
            } else {
                paint.setColor(getResources().getColor(R.color.white));
            }
            paint.setTextSize(40);
            paint.getTextBounds(textView, 0, textView.length(), mBound);
            canvas.drawText(textView, (mWidth - mBound.width()) * 1 / 2, mTextHeight - mBound.height(), paint);
            mTextHeight += mHeight / CityActivity.pinyinList.size();

        }
    }

这里的pinyinList是去除重复的,按照A-Z顺序排列的字母索引集合。遍历这个集合,依次绘制出这些字母。在 i 等于 position -1(点击触摸的位置)的时候,将字体颜色设置为红色,否则字体颜色为白色。这一点在演示动态图中有所体现,触摸点击的字体颜色会改变。

2.重写onTouchEvent()方法

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int y = (int) event.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                backgroundColor = getResources().getColor(R.color.font_text);
                mTextHeight = mHeight / CityActivity.pinyinList.size();
                position = y / (mHeight / (CityActivity.pinyinList.size() + 1));
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                if (isSlide) {
                    backgroundColor = getResources().getColor(R.color.font_text);
                    mTextHeight = mHeight / CityActivity.pinyinList.size();
                    position = y / (mHeight / CityActivity.pinyinList.size() + 1) + 1;
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                backgroundColor = getResources().getColor(R.color.font_info);
                mTextHeight = mHeight / CityActivity.pinyinList.size();
                position = 0;
                invalidate();
                listener.showTextView(selectTxt, true);
                break;
        }
        return true;
    }

case MotionEvent.ACTION_DOWN:设置背景颜色,设置字体初始高度,计算触摸位置,调用invalidate()进行重绘;
case MotionEvent.ACTION_MOVE:与ACTION_DOWN一样的操作,加上一个判断,让滑动的距离大于默认的最小滑动距离才设置滑动有效;
case MotionEvent.ACTION_UP:设置背景颜色,设置字体初始高度,将position设置为0,进行重置操作,调用invalidate()进行重绘;

3.触摸监听

屏幕中间是一个自定义的圆形TextView,默认设置为View.GONE,触摸的时候设置为View.VISIBLE,并将TextView的值设置为点击触摸的字母。因此我们的接口设计如下:

    public interface onTouchListener {
        void showTextView(String textView, boolean dismiss);
    }

在MotionEvent.ACTION_DOWN与MotionEvent.ACTION_MOVE的时候:

listener.showTextView(selectTxt, false);

在MotionEvent.ACTION_UP的时候:

listener.showTextView(selectTxt, true);

然后让Activity实现该接口,通过传过来的boolean值控制圆形TextView是否显示:

    @Override
    public void showTextView(String textView, boolean dismiss) {

        if (dismiss) {
            circleTxt.setVisibility(View.GONE);
        } else {
            circleTxt.setVisibility(View.VISIBLE);
            circleTxt.setText(textView);
        }

        int selectPosition = 0;
        for (int i = 0; i < cityList.size(); i++) {
            if (cityList.get(i).getFirstPinYin().equals(textView)) {
                selectPosition = i;
                break;
            }
        }
        recyclerView.scrollToPosition(selectPosition);
    }     

点击触摸的同时,需要让recyclerView滑动到对应的位置。遍历cityList数组,得到拼音的第一个字母,与传递过来的索引字母进行对比,相等则将
i 设置为selectPosition。最后调用recyclerView.scrollToPosition()方法,滑动到对应的位置,达到索引导航的作用。

3.RecyclerView的悬停实现

1.布局文件
头部布局:layout_sticky_header_view.xml,也就是示例图中红色的部分,里面包含一个索引字母TextView
主界面的布局:一共两层,头部布局覆盖在RecyclerView上面

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_toLeftOf="@+id/my_slide_view">
            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv_sticky_example"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="none" />
            <include layout="@layout/layout_sticky_header_view" />
        </FrameLayout>

子item的布局:线性布局竖直排列,上面引入头部布局,下面为显示城市名字的布局

2.构建CityAdapter

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {

        if (holder instanceof CityViewHolder) {
            CityViewHolder viewHolder = (CityViewHolder) holder;
            City cityModel = cityLists.get(position);
            viewHolder.tvCityName.setText(cityModel.getCityName());

            if (position == 0) {
                viewHolder.tvStickyHeader.setVisibility(View.VISIBLE);
                viewHolder.tvStickyHeader.setText(cityModel.getFirstPinYin());
                viewHolder.itemView.setTag(FIRST_STICKY_VIEW);
            } else {
                if (!TextUtils.equals(cityModel.getFirstPinYin(), cityLists.get(position - 1).getFirstPinYin())) {
                    viewHolder.tvStickyHeader.setVisibility(View.VISIBLE);
                    viewHolder.tvStickyHeader.setText(cityModel.getFirstPinYin());
                    viewHolder.itemView.setTag(HAS_STICKY_VIEW);
                } else {
                    viewHolder.tvStickyHeader.setVisibility(View.GONE);
                    viewHolder.itemView.setTag(NONE_STICKY_VIEW);
                }
            }

         viewHolder.itemView.setContentDescription(cityModel.getFirstPinYin());
        }

    }

这里重点说一下onBindViewHolder这个方法:

每一个RecyclerView的item的布局里面都包含一个头部布局,然后判断当前item和上一个item的头部布局里的索引字母是否相同,来决定是否展示item的头部布局。

第一个item的头部布局是显示的,设置为View.VISIBLE,标记tag为FIRST_STICKY_VIEW;
item布局中,索引字母不相同的头部布局是显示的,设置为View.VISIBLE,标记tag为HAS_STICKY_VIEW;
item布局中,索引字母相同的头部布局是隐藏的,设置为View.GONE,标记tag为NONE_STICKY_VIEW;

最后为每一个item设置一个ContentDescription ,用来记录并获取头部布局展示的信息。

3.RecyclerView的滑动监听

主界面的布局中,最上层有一个头部布局tvStickyHeaderView,通过监听RecyclerView的滚动,根据RecyclerView的滚动距离,决定头部布局向上或者向下滚动的距离,实现悬停效果:

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                View stickyInfoView = recyclerView.findChildViewUnder(
                        tvStickyHeaderView.getMeasuredWidth() / 2, 5);
                if (stickyInfoView != null && stickyInfoView.getContentDescription() != null) {
                    tvStickyHeaderView.setText(String.valueOf(stickyInfoView.getContentDescription()));
                }
                View transInfoView = recyclerView.findChildViewUnder(
                        tvStickyHeaderView.getMeasuredWidth() / 2, tvStickyHeaderView.getMeasuredHeight() + 1);

                if (transInfoView != null && transInfoView.getTag() != null) {
                    int transViewStatus = (int) transInfoView.getTag();
                    int dealtY = transInfoView.getTop() - tvStickyHeaderView.getMeasuredHeight();
                    if (transViewStatus == CityAdapter.HAS_STICKY_VIEW) {
                        if (transInfoView.getTop() > 0) {
                            tvStickyHeaderView.setTranslationY(dealtY);
                        } else {
                            tvStickyHeaderView.setTranslationY(0);
                        }
                    } else if (transViewStatus == CityAdapter.NONE_STICKY_VIEW) {
                        tvStickyHeaderView.setTranslationY(0);
                    }
                }
            }
        });

1.第一次调用RecyclerView的findChildViewUnder()方法,返回指定位置的childView,这里也就是item的头部布局,因为我们的tvStickyHeaderView展示的肯定是最上面item的头部布局里的索引字母信息。
2.第二次调用RecyclerView的findChildViewUnder()方法,这里返回的是固定在屏幕上方那个tvStickyHeaderView下面一个像素位置的RecyclerView的item,根据这个item来更新tvStickyHeaderView要translate多少距离。
3.如果tag为HAS_STICKY_VIEW,表示当前item需要展示头部布局,那么根据这个item的getTop和tvStickyHeaderView的高度相差的距离来滚动tvStickyHeaderView;如果tag为NONE_STICKY_VIEW,表示当前item不需要展示头部布局,那么就不会引起tvStickyHeaderView的滚动。

参考资料

最后使用接口回调处理RecyclerView的点击事件即可。由于篇幅有限,这里就只介绍了一些重点。项目完整源码已经上传到我的github上:

源码地址:

https://github.com/18722527635/MyRecyclerView

欢迎Star,fork,提issues,一起进步!

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
9055 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
12453 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
19691 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
13419 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
17985 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
32715 0
使用NAT网关轻松为单台云服务器设置多个公网IP
在应用中,有时会遇到用户询问如何使单台云服务器具备多个公网IP的问题。 具体如何操作呢,有了NAT网关这个也不是难题。
34554 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
18614 0
+关注
tangyangkai
多实践,多思考,多请教
40
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载