标題是基于RecyclerView通用适配打造城市,成員導航清單,這裡的通用适配是我發表的上一篇部落格RecyclerView 之通用适配,導航清單具有以下特點:
- RecyclerView通用适配所有的特效
- 頂部懸浮标題欄
- 按字母索引
- 隐藏,展開字母清單項
- 快速定位
來張效果圖,來幫助我們了解:

以上的需求基本可以滿足城市,成員等導航清單,事先我了解了一下市面上導航清單,總感覺功能不是很齊全,大部分都是基于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 = ; 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
方法:
@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 = ; i < mDatas.size(); i++) {
if (i == selectorPosition) {
mPaint.setColor(Color.GREEN);
canvas.drawCircle(mWidth / , i * mTextHeight + mTextHeight / - dip2px(), dip2px(), mCirclePaint);
} else {
mPaint.setColor(Color.WHITE);
}
mPaint.setTextSize(dip2px());
mFontMetrics = mPaint.getFontMetrics();
canvas.drawText(mDatas.get(i), mWidth / , i * mTextHeight + mTextHeight / + mFontMetrics.bottom, mPaint);
}
}
onDraw
的方法也比較簡單,
selectorPosition
表示目前字母索引。首先對字母集合的一個周遊,判斷目前的索引,更換畫筆顔色,繪制索引字母圓形背景,最後繪制字母。記得添加
mPaint.setTextAlign(Paint.Align.CENTER);
文本對齊方式。利用
baselineY=mHeight/2+fm.bottom
公式得到
baselineY
的坐标值,不了解的請點選以下連結:
http://blog.csdn.net/u012551350/article/details/51361778
來看看效果圖:
我們還有個功能就是通過觸摸來動态改變字母的索引,這個功能我們又怎麼來實作呢?
最後我們重寫
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_DOWN
,
ACTION_MOVE
根據目前
y
來擷取索引值
selectorPosition
,并且重新整理
View
,來看看
changePosition
方法:
private void changePosition(int y) {
selectorPosition = y / (mHeight / mDatas.size());
if (selectorPosition >= mDatas.size()) {
selectorPosition = mDatas.size() - ;
} else if (selectorPosition <= ) {
selectorPosition = ;
}
if (mOnTouchListener != null) {
mOnTouchListener.onTouchListener(mDatas.get(selectorPosition), false);
}
invalidate();
}
if
語句是防止觸摸到控件以外的點,造成資料越界異常。這裡不是調用了一個接口,幹什麼用的?
mOnTouchListener
接口主要是控制視圖中間大寫字母的顯示和隐藏的。最後來看一看快速導航的效果圖:
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) {
}
});
運作的效果圖如下:
a、去重
不用我說,接下來就是去重。
if (helper.getAdapterPosition() == ) {
helper.setVisible(R.id.tv_letter_header, true);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - ).firstPinYin)) {
helper.setVisible(R.id.tv_letter_header, false);
} else {
helper.setVisible(R.id.tv_letter_header, true);
}
}
采用的是集合上一條資料和下一條資料比較,如果相同則隐藏,反正顯示。當然你也可以在實體類添加字段處理,這種方式交給你們自己去實作。
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);
}
效果圖:
c、頂部懸浮字母
在講解滑動懸浮功能的時候,需要事先了解下
recyclerView.findChildViewUnde
方法,來看看這個方法的一個實作:
public View findChildViewUnder(float x, float y) {
final int count = mChildHelper.getChildCount();
for (int i = count - ; i >= ; 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() - );
//判斷必須加上,防止傳回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() == ) {
helper.itemView.setTag(HEADER_FIRST_VIEW);
} else {
if (item.firstPinYin.equals(mDatas.get(helper.getAdapterPosition() - ).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() > ) {
tvLetterHeader.setTranslationY(headerMoveY);
} else {
tvLetterHeader.setTranslationY();
}
} else {
tvLetterHeader.setTranslationY();
}
}
動态的擷取頂部懸浮字母欄向上平移的距離
transView.getTop() - tvLetterHeader.getMeasuredHeight();
,
if (tag == HEADER_VISIBLE_VIEW)
判斷何時平移。
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(, top);
} else {
//當要置頂的項在目前顯示的最後一項的後面時
mRecyclerView.scrollToPosition(n);
//這裡這個變量是用在RecyclerView滾動監聽裡面的
move = true;
}
然後在
RecyclerView
滾動監聽:
if (move) {
move = false;
//擷取要置頂的項在目前螢幕的位置,selectPosition 是記錄的要置頂項在RecyclerView中的位置
int n = selectPosition - mLinearLayoutManager.findFirstVisibleItemPosition();
if ( <= n && n < mRecyclerView.getChildCount()) {
//擷取要置頂的項頂部離RecyclerView頂部的距離
int top = mRecyclerView.getChildAt(n).getTop();
//最後的移動
mRecyclerView.scrollBy(, top);
}
}
最後在
mNavigationView
的接口
setOnTouchListener
當中調用該方法:
mNavigationView.setOnTouchListener(new LetterNavigationView.OnTouchListener() {
@Override
public void onTouchListener(String str, boolean hideEnable) {
for (int i = ; i < mDatas.size(); i++) {
if (mDatas.get(i).firstPinYin.equals(str)) {
selectPosition = i;
break;
}
}
moveToPosition(selectPosition);
}
});
e、滑動改變字母導航欄的索引
我們在
addOnScrollListener
,擷取目前的索引值來動态重新整理字母導航欄:
String tvLetterStr = tvLetter.getText().toString().trim();
String tvHeaderStr = tvLetterHeader.getText().toString().trim();
if (!tvHeaderStr.equals(tvLetterStr)) {
for (int i = ; i < mLetterDatas.size(); i++) {
if (tvLetterStr.equals(mLetterDatas.get(i))) {
mNavigationView.setSelectorPosition(i);
break;
}
}
}
功能齊全的城市,成員導航就實作了。如果對你有所幫助,還望 github 給 star 。