RecyclerView 的基本使用

RecyclerView 的基本使用

2017年1月21日

為了加強理解 RecyclerView 的使用方法,同時讓自己的寫作能力得到鍛煉,所以實踐一把,都是一些基本的使用,主要分析下拉刷新與上拉加載部分,細節很重要,以此來激勵自己寫出高質量的文章。

  • 基本用法
    • 基本介紹
    • 代碼實現
  • SwipeRefreshLayout 配合實現下拉刷新
    • 基本介紹
    • 代碼實現
  • RecyclerView 實現上拉加載
    • 滾動事件的分析
    • 添加 FooterView 實現上拉加載
  • 參考文章及總結

效果圖

基本使用

RecyclerView 的基本介紹

RecyclerView 是谷歌V7包下新增的控件,用來替代 ListView 的使用,在 RecyclerView 標準化了 ViewHolder 類似于 ListView中 convertView 用來做視圖緩存.

相信大家對于 ListView 都很熟悉,在我剛學 Android 開發的時候接觸就是這個強大的控件,也被稱為最難的控件之一。自從 RecyclerView 已經面市很久,也在很多應用中得到廣泛的使用,在整個開發者圈子里面也擁有很不錯的口碑,那說明 RecyclerView 擁有比 ListView,GridView 之類控件有很多的優點。

  • 設置布局管理器以控制Item的布局方式,橫向、豎向以及瀑布流方式
  • 可設置Item操作的動畫(刪除或者添加等)
  • 可設置Item的間隔樣式(可繪制)

但是對于點擊事件需要自己寫回調借口實現,下面會詳細介紹,并且沒有了 ListView 強大的 addFootView() 的方法,需要自己去實現,不過還好拓展性很強。

代碼實現

1.添加庫依賴:首先要用這個控件,你需要在gradle文件中添加包的引用(配合官方CardView使用)

compile 'com.android.support:recyclerview-v7:25.0.0'
compile 'com.android.support:cardview-v7:25.0.0'

2.然后在 xml 文件里實現

<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/recycler_view"
    android:layout_centerVertical="true"
    android:layout_centerHorizontal="true"/>

3.接著就是在 Activity 里面設置

public class MainActivity extends AppCompatActivity {

    private RecyclerView mRecyclerView;
    private RecyclerAdapter mAdapter;
    private LinearLayoutManager mLayoutManager;
    private List<ShareBean> mData = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();//初始化數據
        initView();//初始化布局
        setListener();//設置監聽事件
    }

    private void initData() {
        for (int i = 0; i < 4; i++) {
            mData.add(new ShareBean(R.mipmap.ic_image, "Android vs IOS"));
        }
    }

    private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler);
        //設置布局管理器
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLayoutManager);
        //確保尺寸是一個常數,避免計算每個item的size
        mRecyclerView.setHasFixedSize(true);
        //設置顯示動畫
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mAdapter = new RecyclerAdapter(this, mData);
        mRecyclerView.setAdapter(mAdapter);
    }

    private void setListener() {
        //設置Items的點擊事件
        mAdapter.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(final View view, int position) {
                onClick(view, position);
            }
        });
      }
}

4.適配器 Adapter 的代碼

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public  OnItemClickListener itemClickListener;
    private List<ShareBean> mData = null;
    private LayoutInflater mInflater;

    public RecyclerAdapter(Context context, List<ShareBean> mData) {
        this.mData = mData;
        this.mInflater = LayoutInflater.from(context);
    }

    //將布局轉化為 View 并傳遞給 RecyclerView 封裝好的 ViewHolder
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){    
        // 實例化展示的view
        View view = mInflater.inflate(R.layout.item_recyclerview, parent, false);
        // 實例化viewholder
        return new ItemViewHolder(view);
    }

    //將數據與視圖進行綁定
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        ((ItemViewHolder) holder).mImageView.setImageResource(mData.get(position).getImage_id());
        ((ItemViewHolder) holder).mTextView.setText(mData.get(position).getName());
    }

    //返回 Item 的數量
    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();;
    }

    public  class ItemViewHolder extends RecyclerView.ViewHolder {

        CardView mCardView;
        CircleImageView mImageView;
        TextView mTextView;

        public ItemViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.textView);
            mCardView = (CardView) itemView.findViewById(R.id.cardView);
            mImageView = (CircleImageView) itemView.findViewById(R.id.image);
            mCardView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (itemClickListener != null) {
                        itemClickListener.onItemClick(view, getPosition());
                    }
                }
            });
        }
    }

    //以下為點擊事件的接口回調部分
    public void setOnItemClickListener(OnItemClickListener itemClickListener) {
        this.itemClickListener = itemClickListener;
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }
}

以上是適配器基本的寫法,這個自定義 Adapter 和我們在使用 Listview 時候的 Adapter 相比還是有點不太一樣的,首先這邊我們需要繼承 RecyclerView.Adaper 類,然后實現兩個重要的方法 onBindViewHodler() 以及 onCreateViewHolder() ,這邊我們看出來區別,使用 RecyclerView 控件我們就可以把 ItemView 視圖創建和數據綁定這兩步進行分來進行管理,用法就更加方便而且靈活了,并且我們可以定制打造千變萬化的布局。同時這邊我們還需要創建一個 ViewHolder 類,該類必須繼承自 RecyclerView.ViewHolder 類,現在 Google 也要求我們必須要實現 ViewHolder 來承載 Item 的視圖。

特別注意一下最下面的點擊事件的回調函數,主要是對接口的熟悉程度,Java 是硬傷 55555.

同時 RecyclerView 有三種實現方式(上面介紹有),通過一下代碼設置

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));//這里用線性顯示 類似于listview
mRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));//這里用線性宮格顯示 類似于grid view
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL));//這里用線性宮格顯示 類似于瀑布流


SwipeRefreshLayout 配合實現下拉刷新

基本介紹

SwipeRefrshLayout 是 Google 官方更新的一個 Widget ,可以實現下拉刷新的效果。該控件集成自 ViewGroup 在 support-v4 兼容包下,不過我們需要升級 supportlibrary 的版本到19.1以上。基本使用的方法如下:

  • setOnRefreshListener(OnRefreshListener):添加下拉刷新監聽器
  • setRefreshing(boolean):顯示或者隱藏刷新進度條
  • isRefreshing():檢查是否處于刷新狀態
  • setColorSchemeResources():設置進度條的顏色主題,最多設置四種,以前的setColorScheme()方法已經棄用了
代碼實現

1.首先先看一下 xml 布局,在 RecyclerView 布局外部嵌套一層 SwipeRefreshLayout 布局即可。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="visible"/>

    </android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

2.在 Activity 中獲取 SwipeRefreshLayout 控件并且設置 OnRefreshListener 監聽器,同時實現里邊的 onRefresh() 方法,在該方法中進行網絡請求最新數據,然后刷新 RecyclerView 列表同時設置 SwipeRefreshLayout 的進度Bar的隱藏或者顯示效果。具體代碼如下:

mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
          @Override
          public void onRefresh() {
              addTopData();
          }
      });

  private void addTopData() {
           new Handler().postDelayed(new Runnable() {
               @Override
               public void run() {
                   for (int i = 0; i < 5; i++) {
                       mData.add(i, new ShareBean(R.mipmap.ic_image_h, "下拉刷新數據" + i));
                   }
                   mAdapter.notifyDataSetChanged();
                   mSwipeRefreshLayout.setRefreshing(false);
               }
           }, 1000);
       }

RecyclerView 實現上拉加載

滾動事件的分析

1.RecyclerView 本身已經提供了滑動的監聽接口,OnScrollListener,這個接口包含了以下的方法,代碼注釋介紹。

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      //當recycleView的滑動狀態改變時回調
       @Override
       public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);
       }

      //當RecycleView滑動之后被回調
       @Override
       public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
           super.onScrolled(recyclerView, dx, dy);
      }
});

2.RecyclerView 的滑動狀態

  • 當前的recycleView不滑動(滑動已經停止時):
    public static final int SCROLL_STATE_IDLE = 0;

  • 當前的recycleView被拖動滑動:
    public static final int SCROLL_STATE_DRAGGING = 1;

  • 當前的recycleView在滾動到某個位置的動畫過程,但沒有被觸摸滾動.調用 scrollToPosition(int) 應該會觸發這個狀態:
    public static final int SCROLL_STATE_SETTLING = 2;

3.滑動位置的監聽
3.1 以下是最簡單的頂部/底部的判斷方式:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      //當recycleView的滑動狀態改變時回調
       @Override
       public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
           super.onScrollStateChanged(recyclerView, newState);

          //獲取最后一個可見view的位置
          int lastItemPosition = linearManager.findLastVisibleItemPosition();
          //獲取第一個可見view的位置
          int firstItemPosition =linearManager.findFirstVisibleItemPosition();

          if (newState == RecyclerView.SCROLL_STATE_IDLE && lastItemPosition + 1 == adapter.getItemCount()) {
          //最后一個itemView的position為adapter中最后一個數據時,說明該itemView就是底部的view了
          //需要注意position從0開始索引,adapter.getItemCount()是數據量總數
          }

          //同理檢測是否為頂部itemView時,只需要判斷其位置是否為0即可
          if (newState == RecyclerView.SCROLL_STATE_IDLE && firstItemPosition == 0) {}
       }
});

以上的實現方式存在諸多問題,比如:

  • itemView會在滑動過程中只顯示一部分或者一半
  • 檢測數據不夠時的RecycleView

3.2 對于顯示不全的問題
也就是說:當某一個itemView只顯示一部分的時候,此時已經算是一個position了,此時很可能去觸發判斷條件。所以官方 Api 里有一下兩個方法:

  • findFirstCompletlyVisibleItemPosition()
  • findLastCompletlyVisibleItemPosition()

就可以盡可能的避免這種問題。

3.3 檢測數據不夠時的狀態

如果為了避免這種情況,我們就需要對在檢測時多進行一步,若當前的RecycleView顯示的itemView不滿屏的情況下,其實并不存在滑動的說法(沒有加載更多,因為根本數據還沒有滿屏顯示),至于下拉刷新的,還是可以的,但一般都會使用SwipeRefreshLayout實現下拉刷新了.因此這里主要考慮的是關于滑動到底部加載更多的問題.

3.4 上拉加載更多代碼實現

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dy > 0) {//向下滾動
            int visibleItemCount = mLayoutManager.getChildCount();
            int totalItemCount = mLayoutManager.getItemCount();
            int pastVisiblesItems = mLayoutManager.findFirstVisibleItemPosition();

            if (!loading && (visibleItemCount + pastVisiblesItems) == totalItemCount) {
                loading = true;
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 5; i++) {
                            mData.add(new ShareBean(R.mipmap.ic_image_s, "上拉加載數據" + i));
                        }
                        mAdapter.notifyDataSetChanged();
                        loading = false;
                    }
                }, 3000);
            }
        }
    }
});
添加 FooterView 實現上拉加載

接下來完善UI,讓用戶知道確實在上拉加載的過程。用RecyclerView實現,使用 getItemType() 方法,就與 ListView 差不多了。不說多了,直接上完整代碼(注釋很清晰):

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    public  OnItemClickListener itemClickListener;
    private List<ShareBean> mData = null;
    private LayoutInflater mInflater;

    //1.加入布局狀態標志-用來判斷此時加載是普通Item還是footView
    //沒有更多了
    public static final int NO_DATA_MORE = 0;
    //上拉加載更多
    public static final int PULLUP_LOAD_MORE = 1;
    //正在加載中
    public static final int LOADING_MORE = 2;

    private int load_more_status = 0;

    private static final int TYPE_ITEM = 0;  //普通Item View
    private static final int TYPE_FOOTER = 1;  //頂部FootView

    public RecyclerAdapter(Context context, List<ShareBean> mData) {
        this.mData = mData;
        this.mInflater = LayoutInflater.from(context);
    }

    //4.接著onCreateViewHolder(ViewGroup parent,int viewType)加載布局的時候根據viewType的類型來選擇指定的布局創建,返回即可:
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//        將布局轉化為View并傳遞給Recycler封裝好的ViewHolder
        if (viewType == TYPE_ITEM) {
            View view = mInflater.inflate(R.layout.item_recyclerview, parent, false);
            return new ItemViewHolder(view);
        } else if (viewType == TYPE_FOOTER) {
            View foot_view = mInflater.inflate(R.layout.part_footer, parent, false);
            return new BottomViewHolder(foot_view);
        }
        return null;
    }

    //5.最后進行判斷數據的時候(onBindViewHolder),判斷holder的類型來進行判定數據即可.
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ItemViewHolder) {
            ((ItemViewHolder) holder).mImageView.setImageResource(mData.get(position).getImage_id());
            ((ItemViewHolder) holder).mTextView.setText(mData.get(position).getName());
        } else if (holder instanceof BottomViewHolder) {
            BottomViewHolder footViewHolder = (BottomViewHolder) holder;
            switch (load_more_status) {
                case PULLUP_LOAD_MORE:
                    footViewHolder.mFooterTv.setText("上拉加載更多...");
                    footViewHolder.mProgressBar.setVisibility(View.VISIBLE);
                    break;
                case LOADING_MORE:
                    footViewHolder.mFooterTv.setText("正在加載中...");
                    footViewHolder.mProgressBar.setVisibility(View.VISIBLE);
                    break;
                case NO_DATA_MORE:
                    footViewHolder.mFooterTv.setText("----我是有底線的----");
                    footViewHolder.mProgressBar.setVisibility(View.GONE);
                    break;
            }
        }
    }

    //3.重寫getItemViewType方法來判斷返回加載的布局的類型
    @Override
    public int getItemViewType(int position) {
        if (position + 1 == getItemCount()) {
            return TYPE_FOOTER;
        } else {
            return TYPE_ITEM;
        }
    }

    //2.返回item數量
    @Override
    public int getItemCount() {
        return mData.size() + 1;
    }

    /**
     * //上拉加載更多
     * PULLUP_LOAD_MORE=0;
     * //正在加載中
     * LOADING_MORE=1;
     * //加載完成已經沒有更多數據了
     * NO_MORE_DATA=2;
     *
     * @param status
     */
    public void changeMoreStatus(int status) {
        load_more_status = status;
        notifyDataSetChanged();
    }

    public  class ItemViewHolder extends RecyclerView.ViewHolder {

        CardView mCardView;
        CircleImageView mImageView;
        TextView mTextView;

        public ItemViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(R.id.textView);
            mCardView = (CardView) itemView.findViewById(R.id.cardView);
            mImageView = (CircleImageView) itemView.findViewById(R.id.image);
            mCardView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (itemClickListener != null) {
                        itemClickListener.onItemClick(view, getPosition());
                    }
                }
            });
        }
    }

     /**
     * 底部FootView布局
     */
    public class BottomViewHolder extends RecyclerView.ViewHolder {

        ProgressBar mProgressBar;
        TextView mFooterTv;

        public BottomViewHolder(View itemView) {
            super(itemView);
            mProgressBar = (ProgressBar) itemView.findViewById(R.id.progress_bar);
            mFooterTv = (TextView) itemView.findViewById(R.id.footer_tv);
        }
    }

    public void setOnItemClickListener(OnItemClickListener itemClickListener) {
        this.itemClickListener = itemClickListener;
    }

}

終于分析完了,感覺滑動需要繼續探究,,,嗯嗯,先就這樣吧。


參考文章及總結

以上說對基礎知識的一些梳理,存在諸多不足,請多指正。

主要參考文章:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容