RecyclerView學習(四)----城市導航列表的實現(上)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章