1. 前言
我们先看看头条、搜狐新闻的下拉更新效果(视频转gif时,有些frame失真,上滑加载的效果没贴,太占地了😅):
头条-下拉更新 搜狐-下拉更新.gif
看过头条、搜狐新闻的下拉更新效果后,我们看看自个写的的TRecyclerView的下拉更新、上滑加载的效果图,下面也给出TRecyclerView的下载地址:
TRecyclerView-下拉更新 TRecyclerView-上滑加载附:TRecyclerView项目地址:TRecyclerView。
实现上面的效果,我们肯定得有一个托盘,假设是TRecyclerView,然后拖盘上面放了一个RecyclerView,下拉托盘超过一定距离后,LoadingView显示出来了,数据更新完后有一个更新多少条的提示,假设是TipView。
TRecyclerView包括LoadingView、RecyclerView、TipView,下面来讲讲这三个View的层次。下拉TRecyclerView,会露出LoadingView,可知LoadingView所处的层次是最下面。
在试头条、搜狐新闻下拉更新时,当列表正处在更新状态,这个时候,我们上推RecyclerView到顶,这个时候更新多少条的提示TipView会盖在RecyclerView上面,可知TIpView所处的层次是最上面。
通过上面分析TRecyclerView中各个View的层次从上到下依次是:
TipView(顶部) 、 RecyclerView(中间) 、 LoadingView(底部)。
知道View的层次后,我们看看TRecyclerView下拉更新是怎么实现的。
2. 下拉更新
我们结合TRecyclerView的header结构图,来分析下拉更新数据时,RecyclerView的三个动作行为:
header结果图1) 下拉高度超过mHeaderHeight,松手之后,RecyclerView回到mHeaderHeight位置,同时请求网络数据;
2)网络数据回来之后,RecyclerView回到mTipHeight位置,同时展示tips更新提示动画;
3) tips更新提示动画结束后,RecyclerView回到顶部位置。
由此可知:RecyclerView整个下拉更新的动画从时序上可以分为下面三个部分:
animToHeader (更新数据) -> animToTip (展示tips动画) -> animToStart (回顶)
因此,我们要在TRecyclerView的onInterceptTouchEvent、onTouchEvent方法做一些事情:
1)onInterceptTouchEvent:判断是否拦截MotionEvent事件,事件交给TRecyclerView或者RecyclerView处理。
2)onTouchEvent:处理RecyclerView的下拉动画,RecyclerView下拉是否触发更新的逻辑。
下面还是看看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已经处在顶部,不能再向下滚动时,这个时候,事件交由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处理,让其可以继续上滑。
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,松手则触发刷新逻辑,反之,触发RecyclerView的回顶动画。
2)触发刷新的逻辑是在animToHeader动画结束之后做的,onAnimationEnd回调里面调用了 mPullRefresh.pullRefresh(),业务逻辑可以通过该接口处理数据请求的逻辑。
animToHeader
//the anim which moves to header position,w hen 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处理,让其可以继续上滑。
下拉刷新讲的差不多了,我们来看看上滑加载的实现。
3. TRecyclerView构成
下面会结合这TRecyclerView的结构、TRecyclerAdapter的实现来讲讲TRecyclerView上滑加载数据的原理。
TRecyclerView 的结构:
TRecycleView是一个FrameLayout主要包括两部分,Header View和RecycleView,而RecycleView的View类型大体分为两部分:Normal View和Footer View。
TRecyclerView中有一个TRecyclerAdapter,是用来加载RecyclerView的Item View,是TRecyclerView中真正加载数据的Adapter,其中包括两大类的数据类型,即正常的Normal View和Header View,Normal View是通过RecyclerView.Adapter来加载,就是我们需要写的Adapter。
TRecyclerView的结构图TRecyclerView的初始化
下面结合TRecyclerView的结构图,我们看看具体的代码实现,首先是TRecycleView的构造方法:
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的实现
我们知道TRecyclerView中真正加载数据的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注册了一个观察者,调用RecyclerView.Adapter的数据更新方法时,会通知TRecyclerAdapter去更新数据数据,代码如下:
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方法的实现。
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类型,则直接返回,否则调用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);
}
}
4. TRecyclerView上滑加载数据
看上面的结构图,我们知道Footer View并不是直接作为TRecyclerView的一个View,而是RecyclerView的一个Item View。
因此,当RecyclerView上滑到最后一个Item View,即Footer View可见时,我们可以通过 mPushRefresh.loadMore()来处理上滑加载数据的逻辑,代码的实现如下:
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是否已经滑动到底部,如果是则执行loadMore方法回调
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (targetInBottom()) {
if(mPushRefresh != null){
mLoadMore = true;
mPushRefresh.loadMore();
}
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
}
//滑动到底部,且最后一个元素可见,则认为到达底部
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;
}
我们自个写了一个NewsRecyclerAdapter,通过TRecyclerView实行了数据的下拉更新,上滑加载的逻辑,下面给出NewsRecyclerAdapter的源码:
public class NewsRecyclerAdapter extends BaseRecyclerAdapter<NewsItem> {
private Context mCtx;
private static final int NEWS_ITEM_TYPE_PIC = 1;
private static final int NEWS_ITEM_TYPE_NORMAL = 2;
public NewsRecyclerAdapter(Context context) {
super(context);
init(context);
}
private void init(Context ctx) {
mCtx = ctx;
}
@Override
protected BaseViewHolder createHolder(ViewGroup parent, int viewType, Context context) {
return buildHolder(parent, viewType, context);
}
@Override
protected void bindData(BaseViewHolder holder, int position) {
initData(holder, position);
}
private BaseViewHolder buildHolder(ViewGroup parent, int viewType, Context context) {
BaseViewHolder holder = null;
switch (viewType) {
case NEWS_ITEM_TYPE_PIC:
View itemView = LayoutInflater.from(context).inflate(
R.layout.item_pic_layout, parent, false);
holder = new BaseViewHolder(itemView);
break;
case NEWS_ITEM_TYPE_NORMAL:
View normalItemView = LayoutInflater.from(context).inflate(
R.layout.item_normal_layout, parent, false);
holder = new BaseViewHolder(normalItemView);
break;
default:
break;
}
return holder;
}
private void initData(BaseViewHolder holder, final int position) {
final int type = getItemViewType(position);
NewsItem item = getItem(position);
switch (type) {
case NEWS_ITEM_TYPE_PIC:
if (item != null) {
((TextView) holder.getView(R.id.title)).setText(item.mTitle);
((TextView) holder.getView(R.id.content)).setText(item.mContent);
((ImageView) holder.getView(R.id.img)).setImageResource(item.mResId);
}
break;
case NEWS_ITEM_TYPE_NORMAL:
if (item != null) {
((TextView) holder.getView(R.id.title)).setText(item.mTitle);
((TextView) holder.getView(R.id.content)).setText(item.mContent);
}
break;
default:
break;
}
}
@Override
public int getItemViewType(int position) {
int itemType = position % 2;
if(itemType == 0){
return NEWS_ITEM_TYPE_NORMAL;
}else{
return NEWS_ITEM_TYPE_PIC;
}
}
}
5. 总结
在写TRecyclerView遇到TRecyclerView中的RecyclerView没有滚动条,这是因为我们是直接new RecyclerView,RecyclerView的一些初始化方法没有执行到,如受保护的initializeScrollbars 方法,在外部无法调用到的。
解法方法:RecyclerView通过inflate的方式去加载一个xml文件。
TRecyclerView项目地址:TRecyclerView。
网友评论