Android右側邊欄滾動選擇

涉及到的內容:

  1. 首先會ListView或RecyclerView的多佈局。html

  2. 自定義View右側拼音列表,簡單地繪製並設立監聽事件等。java

  3. 會使用pinyin4.jar第三方包來識別漢字的首字母(單獨處理重慶多音問題)。git

  4. 將所有的城市列表轉化爲{A a開頭城市名...,B b開頭城市名...}的格式,這個數據轉化是重點**!!!**github

  5. 將第三步獲取的數據來多佈局展現出來。canvas

難點:

一、RecyclerView的滑動問題ide

二、RecyclerView的點擊問題工具

三、繪製SideBar佈局

先來看個圖,看是否是你想要的字體

1557800237747.gif

實現思路

根據城市和拼音列表,能夠想到多佈局,這裏無非是把城市名稱按其首字母進行排列後再填充列表,若是給你一組數據{A、城市一、城市二、B、城市三、城市4...}這樣的數據讓你填充你總會吧,無非就是兩種佈局,將拼音和漢字的背景設置不一樣就行;右側是個自定義佈局,別說你不會自定義佈局,不會也行,這個很簡單,無非是平分高度,經過drawText()繪製字母,而後進行滑動監聽,右側滑動或點擊到哪裏,左側列表相應進行滾動便可。優化

其實原先我已經經過ListView作過了,此次回顧使用RecyclerView再實現一次,發現還遇到了一些新東西,帶大家看看。此次沒有使用BaseQuickAdapter,使用多了都忘記原始的代碼怎麼敲了話很少說開擼吧

1. 肯定數據格式

首先咱們須要肯定下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

2. 整理數據

通常咱們準備的數據都是這樣的

<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填充數據了吧

3. 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);
}

複製代碼

4. 繪製側邊字母欄

這裏的自定義很簡單,無非是定義畫筆,而後在畫布上經過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,經過顯示隱藏來表示其彈出)

5. Activity中使用

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

6. 總結

再次說明下本身遇到的幾個問題:

一、點擊問題,ListViewsetOnItemClickListener()方法,而RecyclerView沒有,須要創建接口進行監聽。 二、滑動問題,listViewsetSelection(position)滑動能夠直接將該項滑至屏幕頂部,而recyclerViewsmoothScrollToPosition(position);只是將其移動至屏幕內,須要再次進行處理。 三、listViewisEnable() 方法能夠設置字母Item不能點擊,而城市名Item能夠點擊,recycleView的實現(直接在設立點擊事件的時候,是頭部就不設立點擊事件就行) 四、item不充滿全屏,加載佈局的緣由

以上就是所有內容,真的是不寫文章不回顧就會忘得很快啊,之前還寫過仿美團的雙RecyclerView聯動,當時關於如何滑動就寫了不少,到這裏就忘了該怎麼將item置頂,真是汗顏,下次抽時間把那篇文章也總結下吧。

若是對你有幫助的話記得start哦

7. 待改善

最關鍵的仍是數據的處理那裏

一、整理數據的部分,每次添加數據都判斷下是否包含重慶感受挺傻的,能夠將所有數據填充完後,在指定位置加上重慶就行,須要優化 二、在sideBarsetOnTouchingLetterChangedListener()方法裏,每次滑動完都從cityList裏0開始找第一個出現該字母的位置,感受很傻,須要優化 三、爲了方便的展現,沒有進行封裝,其實還能夠將一些例如設置側邊欄字體顏色背景等都封裝起來,便於更改,但鑑於有些小夥伴不會自定義View(我懶),因此就沒有寫了,下次再整理整理吧。

各位小夥伴以爲哪些地方還能夠優化呢?

參考文章

Android項目實戰(八):列表右側邊欄拼音展現效果 RecyclerView將指定項滑動到頂部顯示 java.text 類 CollationKey RecycleView4種定位滾動方式演示

相關文章
相關標籤/搜索