动手写一个炫酷的城市导航列表

点击上方“程序员大咖”,选择“置顶公众号”

关键时刻,第一时间送达!


最近公司项目需要一个城市导航的列表,自己捣鼓两天之后实现的效果图如下:





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


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


数据准备


  • 构建城市实体类 


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


/**

 * 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则代表城市拼音的第一个字母,也就是索引。


  • 将汉字转换为拼音 


这里我用的是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”


  • 根据拼音进行排序 


这里用的是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)中的第二个参数,sort方法会根据这个int值对list进行排序。


自定义快速导航栏


  • 重写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(点击触摸的位置)的时候,进行了一下判断,将字体颜色设置为红色,否则字体颜色为白色。这一点在演示动态图中有所体现。


  • 重写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()进行重绘。


  • 触摸监听


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


    public interface onTouchListener {

        void showTextView(String textView, boolean dismiss);

    }


在ACTION_DOWN与ACTION_MOVE的时候:


listener.showTextView(selectTxt, false);


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

    }     


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


RecyclerView的悬停实现


  • 布局文件 


头部布局: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的布局:线性布局竖直排列,上面引入头部布局,下面为显示城市名字的布局


  • 构建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 ,用来记录并获取头部布局展示的信息。


  • 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/CityDemo



  • 来自:CSDN-Young_Kai

  • http://blog.csdn.net/tyk0910/article/details/52066891

  • 程序员大咖整理发布,转载请联系作者获得授权

【点击成为Python大神】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值