首先會ListView或RecyclerView的多佈局。html
自定義View右側拼音列表,簡單地繪製並設立監聽事件等。java
會使用pinyin4.jar第三方包來識別漢字的首字母(單獨處理重慶多音問題)。git
將所有的城市列表轉化爲{A a開頭城市名...,B b開頭城市名...}的格式,這個數據轉化是重點**!!!**github
將第三步獲取的數據來多佈局展現出來。canvas
一、RecyclerView的滑動問題ide
二、RecyclerView的點擊問題工具
三、繪製SideBar佈局
先來看個圖,看是否是你想要的字體
根據城市和拼音列表,能夠想到多佈局,這裏無非是把城市名稱按其首字母進行排列後再填充列表,若是給你一組數據{A、城市一、城市二、B、城市三、城市4...}這樣的數據讓你填充你總會吧,無非就是兩種佈局,將拼音和漢字的背景設置不一樣就行;右側是個自定義佈局,別說你不會自定義佈局,不會也行,這個很簡單,無非是平分高度,經過drawText()
繪製字母,而後進行滑動監聽,右側滑動或點擊到哪裏,左側列表相應進行滾動便可。優化
其實原先我已經經過ListView作過了,此次回顧使用RecyclerView再實現一次,發現還遇到了一些新東西,帶大家看看。此次沒有使用BaseQuickAdapter,使用多了都忘記原始的代碼怎麼敲了話很少說開擼吧
首先咱們須要肯定下Bean的數據格式,畢竟涉及到多佈局
public class ItemBean {
private String itemName;//城市名或者字母A...
private String itemType;//類型,區分是首字母仍是城市名,是首字母的寫「head」,不是的填入其它字母都行
// 標記 拼音頭,head爲0
public static final int TYPE_HEAD = 0;
// 標記 城市名
public static final int TYPE_CITY = 1;
public int getType() {
if (itemType.equals("head")) {
return TYPE_HEAD;
} else {
return TYPE_CITY;
}
}
......Get Set方法
}
複製代碼
能夠看到有兩個字段,一個用來顯示城市名或者字母,另外一個用來區分是城市仍是首字母。這裏定義了個getType()方法,爲字母的話返回0,城市名返回1
通常咱們準備的數據都是這樣的
<resources>
<string-array name ="mycityarray">
<item>北京市</item>
<item>上海市</item>
<item>廣州市</item>
<item>天津市</item>
<item>石家莊市</item>
<item>唐山市</item>
<item>秦皇島市</item>
<item>邯鄲市</item>
<item>邢臺市</item>
<item>保定市</item>
<item>張家口市</item>
<item>承德市市</item>
<item>滄州市</item>
<item>廊坊市</item>
<item>衡水市</item>
......
</string-array>
</resources>
複製代碼
想要獲得咱們那樣的數據,須要先獲取這些城市名的首字母而後進行排序,這裏我使用pinyin4j-2.5.0.jar進行漢字到拼音的轉化,jar下載地址
2.1 編寫工具類
public class HanziToPinYin {
/** * 若是字符串string是漢字,則轉爲拼音並返回,返回的是首字母 * @param string * @return */
public static char toPinYin(String string){
HanyuPinyinOutputFormat hanyuPinyin = new HanyuPinyinOutputFormat();
hanyuPinyin.setCaseType(HanyuPinyinCaseType.UPPERCASE);
hanyuPinyin.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
hanyuPinyin.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);
String[] pinyinArray=null;
char hanzi = string.charAt(0);
try {
//是否在漢字範圍內
if(hanzi>=0x4e00 && hanzi<=0x9fa5){
pinyinArray = PinyinHelper.toHanyuPinyinStringArray(hanzi, hanyuPinyin);
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
//將獲取到的拼音返回,只返回其首字母
return pinyinArray[0].charAt(0);
}
}
複製代碼
2.2 整理數據
private List<String> cityList; //給定的全部的城市名
private List<ItemBean> itemList; //整理後的全部的item子項,多是城市、多是字母
//初始化數據,將全部城市進行排序,且加上字母和它們一塊兒造成新的集合
private void initData(){
itemList = new ArrayList<>();
//獲取全部的城市名
String[] cityArray = getResources().getStringArray(R.array.mycityarray);
cityList = Arrays.asList(cityArray);
//將全部城市進行排序,排完後cityList內全部的城市名都是按首字母進行排序的
Collections.sort(cityList, new CityComparator());
//將剩餘的城市加進去
for (int i = 0; i < cityList.size(); i++) {
String city = cityList.get(i);
String letter = null; //當前所屬的字母
if (city.contains("重慶")) {
letter = HanziToPinYin.toPinYin("崇慶") + "";
} else {
letter = HanziToPinYin.toPinYin(cityList.get(i)) + "";
}
if (letter.equals(currentLetter)) { //在A字母下,屬於當前字母
itemBean = new ItemBean();
itemBean.setItemName(city); //把漢字放進去
itemBean.setItemType(letter); //這裏放入其它不是「head」的字符串就行
itemList.add(itemBean);
} else { //不在當前字母下,先將該字母取出做爲獨立的一個item
//添加標籤(B...)
itemBean = new ItemBean();
itemBean.setItemName(letter); //把首字母進去
itemBean.setItemType("head"); //把head標籤放進去
currentLetter = letter;
itemList.add(itemBean);
//添加城市
itemBean = new ItemBean();
itemBean.setItemName(city); //把漢字放進去
itemBean.setItemType(letter); //把拼音放進去
itemList.add(itemBean);
}
}
}
複製代碼
通過以上步驟就將原先的數據整理成了如下形式排列的一組數據
{
{itemName:"A",itemType:"head"}
{itemName:"阿拉善盟",itemType:"A"}
{itemName:"安撫市",itemType:"A"}
...
{itemName:"巴中市",itemType:"B"}
{itemName:"白山市",itemType:"B"}
....
}
複製代碼
等等,上面有個Collections.sort(cityList, new CityComparator());
和letter = HanziToPinYin.toPinYin("崇慶") + "";
你可能還會有疑惑,我就來多幾嘴 由於pinyin4j.jar這個jar包在將漢字轉爲拼音的時候,會將重慶的拼音轉爲zhongqin,因此在排序和獲取首字母的時候都須要單獨處理
public class CityComparator implements Comparator<String> {
private RuleBasedCollator collator;
public CityComparator() {
collator = (RuleBasedCollator) Collator.getInstance(Locale.CHINA);
}
@Override
public int compare(String lhs, String rhs) {
lhs = lhs.replace("重慶", "崇慶");
rhs = rhs.replace("重慶", "崇慶");
CollationKey c1 = collator.getCollationKey(lhs);
CollationKey c2 = collator.getCollationKey(rhs);
return c1.compareTo(c2);
}
}
複製代碼
這裏先指定RuleBasedCollator
語言環境爲CHINA,而後在compare()
比較方法裏,若是遇到兩邊有"重慶"的字符串,就將其替換爲」崇慶「,而後經過getCollationKey()
獲取首個字符而後進行比較。
letter = HanziToPinYin.toPinYin("崇慶") + "";
獲取首字母的時候也是一樣,不是獲取"重慶"的首字母而是"崇慶"的首字母。
看到這樣的一組數據你總會根據多佈局來給RecyclerView填充數據了吧
既然涉及到多佈局,那麼有幾種佈局就該有幾個ViewHolder
,此次我將採用原始的寫法,不用BaseQuickAdapter,那個太方便搞得我原始的都不會寫了
新建CityAdapter類,讓這個適配器繼承自RecyclerView.Adapter,並將泛型指定爲RecyclerView.ViewHolder,其表明咱們在CityAdapter中定義的內部類
public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
......
//字母頭
public static class HeadViewHolder extends RecyclerView.ViewHolder {
private TextView tvHead;
public HeadViewHolder(View itemView) {
super(itemView);
tvHead = itemView.findViewById(R.id.tv_item_head);
}
}
//城市
public static class CityViewHolder extends RecyclerView.ViewHolder {
private TextView tvCity;
public CityViewHolder(View itemView) {
super(itemView);
tvCity = itemView.findViewById(R.id.tv_item_city);
}
}
}
複製代碼
重寫onCreateViewHolder()
、onBindViewHolder()
、getItemCount()
方法,由於涉及多佈局,還需重寫getItemViewType()
方法來區分是哪一種佈局
完整代碼以下
public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//數據項
private List<ItemBean> dataList;
//點擊事件監聽接口
private OnRecyclerViewClickListener onRecyclerViewClickListener;
public void setOnItemClickListener(OnRecyclerViewClickListener onItemClickListener) {
this.onRecyclerViewClickListener = onItemClickListener;
}
public CityAdapter(List<ItemBean> dataList) {
this.dataList = dataList;
}
//建立ViewHolder實例
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
if (viewType == 0) { //Head頭字母名稱
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_head, viewGroup,false);
RecyclerView.ViewHolder headViewHolder = new HeadViewHolder(view);
return headViewHolder;
} else { //城市名
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_city, viewGroup,false);
RecyclerView.ViewHolder cityViewHolder = new CityViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onRecyclerViewClickListener != null) {
onRecyclerViewClickListener.onItemClickListener(v);
}
}
});
return cityViewHolder;
}
}
//對子項數據進行賦值
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
int itemType = dataList.get(position).getType();
if (itemType == 0) {
HeadViewHolder headViewHolder = (HeadViewHolder) viewHolder;
headViewHolder.tvHead.setText(dataList.get(position).getItemName());
} else {
CityViewHolder cityViewHolder = (CityViewHolder) viewHolder;
cityViewHolder.tvCity.setText(dataList.get(position).getItemName());
}
}
//數據項個數
@Override
public int getItemCount() {
return dataList.size();
}
//區分佈局類型
@Override
public int getItemViewType(int position) {
int type = dataList.get(position).getType();
return type;
}
//字母頭
public static class HeadViewHolder extends RecyclerView.ViewHolder {
private TextView tvHead;
public HeadViewHolder(View itemView) {
super(itemView);
tvHead = itemView.findViewById(R.id.tv_item_head);
}
}
//城市
public static class CityViewHolder extends RecyclerView.ViewHolder {
private TextView tvCity;
public CityViewHolder(View itemView) {
super(itemView);
tvCity = itemView.findViewById(R.id.tv_item_city);
}
}
}
複製代碼
兩種item佈局都是隻放了一個TextView控件
這裏有兩處本身碰到和當時使用ListView不一樣的地方:
一、RecyclerView
沒有setOnItemClickListener()
,須要本身定義接口來實現 二、本身平時加載佈局都直接是View view = LayoutInflater.from(context).inflate(R.layout.item_head, null);
,也沒發現什麼問題,但這次就出現了Item子佈局沒法橫向鋪滿父佈局。 解決辦法:將改成如下方式加載佈局
View view = LayoutInflater.from(context).inflate(R.layout.item_head, viewGroup,false);
複製代碼
(若是遇到不能鋪滿情況也多是RecyclerView沒有明確寬高而是用權重代替的緣由)
創建的監聽器
public interface OnRecyclerViewClickListener {
void onItemClickListener(View view);
}
複製代碼
這裏的自定義很簡單,無非是定義畫筆,而後在畫布上經過drawText()方法來繪製Text便可。
4.1 首先定義類SideBar繼承自View,重寫構造方法,並在三個方法內調用自定義的init();方法來初始化畫筆
public class SideBar extends View {
//畫筆
private Paint paint;
public SideBar(Context context) {
super(context);
init();
}
public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化畫筆工具
private void init() {
paint = new Paint();
paint.setAntiAlias(true);//抗鋸齒
}
}
複製代碼
4.2 在onDraw()方法裏繪製字母
public static String[] characters = new String[]{"❤", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
private int position = -1; //當前選中的位置
private int defaultTextColor = Color.parseColor("#D2D2D2"); //默認拼音文字的顏色
private int selectedTextColor = Color.parseColor("#2DB7E1"); //選中後的拼音文字的顏色
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight(); //當前控件高度
int width = getWidth(); //當前控件寬度
int singleHeight = height / characters.length; //每一個字母佔的長度
for (int i = 0; i < characters.length; i++) {
if (i == position) { //當前選中
paint.setColor(selectedTextColor); //設置選中時的畫筆顏色
} else { //未選中
paint.setColor(defaultTextColor); //設置未選中時的畫筆顏色
}
paint.setTextSize(textSize); //設置字體大小
//設置繪製的位置
float xPos = width / 2 - paint.measureText(characters[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(characters[i], xPos, yPos, paint); //繪製文本
}
}
複製代碼
經過以上兩步,右側邊欄就算繪製完成了,但這只是靜態的,若是要實現側邊欄滑動的時候,咱們還須要監聽其觸摸事件
4.3 定義觸摸回調接口和設置監聽器的方法
//設置觸摸位置改變的監聽器的方法
public void setOnTouchingLetterChangedListener(OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
}
//觸摸位置更改的接口
public interface OnTouchingLetterChangedListener {
void onTouchingLetterChanged(int position);
}
複製代碼
4.4 觸摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float y = event.getY();
position = (int) (y / (getHeight() / characters.length)); //獲取觸摸的位置
if (position >= 0 && position < characters.length) {
//觸摸位置變化的回調
onTouchingLetterChangedListener.onTouchingLetterChanged(position);
switch (action) {
case MotionEvent.ACTION_UP:
setBackgroundColor(Color.TRANSPARENT);//手指起來後的背景變化
position = -1;
invalidate();//從新繪製控件
if (text_dialog != null) {
text_dialog.setVisibility(View.INVISIBLE);
}
break;
default://手指按下
setBackgroundColor(touchedBgColor);
invalidate();
text_dialog.setText(characters[position]);//字母框的彈出
break;
}
} else {
setBackgroundColor(Color.TRANSPARENT);
if (text_dialog != null) {
text_dialog.setVisibility(View.INVISIBLE);
}
}
return true; //必定要返回true,表示攔截了觸摸事件
}
複製代碼
具體的解釋如代碼所示,當手指起來時,position爲-1,當手指按下,更改背景並彈出字母框(這裏的字母框其實就是一個TextView,經過顯示隱藏來表示其彈出)
itemList
數據填充那些就不寫了,在前面整理數據那部分
//全部的item子項,多是城市、多是字母
private List<ItemBean> itemList;
//目標項是否在最後一個可見項以後
private boolean mShouldScroll;
//記錄目標項位置(要移動到的位置)
private int mToPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
//爲左側RecyclerView設立Item的點擊事件
cityAdapter.setOnItemClickListener(this);
sideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(int position) {
String city_label = SideBar.characters[position]; //滑動到的字母
for (int i = 0; i < cityList.size(); i++) {
if (itemList.get(i).getItemName().equals(city_label)) {
moveToPosition(i); //直接滾過去
// smoothMoveToPosition(recyclerView,i); //平滑的滾動
tvDialog.setVisibility(View.VISIBLE);
break;
}
if (i == cityList.size() - 1) {
tvDialog.setVisibility(View.INVISIBLE);
}
}
}
});
}
//實戰中可能會有選擇完後此頁面關閉,返回當前數據等操做,可在此處完成
@Override
public void onItemClickListener(View view) {
int position = recyclerView.getChildAdapterPosition(view);
Toast.makeText(view.getContext(), itemList.get(position).getItemName(), Toast.LENGTH_SHORT).show();
}
複製代碼
在使用ListView
的時候,知道要移動到的位置position
時,直接listView.setSelection(position)
就可將當前的item移動到屏幕頂部,而RecyclerView的scrollToPosition(position)只是將item移動到屏幕內,因此須要咱們經過scrollToPositionWithOffset()
方法將其置頂
private void moveToPosition(int position) {
if (position != -1) {
recyclerView.scrollToPosition(position);
LinearLayoutManager mLayoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
mLayoutManager.scrollToPositionWithOffset(position, 0);
}
}
複製代碼
這裏還有一種平滑的滾動方式,具體見Demo
再次說明下本身遇到的幾個問題:
一、點擊問題,ListView
有setOnItemClickListener()
方法,而RecyclerView
沒有,須要創建接口進行監聽。 二、滑動問題,listView
的setSelection(position)
滑動能夠直接將該項滑至屏幕頂部,而recyclerView
的 smoothScrollToPosition(position);
只是將其移動至屏幕內,須要再次進行處理。 三、listView
的isEnable()
方法能夠設置字母Item不能點擊,而城市名Item能夠點擊,recycleView
的實現(直接在設立點擊事件的時候,是頭部就不設立點擊事件就行) 四、item
不充滿全屏,加載佈局的緣由
以上就是所有內容,真的是不寫文章不回顧就會忘得很快啊,之前還寫過仿美團的雙RecyclerView聯動,當時關於如何滑動就寫了不少,到這裏就忘了該怎麼將item置頂,真是汗顏,下次抽時間把那篇文章也總結下吧。
若是對你有幫助的話記得start哦
最關鍵的仍是數據的處理那裏
一、整理數據的部分,每次添加數據都判斷下是否包含重慶感受挺傻的,能夠將所有數據填充完後,在指定位置加上重慶就行,須要優化 二、在sideBar
的setOnTouchingLetterChangedListener()
方法裏,每次滑動完都從cityList
裏0開始找第一個出現該字母的位置,感受很傻,須要優化 三、爲了方便的展現,沒有進行封裝,其實還能夠將一些例如設置側邊欄字體顏色背景等都封裝起來,便於更改,但鑑於有些小夥伴不會自定義View
(我懶),因此就沒有寫了,下次再整理整理吧。
各位小夥伴以爲哪些地方還能夠優化呢?
Android項目實戰(八):列表右側邊欄拼音展現效果 RecyclerView將指定項滑動到頂部顯示 java.text 類 CollationKey RecycleView4種定位滾動方式演示