天天看點

基于RecyclerView通用适配打造城市,成員導航清單

标題是基于RecyclerView通用适配打造城市,成員導航清單,這裡的通用适配是我發表的上一篇部落格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

來看看效果圖:

基于RecyclerView通用适配打造城市,成員導航清單

我們還有個功能就是通過觸摸來動态改變字母的索引,這個功能我們又怎麼來實作呢?

最後我們重寫

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

接口主要是控制視圖中間大寫字母的顯示和隐藏的。最後來看一看快速導航的效果圖:

基于RecyclerView通用适配打造城市,成員導航清單

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) {
     }
 });
           

運作的效果圖如下:

基于RecyclerView通用适配打造城市,成員導航清單

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

采用的是集合上一條資料和下一條資料比較,如果相同則隐藏,反正顯示。當然你也可以在實體類添加字段處理,這種方式交給你們自己去實作。

基于RecyclerView通用适配打造城市,成員導航清單

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

效果圖:

基于RecyclerView通用适配打造城市,成員導航清單

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)

判斷何時平移。

基于RecyclerView通用适配打造城市,成員導航清單

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);
    }
});
           
基于RecyclerView通用适配打造城市,成員導航清單

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 。