一、数据准备
1.构建城市实体类 假如服务器返回的是一堆杂乱无章的城市数据,我们需要对这些数据根据拼音的先后顺序进行排序。对应的实体类如下:
<span style="font-size:14px;">/** * 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; } }</span>
cityPinyin代表城市名称的拼音,cityName代表城市名称,firstPinYin则代表城市拼音的第一个字母,也就是索引。
2.将汉字转换为拼音 这里我用的是TinyPinyin,一个适用于Java和Android的快速、低内存占用的汉字转拼音库。TinyPinyin的特点有:生成的拼音不包含声调,也不处理多音字,默认一个汉字对应一个拼音;拼音均为大写;无需初始化,执行效率很高(Pinyin4J的4倍);很低的内存占用(小于30KB)。使用起来也很简单:
<span style="font-size:14px;"> 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(); }</span>
比如传入一个汉字“安庆”,返回的结果就是“ANQING”
3.根据拼音进行排序 这里用的是java中的compareto方法,返回参与比较的前后两个字符串的asc码的差值,举个栗子: 若a=”b”,b=”a”,输出1; 若a=”abcdef”,b=”a”输出5; 若a=”abcdef”,b=”ace”输出-1; 即参与比较的两个字符串如果首字符相同,则比较下一个字符,直到有不同的为止,返回该不同的字符的asc码差值。
<span style="font-size:14px;"> public class PinyinComparator implements Comparator<City> { @Override public int compare(City cityFirst, City citySecond) { return cityFirst.getCityPinyin().compareTo(citySecond.getCityPinyin()); } }</span>
使用的时候实现Comparator接口,传入需要比较的实体类,然后将返回值作为 Collections.sort(cityList, pinyinComparator)中的第二个参数,Collections.sort方法会根据这个传入的int值对cityList进行排序。
二、自定义快速导航栏
1.重写onDraw()方法 右侧快速导航栏是一个自定义View,这里重点说一下onDraw()方法。
<span style="font-size:14px;">@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(); } }</span>
这里的pinyinList是去除重复的,按照A-Z顺序排列的字母索引集合。遍历这个集合,依次绘制出这些字母。在 i 等于 position -1(点击触摸的位置)的时候,将字体颜色设置为红色,否则字体颜色为白色。这一点在演示动态图中有所体现,触摸点击的字体颜色会改变。
2.重写onTouchEvent()方法
<span style="font-size:14px;"> @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; }</span>
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的值设置为点击触摸的字母。因此我们的接口设计如下:
<span style="font-size:14px;"> public interface onTouchListener { void showTextView(String textView, boolean dismiss); }</span>
在MotionEvent.ACTION_DOWN与MotionEvent.ACTION_MOVE的时候:
<span style="font-size:14px;">listener.showTextView(selectTxt, false);</span>
在MotionEvent.ACTION_UP的时候:
<span style="font-size:14px;">listener.showTextView(selectTxt, true);</span>
然后让Activity实现该接口,通过传过来的boolean值控制圆形TextView是否显示:
<pre name="code" class="java"><span style="font-size:14px;"> @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); } </span>点击触摸的同时,需要让recyclerView滑动到对应的位置。遍历cityList数组,得到拼音的第一个字母,与传递过来的索引字母进行对比,相等则将 i 设置为selectPosition。最后调用recyclerView.scrollToPosition()方法,滑动到对应的位置,达到索引导航的作用。
三、RecyclerView的悬停实现
1.布局文件 头部布局:layout_sticky_header_view.xml,也就是示例图中红色的部分,里面包含一个索引字母TextView 主界面的布局:一共两层,头部布局覆盖在RecyclerView上面
<span style="font-size:14px;"> <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></span>
子item的布局:线性布局竖直排列,上面引入头部布局,下面为显示城市名字的布局
2.构建CityAdapter
<span style="font-size:14px;"> @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()); } }</span>
这里重点说一下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的滚动距离,决定头部布局向上或者向下滚动的距离,实现悬停效果:
<span style="font-size:14px;">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); } } } });</span>
(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的滚动