本文已授權微信公衆號:鴻洋(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的點擊事件即可。