写在前头:PullToRefresh这个下拉刷新上拉加载更多的控件相信大家并不陌生,Github上搜索也有很多相关的控件,并且star数量也不低,但是呢如果你想拥有很高的自由度的话,那么还是敌不过自己实现一个或者你能把开源的控件源码看明白然后基于上面自己修改成自己需要的样式,那么我就是干了这件事,在网上看到一篇博客,然后下载源码看了看,然后就想将它进行改造一番,那篇文章的地址我是真的找不到了,不是我自己揽功啊,【尴尬】那我就来介绍一下这个被我修改增强后的控件能干些什么啦。
好了,我们该说点正经的了,让我想想从哪儿开始~~~
先上一张效果图吧,其实也没啥好看的,就是个简单的列表
那来看看布局吧,更简单:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.zy.recyclerview.view.PullRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
RelativeLayout>
好了,重点来了,PullRecyclerView这个就是我们今天的主角,当然这个只是被封装好的使用组建,具体的控件的逻辑都在它的父类中,先来看看PullRecyclerView的代码:
/**
* RecyclerView中的所有方法都可以在此类中设置,暴露出去以供调用
*/
public class PullRecyclerView extends PullBaseView<RecyclerView> {
public PullRecyclerView(Context context) {
this(context, null);
}
public PullRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected RecyclerView createRecyclerView(Context context, AttributeSet attrs) {
/**
* 这里返回一个RecyclerView,添加到LinearLayout中
* 那么,如果你想使用ListView的话呢,那么你在这里返回就行了
* 当然需要修改父类中的泛型相关的地方(包括列表所使用到的Adapter)
*/
return new RecyclerView(context, attrs);
}
public void setAdapter(RecyclerView.Adapter adapter) {
mRecyclerView.setAdapter(adapter);
}
public void setLayoutManager(RecyclerView.LayoutManager manager) {
mRecyclerView.setLayoutManager(manager);
}
}
接下来我们顺着来看看它的父类PullBaseView中的具体逻辑,我会在相关的代码的地方添加详细的注释的。
public abstract class PullBaseView<T extends RecyclerView> extends LinearLayout {
//PullBaseView是继承LinearLayout,所以我们的头尾布局和RecycleView都是通过addView方法添加的
protected T mRecyclerView;
private boolean isCanScrollAtRereshing = false;//刷新时是否可滑动
private boolean isCanPullDown = true;//是否可下拉
private boolean isCanPullUp = true;//是否可上拉
// pull state
private static final int PULL_UP_STATE = 0;
private static final int PULL_DOWN_STATE = 1;
// refresh states
private static final int PULL_TO_REFRESH = 2;
private static final int RELEASE_TO_REFRESH = 3;
private static final int REFRESHING = 4;
//记住上次落点的坐标
private int mLastMotionY;
//headerview-头布局
private BaseHeaderOrFooterView mHeaderView;
//footerview-尾布局
private BaseHeaderOrFooterView mFooterView;
//头状态
private int mHeaderState;
//尾状态
private int mFooterState;
//下拉状态
private int mPullState;
//刷新接口-提供下拉刷新+上拉加载的回调方法
private OnRefreshListener refreshListener;
public PullBaseView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public PullBaseView(Context context) {
super(context);
}
/**
* init-初始化方法,为我们的RecyclerView做一些必要的初始化工作
*/
private void init(Context context, AttributeSet attrs) {
//通过回调方法获得一个RecyclerView对象
mRecyclerView = createRecyclerView(context, attrs);
//设置RecyclerView全屏显示
mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
//这里仅仅添加了一个RecyclerView只是做占位使用,在我们具体设置头布局的时候
//会清空LinearLayout中所有的View,重新添加头布局,然后添加RecyclerView
addView(mRecyclerView);
}
/**
* 当view渲染完成后回调此方法,原先在此方法中初始化了尾布局,现在暂时废弃不用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
//刷新时禁止滑动
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (!isCanScrollAtRereshing) {
if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
return true;
}
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 判断是否应该到了父View,即PullToRefreshView滑动
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
int y = (int) e.getRawY();
int x = (int) e.getRawX();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// 首先拦截down事件,记录y坐标
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
// deltaY > 0 是向下运动,< 0是向上运动
int deltaY = y - mLastMotionY;
if (isRefreshViewScroll(deltaY)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return false;
}
/*
* 如果在onInterceptTouchEvent()方法中没有拦截(即onInterceptTouchEvent()方法中 return
* false)PullBaseView 的子View来处理;否则由下面的方法来处理(即由PullToRefreshView自己来处理)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaY = y - mLastMotionY;
if (isCanPullDown && mPullState == PULL_DOWN_STATE) {
//头布局准备刷新
headerPrepareToRefresh(deltaY);
} else if (isCanPullUp && mPullState == PULL_UP_STATE) {
//尾布局准备加载
footerPrepareToRefresh(deltaY);
}
mLastMotionY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//当我们的手指离开屏幕的时候,应该判断做什么处理
int topMargin = getHeaderTopMargin();
if (isCanPullDown && mPullState == PULL_DOWN_STATE) {
if (topMargin >= 0) {
// 开始刷新
headerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderView.getViewHeight());
}
} else if (isCanPullUp && mPullState == PULL_UP_STATE) {
if (Math.abs(topMargin) >= mHeaderView.getViewHeight() + mFooterView.getViewHeight()) {
// 开始执行footer 刷新
footerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderView.getViewHeight());
}
}
break;
}
return super.onTouchEvent(event);
}
/**
* 是否应该到了父View,即PullToRefreshView滑动
*
* @param deltaY , deltaY > 0 是向下运动,< 0是向上运动
* @return
*/
private boolean isRefreshViewScroll(int deltaY) {
if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
return false;
}
if (deltaY >= -20 && deltaY <= 20)
return false;
if (mRecyclerView != null) {
// 子view(ListView or GridView)滑动到最顶端
if (deltaY > 0) {
View child = mRecyclerView.getChildAt(0);
if (child == null) {
// 如果mRecyclerView中没有数据,不拦截
return false;
}
if (isScrollTop() && child.getTop() == 0) {
//如果滑动到了顶端,要拦截事件交由自己处理
mPullState = PULL_DOWN_STATE;
return true;
}
int top = child.getTop();
int padding = mRecyclerView.getPaddingTop();
if (isScrollTop() && Math.abs(top - padding) <= 8) {// 这里之前用3可以判断,但现在不行,还没找到原因
mPullState = PULL_DOWN_STATE;
return true;
}
} else if (deltaY < 0) {
View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
if (lastChild == null) {
// 如果mRecyclerView中没有数据,不拦截
return false;
}
// 最后一个子view的Bottom小于父View的高度说明mRecyclerView的数据没有填满父view,
// 等于父View的高度说明mRecyclerView已经滑动到最后
if (lastChild.getBottom() <= getHeight() && isScrollBottom()) {
mPullState = PULL_UP_STATE;
return true;
}
}
}
return false;
}
/**
* 判断mRecyclerView是否滑动到顶部
*
* @return
*/
boolean isScrollTop() {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (linearLayoutManager.findFirstVisibleItemPosition() == 0) {
return true;
} else {
return false;
}
}
/**
* 判断mRecyclerView是否滑动到底部
*
* @return
*/
boolean isScrollBottom() {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (linearLayoutManager.findLastVisibleItemPosition() == (mRecyclerView.getAdapter().getItemCount() - 1)) {
return true;
} else {
return false;
}
}
/**
* header 准备刷新,手指移动过程,还没有释放
*
* @param deltaY ,手指滑动的距离
*/
private void headerPrepareToRefresh(int deltaY) {
int newTopMargin = changingHeaderViewTopMargin(deltaY);
// 当headerview的topMargin>=0时,说明已经完全显示出来了,修改header view的提示状态
if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) {
//调用我们自定义头布局的释放刷新操作,具体代码在自定义headerview中实现
mHeaderView.releaseToRefreshOrLoad();
mHeaderState = RELEASE_TO_REFRESH;
} else if (newTopMargin < 0 && newTopMargin > -mHeaderView.getViewHeight()) {// 拖动时没有释放
//调用我们自定义头布局的下拉刷新操作,具体代码在自定义headerview中实现
mHeaderView.pullToRefreshOrLoad();
mHeaderState = PULL_TO_REFRESH;
}
}
/**
* footer准备刷新,手指移动过程,还没有释放 移动footerview高度同样和移动header view
* 高度是一样,都是通过修改headerview的topmargin的值来达到
*
* @param deltaY ,手指滑动的距离
*/
private void footerPrepareToRefresh(int deltaY) {
int newTopMargin = changingHeaderViewTopMargin(deltaY);
// 如果header view topMargin 的绝对值大于或等于header + footer 的高度
// 说明footer view 完全显示出来了,修改footer view 的提示状态
if (Math.abs(newTopMargin) >= (mHeaderView.getViewHeight() + mFooterView.getViewHeight()) && mFooterState != RELEASE_TO_REFRESH) {
//调用我们自定义尾布局的释放加载操作,具体代码在自定义footerview中实现
mFooterView.releaseToRefreshOrLoad();
mFooterState = RELEASE_TO_REFRESH;
} else if (Math.abs(newTopMargin) < (mHeaderView.getViewHeight() + mFooterView.getViewHeight())) {
//调用我们自定义尾布局的上拉加载操作,具体代码在自定义footerview中实现
mFooterView.pullToRefreshOrLoad();
mFooterState = PULL_TO_REFRESH;
}
}
/**
* 修改Headerview topmargin的值
*
* @param deltaY
* @description
*/
private int changingHeaderViewTopMargin(int deltaY) {
LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
float newTopMargin = params.topMargin + deltaY * 0.3f;
//此处要做的事情是通过布局返回一个百分比大小,供有的头布局动画使用
if ((mHeaderView.getViewHeight() + params.topMargin) <= mHeaderView.getViewHeight()) {
//如果我们头布局的高度是正值,params.topMargin是负值
//当头布局从完全隐藏到刚好显示的过程是0~mHeaderView.getViewHeight()的过程
//所以用它做分子,分母就是我们的头布局高度
//计算并通过方法返回比例
DecimalFormat format = new DecimalFormat("0.00");
float differ = mHeaderView.getViewHeight()+ params.topMargin;
float total = mHeaderView.getViewHeight();
float rate = differ/total;
mHeaderView.getPercentage(Float.parseFloat(format.format(rate)));
} else {
//走到这儿说明我们的头布局已经被继续下拉,超过了本身的大小
//所以返回1恒定值表示100%,不在继续增加
mHeaderView.getPercentage(1.00f);
}
// 这里对上拉做一下限制,因为当前上拉后然后不释放手指直接下拉,会把下拉刷新给触发了,感谢网友yufengzungzhe的指出
// 表示如果是在上拉后一段距离,然后直接下拉
if (deltaY > 0 && mPullState == PULL_UP_STATE && Math.abs(params.topMargin) <= mHeaderView.getViewHeight()) {
//如果每次偏移量>0,说明是下拉刷新,并且params.topMargin绝对值小于等于头布局高度
//说明头布局还未完全显示,直接返回
return params.topMargin;
}
// 同样地,对下拉做一下限制,避免出现跟上拉操作时一样的bug
if (deltaY < 0 && mPullState == PULL_DOWN_STATE && Math.abs(params.topMargin) >= mHeaderView.getViewHeight()) {
return params.topMargin;
}
params.topMargin = (int) newTopMargin;
mHeaderView.getView().setLayoutParams(params);
invalidate();
return params.topMargin;
}
/**
* header refreshing
*/
public void headerRefreshing() {
mHeaderState = REFRESHING;
//设置头布局完全显示
setHeaderTopMargin(0);
//通过自定义头布局回调方法,实行我们自定义的逻辑
mHeaderView.isRefreshingOrLoading();
if (refreshListener != null) {
//接口回调,用于处理网络
refreshListener.onPullToRefresh(this);
}
}
/**
* footer refreshing
*/
private void footerRefreshing() {
mFooterState = REFRESHING;
//将我们的头布局margin设为头布局+尾布局高度和,这样尾布局将完全显示
int top = mHeaderView.getViewHeight() + mFooterView.getViewHeight();
setHeaderTopMargin(-top);
//通过自定义尾布局回调方法,实行我们自定义的逻辑
mFooterView.isRefreshingOrLoading();
if (refreshListener != null) {
//接口回调,用于网络处理
refreshListener.onPullToLoadMore(this);
}
}
/**
* 设置header view 的topMargin的值
*
* @param topMargin ,为0时,说明header view 刚好完全显示出来; 为-mHeaderViewHeight时,说明完全隐藏了
* @description
*/
private void setHeaderTopMargin(int topMargin) {
LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
params.topMargin = topMargin;
mHeaderView.getView().setLayoutParams(params);
invalidate();
}
/**
* header view 完成更新后恢复初始状态
*/
public void onHeaderRefreshComplete() {
setHeaderTopMargin(-mHeaderView.getViewHeight());
//通过自定义头布局更新我们刷新完成后的头布局各控件的状态和显示
mHeaderView.refreshOrLoadComplete();
mHeaderState = PULL_TO_REFRESH;
}
/**
* footer view 完成更新后恢复初始状态
*/
public void onFooterRefreshComplete() {
setHeaderTopMargin(-mHeaderView.getViewHeight());
//通过自定义尾布局更新我们刷新完成后的尾布局各控件的状态和显示
mFooterView.refreshOrLoadComplete();
mFooterState = PULL_TO_REFRESH;
if (mRecyclerView != null) {
//加载完后列表停留在最后一项
mRecyclerView.scrollToPosition(mRecyclerView.getAdapter().getItemCount() - 1);
}
}
/**
* 获取当前header view 的topMargin
*
* @description
*/
private int getHeaderTopMargin() {
LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
return params.topMargin;
}
/**
* set headerRefreshListener
* 设置我们的接口
* @description
*/
public void setOnRefreshListener(OnRefreshListener refreshListener) {
this.refreshListener = refreshListener;
}
/**
* Interface definition for a callback to be invoked when list/grid footer
* view should be refreshed.
*/
public interface OnRefreshListener {
//下拉刷新的回调方法
void onPullToRefresh(PullBaseView view);
//上拉加载的回调方法
void onPullToLoadMore(PullBaseView view);
}
/**
* 设置是否可以在刷新时滑动
*
* @param canScrollAtRereshing
*/
public void setCanScrollAtRereshing(boolean canScrollAtRereshing) {
isCanScrollAtRereshing = canScrollAtRereshing;
}
/**
* 设置是否可上拉
*
* @param canPullUp
*/
public void setCanPullUp(boolean canPullUp) {
isCanPullUp = canPullUp;
}
/**
* 设置是否可下拉
*
* @param canPullDown
*/
public void setCanPullDown(boolean canPullDown) {
isCanPullDown = canPullDown;
}
protected abstract T createRecyclerView(Context context, AttributeSet attrs);
/**
* 用于设置刷新列表头部显示样式
*
* @param headerView
* @return
*/
public PullBaseView setHeaderView(BaseHeaderOrFooterView headerView) {
this.mHeaderView = headerView;
//清除所有的view,重新添加布局
removeAllViews();
//添加我们自定义的头布局
addView(mHeaderView.getView(), mHeaderView.getParams());
addView(mRecyclerView);
return this;
}
/**
* 用于设置刷新列表底部显示样式
*
* @param footerView
* @return
*/
public PullBaseView setFooterView(BaseHeaderOrFooterView footerView) {
this.mFooterView = footerView;
//添加我们自定义的尾布局
addView(mFooterView.getView(), mFooterView.getParams());
return this;
}
}
好啦,到这里我们的真正的处理逻辑的代码已经完全讲完了,注释标注的应该非常的详尽了,那么我们接下来来看看如何自定义头尾布局呢,这里我封装来一个基类BaseHeaderOrFooterView,先来看这个类的代码并加以注释说明:
/**
* 所有HeaderView或者是FooterView的基类
* 因为需要强制我们的子类实现某些方法,所以这里用的是抽象类
*/
public abstract class BaseHeaderOrFooterView {
private Context mContext;
private View mView;
protected int mViewHeight;
protected LinearLayout.LayoutParams params;
public static final int HEADER = 0;
public static final int FOOTER = 1;
private int type;
public BaseHeaderOrFooterView(Context context, @NonNull ViewGroup root, int type) {
this(context, root, type,false);
}
public BaseHeaderOrFooterView(Context context, @NonNull ViewGroup root,int type, boolean attachToRoot) {
mContext = context;
this.type = type;
createView(context, root, attachToRoot);
}
private void createView(Context context, ViewGroup root, boolean attachToRoot) {
mView = LayoutInflater.from(context).inflate(onBindLayoutId(), root, attachToRoot);
measureView(mView);
mViewHeight = mView.getMeasuredHeight();
params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, mViewHeight);
// 设置topMargin的值为负的header View高度,即将其隐藏在最上方
if (type == HEADER)
params.topMargin = -(mViewHeight);
onViewCreated(mView);
}
public final String getFormatDateString(String format) {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(new Date());
}
protected abstract void onViewCreated(View view);
protected abstract int onBindLayoutId();
protected LinearLayout.LayoutParams getParams() {
return params;
}
protected int getViewHeight() {
return mViewHeight;
}
/**
* 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
* 释放刷新/加载更多
*/
public abstract void releaseToRefreshOrLoad();
/**
* 下拉刷新/上拉加载
*/
public abstract void pullToRefreshOrLoad();
/**
* 执行正在刷新/加载的操作
*
* @return
*/
public abstract void isRefreshingOrLoading();
/**
* 刷新/加载完成的操作
*
* @return
*/
public abstract void refreshOrLoadComplete();
/**
* 这里提供一个方法能够获取下拉头显示的百分比,供动画效果使用
*/
public abstract void getPercentage(float rate);
public View getView() {
return mView;
}
protected Context getContext() {
return mContext;
}
public void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
} else {
childHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
}
好的,我们的头尾布局的基类已经解释完了,那么我们可以自定义两个头尾布局继承自BaseHeaderOrFooterView,并实现某些方法来验证我们的控件的效果,先来个TicketHeaderView:
public class TicketHeaderView extends BaseHeaderOrFooterView {
private ProgressBar progressBar;
private ImageView image;
private TextView text;
public TicketHeaderView(Context context, @NonNull ViewGroup root) {
super(context, root, HEADER);
}
@Override
protected void onViewCreated(View view) {
progressBar = (ProgressBar) view.findViewById(R.id.progress);
image = (ImageView) view.findViewById(R.id.image);
text = (TextView) view.findViewById(R.id.text);
}
@Override
protected int onBindLayoutId() {
return R.layout.ticket_head;
}
/**
* 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
* 释放刷新的提示
*/
@Override
public void releaseToRefreshOrLoad() {
text.setText("释放刷新数据");
progressBar.setVisibility(View.GONE);
}
@Override
public void pullToRefreshOrLoad() {
text.setText("下拉刷新数据");
progressBar.setVisibility(View.GONE);
}
/**
* 正在刷新
*/
@Override
public void isRefreshingOrLoading() {
text.setText("正在刷新...");
image.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
/**
* 刷新完成的回调
*/
@Override
public void refreshOrLoadComplete() {
text.setText("下拉刷新数据");
progressBar.setVisibility(View.GONE);
image.setVisibility(View.VISIBLE);
}
@Override
public void getPercentage(float rate) {
//这里设置的是根据下拉头显示的百分比进行一个头部图片动态缩放的效果
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) image.getLayoutParams();
params.width = (int) (80 * rate);
params.height = (int) (80 * rate);
image.setLayoutParams(params);
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center"
android:paddingBottom="15dip"
android:paddingTop="15dip">
<RelativeLayout
android:id="@+id/relativelaytout"
android:layout_width="30dp"
android:layout_height="30dp">
<ProgressBar
android:id="@+id/progress"
android:layout_width="25dp"
android:layout_height="25dp"
android:visibility="gone" />
<ImageView
android:id="@+id/image"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerInParent="true"
android:src="@drawable/ha" />
RelativeLayout>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
android:layout_toRightOf="@+id/relativelaytout"
android:text="下拉刷新数据"
android:textSize="15dp" />
RelativeLayout>
public class TicketFooterView extends BaseHeaderOrFooterView {
private TextView mFooterTextView;
private ProgressBar mFooterProgressBar;
public TicketFooterView(Context context, @NonNull ViewGroup root) {
super(context, root, FOOTER);
}
@Override
protected void onViewCreated(View view) {
mFooterTextView = (TextView) view.findViewById(R.id.pull_to_load_text);
mFooterProgressBar = (ProgressBar) view.findViewById(R.id.pull_to_load_progress);
}
@Override
protected int onBindLayoutId() {
return R.layout.refresh_footer;
}
@Override
public void releaseToRefreshOrLoad() {
mFooterTextView.setText("松开加载更多");
}
@Override
public void pullToRefreshOrLoad() {
mFooterTextView.setText("上拉加载更多");
}
@Override
public void isRefreshingOrLoading() {
mFooterTextView.setVisibility(View.GONE);
mFooterProgressBar.setVisibility(View.VISIBLE);
}
@Override
public void refreshOrLoadComplete() {
mFooterTextView.setText("上拉加载更多");
mFooterTextView.setVisibility(View.VISIBLE);
mFooterProgressBar.setVisibility(View.GONE);
}
@Override
public void getPercentage(float rate) {
//用不上的话可以不实现具体逻辑
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pull_to_refresh_header"
android:layout_width="fill_parent"
android:layout_height="60dp">
<ProgressBar
android:id="@+id/pull_to_load_progress"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerInParent="true"
android:visibility="gone" />
<TextView
android:id="@+id/pull_to_load_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="上拉加载更多"
android:textColor="#BBBCBD"
android:textSize="16sp" />
RelativeLayout>
接下来我们来看看数据的显示,这里提供了一个BaseAdapter类,当中封装了一些常用的方法,如果有需要大家可以根据自己的场景进行扩展。
package com.zy.recyclerview.view;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
/**
* BaseAdapter
*/
public class BaseAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
public Context context;//上下文
public List
下面我们来看看我们自己的实现的TicketAdapter
package com.zy.recyclerview.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.zy.recyclerview.R;
import com.zy.recyclerview.bean.SampleBean;
import com.zy.recyclerview.view.BaseAdapter;
import java.util.List;
public class TicketAdapter extends BaseAdapter.MyViewHolder> {
public TicketAdapter(Context context, List listDatas, OnViewClickListener onViewClickListener) {
super(context, listDatas, onViewClickListener);
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyViewHolder(mInflater.inflate(R.layout.list_item, parent, false));
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
SampleBean item = (SampleBean) listDatas.get(position);
holder.mTitleTextView.setText(
TextUtils.isEmpty(item.getTitle()) ? "暂无价格"
: item.getTitle());
holder.mInfoTextView.setVisibility(View.GONE);
if (TextUtils.isEmpty(item.getPrice_str())) {
holder.mPriceTextView.setText("暂无价格");
holder.mPriceUnit.setVisibility(View.GONE);
} else {
holder.mPriceTextView.setText(item.getPrice_str());
holder.mPriceUnit.setVisibility(View.VISIBLE);
}
holder.mSoldTimeTextView.setText(item.getSign_date());
if (position == listDatas.size() - 1) {
holder.mDivider.setVisibility(View.GONE);
} else {
holder.mDivider.setVisibility(View.VISIBLE);
}
}
@Override
public int getItemCount() {
return listDatas.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
public ImageView mImageView;
public TextView mTitleTextView;
public TextView mInfoTextView;
public TextView mPriceTextView;
public TextView mSoldTimeTextView;
public TextView mPriceUnit;
public View mDivider;
public MyViewHolder(View view) {
super(view);
mImageView = (ImageView) view.findViewById(R.id.iv_img);
mTitleTextView = (TextView) view.findViewById(R.id.tv_title);
mInfoTextView = (TextView) view.findViewById(R.id.tv_info);
mPriceTextView = (TextView) view.findViewById(R.id.tv_ticket_price);
mSoldTimeTextView = (TextView) view.findViewById(R.id.tv_sold_time);
mPriceUnit = (TextView) view.findViewById(R.id.tv_price_unit);
mDivider = view.findViewById(R.id.divider);
}
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_item_selector"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp">
<RelativeLayout
android:id="@+id/rl_image"
android:layout_width="105dp"
android:layout_height="80dp"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp">
<ImageView
android:id="@+id/iv_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/sample" />
RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="87dp"
android:layout_marginLeft="13dp"
android:layout_marginTop="17dp"
android:layout_toRightOf="@id/rl_image">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#101D37"
android:textSize="16dp" />
<TextView
android:id="@+id/tv_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tv_title"
android:layout_marginTop="5dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#101D37"
android:textSize="12dp" />
<TextView
android:id="@+id/tv_sold_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tv_info"
android:layout_marginTop="5dp"
android:ellipsize="end"
android:singleLine="true"
android:textColor="#101D37"
android:textSize="12dp" />
<LinearLayout
android:id="@+id/ll_ticket_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_ticket_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:textColor="#ff5848"
android:textSize="15dp" />
<TextView
android:id="@+id/tv_price_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:gravity="bottom"
android:text="元"
android:textColor="#ff5848"
android:textSize="13dp" />
LinearLayout>
RelativeLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#E4E6F0" />
RelativeLayout>
这就是一个最普通的adapter使用了,看看就懂。
最后来看看TicketActivity如何实现的吧
package com.zy.recyclerview.ui;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.LinearLayoutManager;
import android.view.Window;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.zy.recyclerview.R;
import com.zy.recyclerview.adapter.TicketAdapter;
import com.zy.recyclerview.bean.SampleBean;
import com.zy.recyclerview.view.BaseAdapter;
import com.zy.recyclerview.view.TicketHeaderView;
import com.zy.recyclerview.view.TicketFooterView;
import com.zy.recyclerview.view.PullBaseView;
import com.zy.recyclerview.view.PullRecyclerView;
import java.util.ArrayList;
import java.util.List;
public class TicketActivity extends Activity implements PullBaseView.OnRefreshListener, BaseAdapter.OnItemClickListener {
private PullRecyclerView recyclerView;
List mList = new ArrayList<>();
TicketAdapter ticketAdapter;
RelativeLayout container;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_ticket);
container = (RelativeLayout) findViewById(R.id.ll_container);
initData();
initRecycleView();
}
private void initRecycleView() {
recyclerView = (PullRecyclerView) findViewById(R.id.recyclerView);
recyclerView.setHeaderView(new TicketHeaderView(this, container))
.setFooterView(new TicketFooterView(this, container));
recyclerView.setOnRefreshListener(this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
ticketAdapter = new TicketAdapter(this, mList, null);
ticketAdapter.setOnItemClickListener(this);
recyclerView.setAdapter(ticketAdapter);
}
private void initData() {
for (int i = 0; i < 10; i++) {
SampleBean bean = new SampleBean();
bean.setPrice_str(200 + i + "");
bean.setPrice_unit("元");
bean.setSign_date("2018.05.31");
bean.setTitle("国家大剧院-风雪夜归人-" + (i + 1) + "场");
mList.add(bean);
}
}
@Override
public void onPullToRefresh(PullBaseView view) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
SampleBean bean = new SampleBean();
bean.setPrice_str("666");
bean.setPrice_unit("元");
bean.setSign_date("2018.05.31");
bean.setTitle("国家大剧院-风雪夜归人-VIP场");
mList.add(0, bean);
ticketAdapter.notifyDataSetChanged();
recyclerView.onHeaderRefreshComplete();
}
}, 1500);
}
@Override
public void onPullToLoadMore(PullBaseView view) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
SampleBean bean = new SampleBean();
bean.setPrice_str("888");
bean.setPrice_unit("元");
bean.setSign_date("2018.05.31");
bean.setTitle("国家大剧院-风雪夜归人-VVIP场");
mList.add(bean);
ticketAdapter.notifyDataSetChanged();
recyclerView.onFooterRefreshComplete();
}
}, 1500);
}
@Override
public void onItemClick(int position) {
Toast.makeText(this, "position : " + position, Toast.LENGTH_SHORT).show();
}
}
好了,到这里整个控件的原理+使用已经全部讲完了,来回顾一下这些内容:
1.PullToRefreshRecycleView的源码自己好好揣摩揣摩,在本子上画一下,然后根据代码的注释自己分析一下是不是这么个道理
2.如果想自定义或者扩展更多的东西,那么第一步能必须做到,其实原理看懂了理解了并不难,不是吗?
3.头尾布局的问题,如果你想让别人能够使用自定义布局,将应当像我这样把东西整理一下实现一个基类,并对外暴露足够的方法,头尾布局中只应该关心自己布局中的控件,以及刷新或者加载前后中的状态的控件的变化
4.接下来你能在这个基础上做些什么呢?其实我觉得能做的东西还是很多的,毕竟这只是一个初版,你也可以发挥自己的想象力在这个的基础上进行大胆的改造,比如除了刷新头布局以外,如果要求你在RecycleView列表上方加上一个banner控件,是不是很简单?毕竟我们的PullBaseView是一个LinearLayout,可以自由的addView进去,所以这就是你可以做的事情
好啦,说完了啦,有什么问题,欢迎留言,一起探讨~【微笑】