仿新浪個人資訊頁佈局
最近專案中需要一個類似微博個人資訊頁的佈局,網上找了一些資料但都不符合自己的需求,故自己實現一個。
一.模仿佈局樣式原型:
上圖是新浪微博個人資訊頁的整體佈局,可以看到整個佈局是一個可以滾動列表結構,上邊是一個個人資訊佈局和一個tab選擇欄佈局,看到這個佈局之後我們可以想到用scrollview+listview來完成,但由於使用這種巢狀方式之後需重寫listview高度而導致item無法複用,故我們使用另一種方式來完成,也就是RecyclerView充當整個佈局的方式,個人資訊佈局和tab選擇欄作為header插入到列表中。
下邊來讓我們思考一下即將實現佈局將會遇到的問題:
1.RecyclerView如何加header和footer進去?
2.怎樣做到懸浮tab欄效果?
3.怎麼在同一個列表中載入不同的樣式?
4.如何在點tab欄切換資料的時候保持列表處於上一次的位置?
問題一:RecyclerView如何加header和footer進去?
我們都知道RecylerView不像Listview一樣有addheaderview和addfooterview兩個方法,既然沒有這兩個方法我們就需要自己去寫這兩個方法並且去實現了,我們知道RecyclerView的adapter中有一個叫getItemViewType(int position)的方法,我們可以通過重寫這個方法來讓RecyclerView的item根據條件不同載入不同種類的佈局,那我們便可以通過重寫這個方法將header和footer作為item項加入到RecyclerView中從而達到加header和footer的效果。介面卡的程式碼如下:
package com.ifeng.art.common.widget.recyclerview;
import android.content.Context;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lvzishen on 16/10/8.
*/
public abstract class WrapperRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
protected List list = new ArrayList<>();
protected Context context;
// /**
// * 頭佈局
// */
// private View headView;
// /**
// * 頭佈局Tab
// */
// private View headViewTab;
/**
* 頭佈局
* key: Integer; value: object
*/
private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
/**
* 頭佈局Tab
*/
private View headViewTab;
/**
* 腳佈局
*/
private FooterView footView;
// /**
// * 頭佈局數量
// */
// public int headViewCount = 0;
/**
* 腳佈局數量
*/
public int footViewCount = 0;
public int getFootViewCount() {
return footViewCount;
}
private static final int TYPE_HEADER = 1;
private static final int TYPE_FOOTER = 3;
private FootViewHolder footViewHolder;
public void setList(List listData) {
this.list.clear();
this.list.addAll(listData);
this.notifyDataSetChanged();
}
public WrapperRecyclerAdapter(Context context) {
this.context = context;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (mHeaderViews.get(viewType) != null) {
return new HeadViewHolder(mHeaderViews.get(viewType));
}
else if (viewType == TYPE_FOOTER) {
return footViewHolder;
} else {
return onCreateViewHolderNormal(parent, viewType);
}
}
public abstract RecyclerView.ViewHolder onCreateViewHolderNormal(ViewGroup parent, int viewType);
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
position = getRealPosition(position);
onBindViewHolderNormal(holder, position);
}
public abstract void onBindViewHolderNormal(RecyclerView.ViewHolder holder, int position);
@Override
public int getItemCount() {
return list.size() + mHeaderViews.size() + footViewCount;
}
public void addHeaderView(View head) {
this.mHeaderViews.put(mHeaderViews.size() + TYPE_HEADER, head);
notifyItemInserted(mHeaderViews.size() - 1);
}
public void addFooterView(FooterView footView) {
this.footView = footView;
footViewCount++;
footViewHolder = new FootViewHolder(footView);
notifyItemInserted(getItemCount() - 1);
}
public void removeFooterView() {
if (footView != null) {
footViewCount = 0;
footViewHolder = null;
footView = null;
notifyItemRemoved(getItemCount() - 1);
}
}
public void removeHeaderView() {
if (mHeaderViews.size() > 0) {
// headViewCount = 0;
mHeaderViews.clear();
for (int i = 0; i < mHeaderViews.size(); i++) {
notifyItemRemoved(i);
}
}
}
@Override
public int getItemViewType(int position) {
if (position < mHeaderViews.size()) {
return mHeaderViews.keyAt(position);
}
if (position == getItemCount() - 1 && footView != null) {
//最後一個,應該載入Footer
return TYPE_FOOTER;
}
return getItemViewTypeNormal(getRealPosition(position));
}
public abstract int getItemViewTypeNormal(int position);
/**
* 得到真實的position
*
* @return
*/
private int getRealPosition(int position) {
return position = position - mHeaderViews.size();
}
public void setState(int status) {
if (footViewHolder != null) {
footView.setState(status);
}
}
/**
* @param errorListener 腳佈局載入失敗
*/
public void setErrorListener(FooterView.ErrorListener errorListener) {
footView.setOnErrorListener(errorListener);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int spanSize = 1; //每個griditem 佔1份 一行有3個網格item 每個spansize為1 一個item佔滿則需要返回3
if (position == 0 && mHeaderViews != null && mHeaderViews.size() > 0) {
return gridLayoutManager.getSpanCount();
}
if (position == getItemCount() - 1 && footView != null && footViewCount > 0) {
return gridLayoutManager.getSpanCount();
}
return spanSize;
}
});
}
}
public int getState() {
if (footView != null) {
return footView.getStatus();
}
return 0;
}
class HeadViewHolder extends RecyclerView.ViewHolder {
public HeadViewHolder(View itemView) {
super(itemView);
}
}
class FootViewHolder extends RecyclerView.ViewHolder {
public FootViewHolder(View itemView) {
super(itemView);
}
}
}
這裡講的不是很詳細,只是給了大家思路並且簡單的實現了一下效果,因為網上已經有很多關於這方面的資料了,如果有不明白的地方可以評論留言給我或者自行查閱資料。這個adapter中我還加入了一些關於footer狀態的地方,是因為我們要控制footer去根據不同的狀態載入不同的佈局型別,具體可以看最後的demo程式碼。
問題二:怎樣做到懸浮tab欄效果?
可以看到當我們向上拖動列表的時候會出現一個跟頭佈局相同的tab懸浮欄懸浮在最上方。這裡我們採用一個簡單的方法,RecyclerView中可以通過設定 RecyclerView.OnScrollListener這個介面去監聽到RecyclerView的滾動狀態,第一個顯示的item,總共顯示的item個數等等,我們可以利用這個抽象類去達到效果。
具體的思路為在RecyclerView的根佈局中加一個和headview tab欄中佈局相同的tab,並且隱藏這個tab。當我們滾動列表時,第一個可見的item為headview tab的位置的時候(此佈局position為1)我們就將隱藏的tab顯示出來從而給使用者造成一個tab懸浮在最上層的假象。因為我們這個隱藏顯示的tab和headview中的tab並不是同一個tab,所以當我們改變其中一個tab的狀態的時候要對應的改變另一個tab,這樣才能讓資料同步。下邊給出OnScrollListener的程式碼:
package com.ifeng.art.common.widget.recyclerview;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
/**
* Created by lvzishen on 16/10/9.
*/
public abstract class RecyclerViewLoadData extends RecyclerView.OnScrollListener {
private RecyclerView.LayoutManager layoutManager;
private int lastVisibleItem;
private int firstVisbleItem;
private int totalItemCount;
private int visibleItemCount;
public RecyclerViewLoadData(RecyclerView.LayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 獲取到最後一個可見的item
if (layoutManager instanceof LinearLayoutManager) {
lastVisibleItem = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
//得到當前顯示的第一個item的位置
firstVisbleItem = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
//如果第一個可見的item為大於等於1則顯示懸浮tab
if (firstVisbleItem >= 1) {
showTab();
} else {
hideTab();
}
getFirstVisiblePosition(firstVisbleItem);
}
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItem = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 獲取item的總數
totalItemCount = layoutManager.getItemCount();
//可見的item數
visibleItemCount = layoutManager.getChildCount();
// Log.e("totalItemCount", totalItemCount + "");
// Log.e("visibleItemCount", visibleItemCount + "");
if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem >= totalItemCount - 1 && visibleItemCount > 0) {
// 此處呼叫載入更多回調介面的回撥方法
int state = RecyclerViewStateUtils.getFooterViewState(recyclerView);
if (state == FooterView.Loading || state == FooterView.TheEnd) {
Log.d("state not allow", "the state is Loading/The End, just wait..");
return;
}
loadMoreData(recyclerView);
}
}
public abstract void loadMoreData(RecyclerView recyclerView);
public abstract void getFirstVisiblePosition(int firstVisiblePosition);
public abstract void showTab();
public abstract void hideTab();
}
問題三:怎麼在同一個列表中載入不同的樣式?
這個問題的答案和我們第一個加headview和footview的思路相同,再次不再多說,要注意adapter中並不是只能載入一種型別的資料,我們可以根據載入的資料型別不同載入不同型別的佈局從而達到像微博列表中那樣的一個列表是正常的列表佈局(多個list集合,根據tab的切換讓adapter載入不同的list集合),一個列表是網格形佈局的情況,其實只不過是載入了不同的佈局(第一種是載入正常佈局item,第二種是載入三個imageview並排的item),下邊給出介面卡的程式碼:
package com.ifeng.art.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.ifeng.art.R;
import com.ifeng.art.common.widget.recyclerview.WrapperRecyclerAdapter;
import com.ifeng.art.entity.UserNickname;
import com.ifeng.art.entity.UserNumber;
/**
* Created by lvzishen on 16/11/2.
*/
public class DemoWrapperRecyclerAdapter extends WrapperRecyclerAdapter {
private final int TYPE_ONE = 10;
private final int TYPE_TWO = 11;
public DemoWrapperRecyclerAdapter(Context context) {
super(context);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolderNormal(ViewGroup parent, int viewType) {
if (viewType == TYPE_ONE) {
View view = LayoutInflater.from(context).inflate(R.layout.item_normal, parent, false);
return new NormalHolder(view);
}
if (viewType == TYPE_TWO) {
View view2 = LayoutInflater.from(context).inflate(R.layout.item_normal_two, parent, false);
return new NormalHolder2(view2);
}
return null;
}
@Override
public void onBindViewHolderNormal(RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof NormalHolder) {
final UserNumber art = (UserNumber) list.get(position);
((NormalHolder) holder).textView.setText(art.follows);
((NormalHolder) holder).textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("position", position + "");
}
});
}
if (holder instanceof NormalHolder2) {
final UserNickname huodong = (UserNickname) list.get(position);
((NormalHolder2) holder).textView.setText(huodong.uname);
// Glide.with(context)
// .load(huodong.image)
// .placeholder(R.color.default_image)
// .into(((NormalHolder2) holder).imageView);
((NormalHolder2) holder).textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("position", position + "");
}
});
}
}
@Override
public int getItemViewTypeNormal(int position) {
if (list.get(position) instanceof UserNickname) {
return TYPE_TWO;
} else {
return TYPE_ONE;
}
}
class NormalHolder extends RecyclerView.ViewHolder {
TextView textView;
public NormalHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.item_textview);
}
}
class NormalHolder2 extends RecyclerView.ViewHolder {
TextView textView;
ImageView imageView;
public NormalHolder2(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.item_textview);
imageView = (ImageView) itemView.findViewById(R.id.image);
}
}
}
問題四:如何在點tab欄切換資料的時候保持列表處於上一次的位置?
當我們點選tab欄切換不同的集合時需要保持列表處於上一次顯示的位置,我們可以獲取到第一個可見的item位置並且記錄下來,然後使用scrollToPositionWithOffset(int position, int offset)這個方法來讓RecyclerView去切換到指定的位置上,其中的邏輯判斷比較繁瑣,具體的邏輯可以看最後demo中的程式碼。