1. 前言
我們先看看頭條、搜狐新聞的下拉更新效果(視頻轉(zhuǎn)gif時浪南,有些frame失真笼才,上滑加載的效果沒貼,太占地了??):
看過頭條络凿、搜狐新聞的下拉更新效果后骡送,我們看看自個寫的的TRecyclerView的下拉更新、上滑加載的效果圖絮记,下面也給出TRecyclerView的下載地址:
附:TRecyclerView項目地址:TRecyclerView摔踱。
實現(xiàn)上面的效果,我們肯定得有一個托盤到千,假設是TRecyclerView,然后拖盤上面放了一個RecyclerView赴穗,下拉托盤超過一定距離后憔四,LoadingView顯示出來了,數(shù)據(jù)更新完后有一個更新多少條的提示般眉,假設是TipView了赵。
TRecyclerView包括LoadingView、RecyclerView甸赃、TipView柿汛,下面來講講這三個View的層次。下拉TRecyclerView,會露出LoadingView络断,可知LoadingView所處的層次是最下面裁替。
在試頭條、搜狐新聞下拉更新時貌笨,當列表正處在更新狀態(tài)弱判,這個時候,我們上推RecyclerView到頂锥惋,這個時候更新多少條的提示TipView會蓋在RecyclerView上面昌腰,可知TIpView所處的層次是最上面。
通過上面分析TRecyclerView中各個View的層次從上到下依次是:
TipView(頂部) 膀跌、 RecyclerView(中間) 遭商、 LoadingView(底部)。
知道View的層次后捅伤,我們看看TRecyclerView下拉更新是怎么實現(xiàn)的劫流。
2. 下拉更新
我們結(jié)合TRecyclerView的header結(jié)構(gòu)圖,來分析下拉更新數(shù)據(jù)時暑认,RecyclerView的三個動作行為:
1) 下拉高度超過mHeaderHeight困介,松手之后,RecyclerView回到mHeaderHeight位置蘸际,同時請求網(wǎng)絡數(shù)據(jù)座哩;
2)網(wǎng)絡數(shù)據(jù)回來之后,RecyclerView回到mTipHeight位置粮彤,同時展示tips更新提示動畫;
3) tips更新提示動畫結(jié)束后根穷,RecyclerView回到頂部位置。
由此可知:RecyclerView整個下拉更新的動畫從時序上可以分為下面三個部分:
animToHeader (更新數(shù)據(jù)) -> animToTip (展示tips動畫) -> animToStart (回頂)
因此导坟,我們要在TRecyclerView的onInterceptTouchEvent屿良、onTouchEvent方法做一些事情:
1)onInterceptTouchEvent:判斷是否攔截MotionEvent事件,事件交給TRecyclerView或者RecyclerView處理惫周。
2)onTouchEvent:處理RecyclerView的下拉動畫尘惧,RecyclerView下拉是否觸發(fā)更新的邏輯。
下面還是看看TRecyclerView的onInterceptTouchEvent方法和onTouchEvent方法。
onInterceptTouchEvent(MotionEvent ev) 方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
//if the recycleView can scroll,
//then the TRecyclerView doesn't intercept the event.
if (isUnIntercept() || mRefresh) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mIsDrag = false;
mInitY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float y = ev.getY();
//if the distance of moving is over the touchSlop,
//then The TRecyclerView is dragged.
if (y - mInitY >= mTouchSlop && !mIsDrag) {
mIsDrag = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDrag = false;
break;
default:
break;
}
return mIsDrag;
}
看onInterceptTouchEvent的代碼盏求,其實是處理了兩個邏輯:
1)某些情況下不攔截event锣笨,把事件交給RecyclerView處理,只要RecyclerView 能夠滑動贰逾,就不攔截event;
2)如果RecyclerView已經(jīng)處在頂部菠秒,不能再向下滾動時疙剑,這個時候,事件交由TRecyclerView處理。
onTouchEvent(MotionEvent event) 方法:
public boolean onTouchEvent(MotionEvent event) {
if (isUnIntercept()) {
return false;
}
float dist = 0f;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsDrag = false;
break;
case MotionEvent.ACTION_MOVE:
if (mIsDrag) {
float y = event.getY();
dist = (y - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;
if(mCurrentTargetOffsetTop >= mOriginalOffsetTop) {
//如果下次移動的距離加上當前的距離頂部的距離
//小于header的初始位置言缤,則RecyclerView回頂,
// 同時檢查SuperSwipe是否移動頂部嚼蚀,RecycleView滑到頂部,
//則造一個down事件轧简,交給RecycleView處理驰坊,讓其可以繼續(xù)上滑。
if(dist < mOriginalOffsetTop ){
quickToStart();
buildDownEvent(event);
}else {
setTargetOffsetTopAndBottom(dist);
}
}else{
buildDownEvent(event);
}
//the distance of pull can trigger off refresh
if (mPullRefresh != null) {
mPullRefresh.pullRefreshEnable(dist >= mHeaderHeight);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
dist = (event.getY() - mInitY) *
TRecycleViewConst.PULL_DRAG_RATE;
if (mIsDrag) {
//if the distance of moving is over the header height ,
// then show the anim which moves to header position,
//else show the anim which moves to start position.
if (dist >= mHeaderHeight) {
animToHeader();
} else {
animToStart();
}
}
mIsDrag = false;
break;
}
return true;
}
我們庖丁解牛哮独,看看onTouchEvent的ACTION_UP和ACTION_MOVE的邏輯拳芙。
onTouchEvent - ACTION_UP
......
case MotionEvent.ACTION_CANCEL:
dist = (event.getY() - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;
if (mIsDrag) {
//if the distance of moving is over the header height ,
// then show the anim which moves to header position,
//else show the anim which moves to start position.
if (dist >= mHeaderHeight) {
animToHeader();
} else {
animToStart();
}
}
......
說明:
1)當TRecyclerView攔截了event事件后,如果下拉距離超過mHeaderHeight皮璧,松手則觸發(fā)刷新邏輯舟扎,反之,觸發(fā)RecyclerView的回頂動畫悴务。
2)觸發(fā)刷新的邏輯是在animToHeader動畫結(jié)束之后做的睹限,onAnimationEnd回調(diào)里面調(diào)用了 mPullRefresh.pullRefresh(),業(yè)務邏輯可以通過該接口處理數(shù)據(jù)請求的邏輯讯檐。
animToHeader
//the anim which moves to header position,
//when the anim is end, start to refresh data
private void animToHeader() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mRecyclerView, "translationY", mHeaderHeight);
animator.addListener(mToHeaderListener);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentTargetOffsetTop = (float) animation.getAnimatedValue();
Log.d(TAG, "animToHeader():" + "mCurrentTargetOffsetTop:" + mCurrentTargetOffsetTop);
}
});
animator.setDuration(AnimDurConst.ANIM_TO_HEADER_DUR);
animator.start();
}
private Animator.AnimatorListener mToHeaderListener
= new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
//when the anim of move to header is end, start to refresh data
if (mPullRefresh != null) {
mRefresh = true;
mPullRefresh.pullRefresh();
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
onTouchEvent - ACTION_MOVE
......
case MotionEvent.ACTION_MOVE:
if (mIsDrag) {
float y = event.getY();
dist = (y - mInitY) * TRecycleViewConst.PULL_DRAG_RATE;
if(mCurrentTargetOffsetTop >= mOriginalOffsetTop) {
if(dist < mOriginalOffsetTop ){
quickToStart();
buildDownEvent(event);
}else {
setTargetOffsetTopAndBottom(dist);
}
}else{
buildDownEvent(event);
}
......
}
break;
......
說明:
1)TRecyclerView滿足當前位置 mCurrentTargetOffsetTop大于mOriginalOffsetTop(默認是0)羡疗、下拉距離dist大于mOriginalOffsetTop這兩個條件,則通過setTranslationY來垂直向下移動RecyclerView别洪。
//move the target by setTranslationY
private void setTargetOffsetTopAndBottom(float offset) {
mRecyclerView.setTranslationY(offset);
mCurrentTargetOffsetTop = offset;
}
2)TRecyclerView如果當前位置mCurrentTargetOffsetTop大于mOriginalOffsetTop叨恨,但是下拉距離dist小于mOriginalOffsetTop或者mCurrentTargetOffsetTop小于mOriginalOffsetTop,則造一個down事件挖垛,交給RecycleView處理痒钝,讓其可以繼續(xù)上滑。
下拉刷新講的差不多了痢毒,我們來看看上滑加載的實現(xiàn)送矩。
3. TRecyclerView構(gòu)成
下面會結(jié)合這TRecyclerView的結(jié)構(gòu)、TRecyclerAdapter的實現(xiàn)來講講TRecyclerView上滑加載數(shù)據(jù)的原理哪替。
TRecyclerView 的結(jié)構(gòu):
TRecycleView是一個FrameLayout主要包括兩部分栋荸,Header View和RecycleView,而RecycleView的View類型大體分為兩部分:Normal View和Footer View凭舶。
TRecyclerView中有一個TRecyclerAdapter晌块,是用來加載RecyclerView的Item View,是TRecyclerView中真正加載數(shù)據(jù)的Adapter库快,其中包括兩大類的數(shù)據(jù)類型摸袁,即正常的Normal View和Header View钥顽,Normal View是通過RecyclerView.Adapter來加載义屏,就是我們需要寫的Adapter。
TRecyclerView的初始化
下面結(jié)合TRecyclerView的結(jié)構(gòu)圖,我們看看具體的代碼實現(xiàn)闽铐,首先是TRecycleView的構(gòu)造方法:
public TRecyclerView(Context context) {
super(context);
init(context);
}
private void init(Context ctx) {
mCtx = ctx;
mTouchSlop = ViewConfiguration.get(mCtx).getScaledTouchSlop();
initView();
}
private void initView() {
mHeaderHolder = new HeaderHolder(mCtx);
mHeaderHolder.setAnimListener(mAnimListener);
addProgressView();
addTargetView();
addTipView();
linearLayoutManager = new LinearLayoutManager(mCtx);
mRecyclerView.setLayoutManager(linearLayoutManager);
mRecyclerView.setVerticalScrollBarEnabled(true);
initListener();
}
//add progress view
private void addProgressView() {
mHeaderHeight = (int) mCtx.getResources().getDimension(R.dimen.header_height);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderHeight);
params.gravity = Gravity.TOP;
addView(mHeaderHolder.getProgressView(), params);
}
private void addTargetView() {
// mRecyclerView = new RecyclerView(mCtx);
mRecyclerView = (RecyclerView) LayoutInflater.from(mCtx).inflate(
R.layout.recycler_view, this, false);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addView(mRecyclerView, params);
}
// add tip view
private void addTipView() {
mTipHeight = (int) mCtx.getResources().getDimension(R.dimen.header_tip_height);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, mTipHeight);
params.gravity = Gravity.TOP;
addView(mHeaderHolder.getTipView(), params);
}
TRecyclerAdapter的實現(xiàn)
我們知道TRecyclerView中真正加載數(shù)據(jù)的Adapter是TRecyclerAdapter蝶怔,我們看看TRecyclerView設置RecyclerView.Adapter的API,代碼如下:
public void setAdapter(RecyclerView.Adapter adapter){
adapter.registerAdapterDataObserver(mDataObserver);
mTAdapter = new TRecyclerAdapter(mCtx, adapter);
mRecyclerView.setAdapter(mTAdapter);
}
我們給RecyclerView.Adapter注冊了一個觀察者兄墅,調(diào)用RecyclerView.Adapter的數(shù)據(jù)更新方法時踢星,會通知TRecyclerAdapter去更新數(shù)據(jù)數(shù)據(jù),代碼如下:
private RecyclerView.AdapterDataObserver mDataObserver
= new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
mTAdapter.notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
mTAdapter.notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
mTAdapter.notifyItemRangeChanged(positionStart , itemCount, payload);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mTAdapter.notifyItemRangeInserted(positionStart , itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mTAdapter.notifyItemRangeRemoved(positionStart , itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
mTAdapter.notifyItemMoved(fromPosition, toPosition );
}
};
再看看TRecyclerAdapter的onCreateViewHolder和onBindViewHolder方法的實現(xiàn)隙咸。
onCreateViewHolder方法:
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return buildHolder(parent, viewType);
}
private RecyclerView.ViewHolder buildHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder holder = null;
switch (viewType) {
case ITEM_TYPE_FOOTER:
//Footer View的類型
holder = new BaseViewHolder(mFooterHolder.getFooterView());
break;
default:
//Normal View 的類型
holder = mAdapter.onCreateViewHolder(parent, viewType);
break;
}
return holder;
}
@Override
public int getItemViewType(int position) {
if (isFooter(position)) {
//底部View
return ITEM_TYPE_FOOTER;
} else {
return mAdapter.getItemViewType(position);
}
}
onBindViewHolder方法:
//如果是Footer View類型沐悦,則直接返回,否則調(diào)用mAdapter的onBindViewHolder方法
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (isFooter(position)) {
return;
}
initData(holder, position);
}
private void initData(RecyclerView.ViewHolder holder, final int position) {
final int type = getItemViewType(position);
if (type != ITEM_TYPE_FOOTER) {
mAdapter.onBindViewHolder(holder, position);
}
}
通過上面的代碼五督,我們知道是在create view holder時藏否,通過判斷viewType來判斷:
1)如果viewType是ITEM_TYPE_FOOTER,則認為ViewHolder是Footer類型充包,否則是Normal ViewHolder副签;
2)mAdapter是暴露給外部的RecyclerView.Adapter,但是真正加載數(shù)據(jù)的Adapter是TRecyclerAdapter基矮。
4. TRecyclerView上滑加載數(shù)據(jù)
看上面的結(jié)構(gòu)圖淆储,我們知道Footer View并不是直接作為TRecyclerView的一個View,而是RecyclerView的一個Item View家浇。
因此本砰,當RecyclerView上滑到最后一個Item View,即Footer View可見時蓝谨,我們可以通過 mPushRefresh.loadMore()來處理上滑加載數(shù)據(jù)的邏輯灌具,代碼的實現(xiàn)如下:
private void initListener(){
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//如果RecyclerView的Scroll State是IDLE,我們判斷下RecyclerView
//是否已經(jīng)滑動到底部譬巫,如果是則執(zhí)行l(wèi)oadMore方法回調(diào)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (targetInBottom()) {
if(mPushRefresh != null){
mLoadMore = true;
mPushRefresh.loadMore();
}
}
}
}
});
}
//滑動到底部咖楣,且最后一個元素可見,則認為到達底部
private boolean targetInBottom() {
if (targetInTop()) {
return false;
}
RecyclerView.LayoutManager layoutManager =
mRecyclerView.getLayoutManager();
int count = mRecyclerView.getAdapter().getItemCount();
if (layoutManager instanceof LinearLayoutManager && count > 0) {
LinearLayoutManager linearLayoutManager
= (LinearLayoutManager) layoutManager;
if (linearLayoutManager.findLastVisibleItemPosition() == count - 1) {
return true;
}
}
return false;
}
說明:
上滑加載更多的原理很簡單芦昔,其實我們就是判斷RecyclerView的Footer View
是否可見诱贿,可見則觸發(fā)加載更多的回調(diào)。
5. 總結(jié)
在寫TRecyclerView遇到TRecyclerView中的RecyclerView沒有滾動條咕缎,這是因為我們是直接new RecyclerView珠十,RecyclerView的一些初始化方法沒有執(zhí)行到,如受保護的initializeScrollbars 方法凭豪,在外部無法調(diào)用到的焙蹭。
解法方法:RecyclerView通過inflate的方式去加載一個xml文件。
工程用到的其它文件NewsRecyclerAdapter嫂伞、LoadingView等等孔厉,大家可以去github地址下載拯钻,下面附有項目地址。
TRecyclerView項目地址:TRecyclerView撰豺。