大家好,本人挺久没写博客了,一方面不知道到有什么好写的,想写的东西虽然东西也实现了,但是代码很简练,也无法和市面上那些灰常成熟的做比较,因此就没写,但是,下拉刷新和上拉加载这东西,相信一大部分人和博主一样,都是用市面上的,一来有些确实扩展性比较差,每次修改头和脚的布局都要在自定义的listview或者自定义的view类查找位置,灰常浪费时间,因此,博主就打算写一个扩展性不错的,可以直接在activity加载头和尾布局,并在相应的回调方法实现头布局的动画效果。
老规矩,先上3张效果图,
效果图1:只有下拉刷新:
从图可以看出, 箭头的旋转是和下拉的高度有关,也就是在回调方法实现旋转效果,同时,“正在刷新”状态的时候,可以通过向上滑动,打断刷新动作。
效果图2:只有上拉加载
这里没什么好说的,唯一不同的是,加载的时候,滑动不会打算加载过程,至于为什么这样设置,一来如果要打算,代码量比较大,二来很多童鞋都试过加载的同时也会滑动吧,在这里,肯定有童鞋说,那为什么图一可以打断,其实那个打断和刷新结束用的是同一个方法,只要在方法内部不要实现打断效果就行了,实在不懂得话,等会讲解会说到。
效果图三,不用猜的知道,就是上面两者结合,即下拉刷新+上拉加载
多的不说了,也就是2个功能合并
看到这里,有童鞋可能会说,切,不就是下拉刷新和上拉加载么,如果这么想,请你重新看回文章标题哈。
看自定义的listview之前,先看看adapter
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
convertView = li.inflate(R.layout.item_list,null);
convertView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, "position:" + position, Toast.LENGTH_SHORT).show();
}
});
TextView tv = (TextView) convertView.findViewById(R.id.tv);
tv.setText(mList.get(position));
return convertView;
}
如果想使用博主的listview类的童鞋,请不要调用listview的setOnItemClick();方法,因为某些情况,下拉刷新的时候也会有点中效果,因此,博主直接给convertView设置点击,抢占了listview的事件,使listview的setOnItemClick();方法失效。在这里,有些童鞋也会说到,人家点击的有时候有个“小灰灰”出现啊,这里没咋办,OK,直接给根布局设置个selector,先看代码,
这是adapter对应的item_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/root_selector"
android:gravity="center">
<TextView
android:id="@+id/tv"
android:padding="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="22sp"
tools:text="萨大大缩短"/>
</LinearLayout>
android:background="@drawable/root_selector"
在看看selector的xml文件:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@color/gray"/>
<item android:state_pressed="false" android:drawable="@color/white"/>
</selector>
这里,“小灰灰”就不会丢了,点击的时候仍然有效!
好吧,gray对应的颜色是#eee,white就不说了!!!
现在开始上自定义listview的代码:
public class PullRefreshListView extends ListView implements AbsListView.OnScrollListener {
private Context mContext;
/**
* listview的头部
*/
private View headerView;
/**
* listview的脚部
*/
private View footerView;
/**
* 头部高度
*/
private int headerHeight;
/**
* 下拉刷新状态
*/
public static final int PULL_REFRESH = 0x001;
/**
* 松开刷新状态
*/
public static final int LOOSEN_REFRESH = 0x002;
/**
* 正在刷新状态
*/
public static final int REFRESHING = 0x003;
/**
* 正在加载
*/
public static final int LOADING = 0x004;
/**
* 初始化状态
*/
private int mState = PULL_REFRESH;
private ValueAnimator valueAnimator;
private int footerHeight;
在这里有一些状态,以及头和脚布局的view
对应的状态也有4种之多,分别是默认的下拉刷新,然后是松开刷新,再然后是正在刷新,最后那个正在加载其实和前面三个半毛钱关系都没有,严谨的同学,最好自己在业务上处理一下,避免下拉刷新的时候上拉加载,所以,最好还是在中断刷新的方法上把打断刷新的逻辑也写上,这样,正在刷新的状态,手指向上划,就会把“正在刷新”状态变成“下拉刷新”状态(下拉刷新是最初始的状态。)
中断刷新的方法是:
@Override
public void stopRefresh() {
h.removeCallbacks(r);
Toast.makeText(this, "停止刷新可以通过手动滑动(更新状态为非“正在刷新”)停止", Toast.LENGTH_SHORT).show();
}
这个方法等会也会说上的。
接下来,看三个构造器:
public PullRefreshListView(Context context) {
super(context);
mContext = context;
setOnScrollListener(this);
}
public PullRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
setOnScrollListener(this);
}
public PullRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
setOnScrollListener(this);
}
这三个构造器其实都是一个样子,博主有点懒,就不短调长了。
/**
* 设置头部
*
* @param view
*/
private void setHeaderView(View view) {
headerView = view;
headerView.measure(0, 0);
headerView.setPadding(0, -headerView.getMeasuredHeight(), 0, 0);
headerHeight = headerView.getPaddingTop();
addHeaderView(headerView);
}
这是设置头部的布局,看到哦,这是私有的!好,现在无视“私有”这两个字,这个方法会在activity调用,这样就可以省去繁琐的查找源码,修改布局的时间。
有头就有脚,看看脚
/**
* 设置脚部
*
* @param view
*/
private void setFooterView(View view) {
footerView = view;
footerView.measure(0, 0);
footerView.setPadding(0, 0, 0, -footerView.getMeasuredHeight());
footerHeight = footerView.getPaddingBottom();
addFooterView(footerView);
}
这2个其实有点相似,头是将paddingtop设置为(负数)头的高度,脚就是将paddingBottom设置成(负数)脚的高度。这样就达到了一开始隐藏头和脚的目的。
来到本章重点了,处理滑动监听,
/**
* 不使用actiondown,避免adapter的点击事件与其冲突
*
* @param ev
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (headerView == null || isLoading)
return super.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if (isUp) {
isUp = false;
// TODO 这里进行down操作
lastY = (int) ev.getY();
}
float dY = (ev.getY() - lastY) * mDamp;
lastY = (int) ev.getY();
break;
case MotionEvent.ACTION_UP:
isUp = true;
break;
}
return super.onTouchEvent(ev);
}
事件处理就是这种模式,有童鞋会留意到,actiondown好像不见了,因为博主担心有些童鞋和博主一样,不怎么喜欢使用listview的onitemClick方法,如果在adapter上将convertView设置点击监听,listview的actionDown会被抢占,也就是说,没反应!
所以,在调用move的时候判断up了没,up了就表示是第一次move,博主也就将第一次Move当成down来使用。
现在先按顺序看看move的其他部分,
boolean isPulling = ev.getY() - lastY > 0;
float dY = (ev.getY() - lastY) * mDamp;
lastY = (int) ev.getY();
int paddingTop = (int) (headerView.getPaddingTop() + dY);
isPulling为true表示手指向下滑动,
dY表示两次move之间的间隔,其中mDamp为阻尼系数,有这么一个方法
private float mDamp = 1.0f;
/**
* 设置阻尼系数,默认为1.0
*/
private void setDamp(float damp) {
mDamp = damp;
}
默认阻尼系数为1.0,说明手指滑动和listview滚动的距离是一样的,如果大于1.0,listview会比手指滚得快,反之,listview比手指滚的慢。
int paddingTop = (int) (headerView.getPaddingTop() + dY);
这个是用来判断头部将要达到的高度,目的高度=原来高度+dY,因为向下滑动的时候dY>0,所以向下滑的时候,头部高度会更高。
if (isPulling && mState == REFRESHING)
return true;
这个方法,是用于当显示“正在刷新”状态的时候,禁止向上滑动,因为向上滑动会变成“松开刷新”状态,此时会允许他向下滑动,即变为“下拉刷新”状态。
接下来是move的最后一个地方,也是逻辑量比较大的
if (paddingTop > headerHeight && getFirstVisiblePosition() == 0) {
scrollHeaderBy((int) dY);
// 露出头的百分比,超过1转松开刷新
float percent = (headerView.getPaddingTop() + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);
if (mCallback != null) {
mCallback.drag(percent, (int) dY);
mCallback.dragToLoosen(percent <= 1 ? percent : 1, percent <= 1 ? (int) dY : 0);
}
if (percent <= 1) {
if (mState != PULL_REFRESH)
setState(PULL_REFRESH);
} else {
if (mState != LOOSEN_REFRESH)
setState(LOOSEN_REFRESH);
}
return true;
}
首先看判断,因为之前做了预判paddingTop,也就是知道了目的高度是怎样,再和头部本身高度作对比,后面那个,可以当作当listview拼命向下滑到最高度的时候,它的值就是0(这里的0不是指第一个item,而是指头布局,因为头布局已经被addHeader,也就会在所有item的上面。)
/**
* 通过偏移量移动头部
*
* @param dY
*/
private void scrollHeaderBy(int dY) {
int paddingTop = headerView.getPaddingTop();
int end = paddingTop + dY;
headerView.setPadding(0, end <= headerHeight ? headerHeight : end, 0, 0);
}
这个方法是通过一点点的小小偏移量dY来偏移头布局。当头部刚好完全不可见的时候,paddingTop刚好为(负数)头的高度,再小就没意义了,因此作纠正。
// 露出头的百分比,超过1转松开刷新
float percent = (headerView.getPaddingTop() + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);
假设头部完全不可见的时候,getPaddingTop = -高度,而(-高度)的绝对值等于正高度,两者抵消为0,此时percent为0,刚手指慢慢向下滑动,paddingTop慢慢增加,而分子的值慢慢向上增大,percent从0 到0.01 到0.1到1,然后再超过1。
if (mCallback != null) {
mCallback.drag(percent, (int) dY);
mCallback.dragToLoosen(percent <= 1 ? percent : 1, percent <= 1 ? (int) dY : 0);
}
callback是一个回调,当没有设置监听的时候,也就不需要回调方法,drag和dragToLoosen的区别就是,drag意思就是拖动,也就是,头部从完全不可见到恰好完全可见,到继续往下拖动的时候(状态为“松开刷新”),这个方法在松开手指之前,会一直调用,percent将可以达到大于1的值,对于一些特别奇葩的动画,相信这个方法可以用上,dragToLoosen的意思就是,只会在“下拉刷新”状态的时候调用,也就是percent的值为0-1,
总结起来就是这样,当状态为"松开刷新"时,松手(Up)的话,也会有一个回调执行drag,让它的percent从大于1到1.0,当状态为“下拉刷新”时,松手的话,也会有一个回调执行dragToLoosen,让它的percent从小于1大于0 到0.0。
if (percent <= 1) {
if (mState != PULL_REFRESH)
setState(PULL_REFRESH);
} else {
if (mState != LOOSEN_REFRESH)
setState(LOOSEN_REFRESH);
}
当percent<=1 说明此刻状态是"下拉刷新",否则就是"松开刷新"。
private void setState(int state) {
switch (state) {
// 下拉刷新状态
case PULL_REFRESH:
if (mState == REFRESHING)
if (mCallback != null && headerView != null)
mCallback.stopRefresh();
if (mCallback != null && headerView != null)
mCallback.toRullRefresh();
if (mState != PULL_REFRESH)
mState = PULL_REFRESH;
break;
// 松开刷新状态
case LOOSEN_REFRESH:
if (mCallback != null && headerView != null)
mCallback.toLoosenRefresh();
if (mState != LOOSEN_REFRESH)
mState = LOOSEN_REFRESH;
break;
// 正在刷新状态
case REFRESHING:
if (mState == LOOSEN_REFRESH && headerView != null) {
mState = REFRESHING;
if (mCallback != null)
mCallback.toRefreshing();
}
break;
case LOADING:
if (mState == PULL_REFRESH) {
mState = LOADING;
if (!isLoading) {
mCallback.toLoading();
isLoading = true;
}
}
break;
default:
break;
}
}
当想设置状态为“下拉刷新”时,上一个状态如果是“正在刷新”,会通过回调调用停止刷新这个方法,也就是之前说到的打断。此时,回调调用下拉刷新方法,将状态设置为“下拉刷新”。
当想设置状态为“松开刷新”,回调调用松开刷新方法,将状态设置为“松开刷新”。
当想设置状态为“正在刷新”,先判断上一个状态是不是松开刷新,如果是,回调调用正在刷新方法,并将状态设置为“正在刷新”。
当想设置状态为“正在加载”,先判断上一个状态是不是“下拉刷新”,如果是,回调调用正在加载方法,并将isLoading设为true。
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (footerView == null )
return;
if (getLastVisiblePosition() == getCount() - 1 && mState != LOADING) {
footerView.setPadding(0, 0, 0, 0);
if (!isLoading) {
setState(LOADING);
setSelection(getCount());
}
}
}
滑动时候,如果有footerView,并判断最后一个可见的position是不是总数-1(开始为0),并且状态不是正在加载,此时,将脚布局显示出来,并根据条件执行。
此时,看up:
// 最终位置
int endHeight = 0;
if (mState == PULL_REFRESH)
endHeight = headerHeight;
else if (mState == LOOSEN_REFRESH)
endHeight = 0;
根据状态,决定最终的动画停止位置,if条件时,停止在(负)头布局的高度,else if条件时,停止在0,即头部恰好完全可见。
valueAnimator = ValueAnimator.ofInt(headerView.getPaddingTop(), endHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int padding = (int) animation.getAnimatedValue();
lastPadding = padding;
float percent = (padding + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);
mCallback.drag(percent,padding - lastPadding);
if (padding < 0) {
if (mCallback != null)
mCallback.dragToLoosen(percent, padding - lastPadding);
}
headerView.setPadding(0, padding, 0, 0);
}
});
此时,是通过动画,实现回弹效果。
valueAnimator.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
if (lastPadding == 0) {
if (mState != REFRESHING)
setState(REFRESHING);
} else if (lastPadding == headerHeight) {
if (mState != PULL_REFRESH)
setState(PULL_REFRESH);
}
}
});
这是监听动画结束时候,来决定状态。
在move时候,有一个地方是用来取消动画的,动画取消的时候也会执行onAnimationEnd,
if (isUp) {
isUp = false;
// TODO 这里进行down操作
lastY = (int) ev.getY();
if (valueAnimator != null && valueAnimator.isRunning())
valueAnimator.cancel();
}
/**
* 避免繁琐的判断
*/
private StateCallBack mCallback;
public void setStateCallBace(StateCallBack callback) {
mCallback = callback;
}
public interface StateCallBack {
/**
* 从下拉刷新到松开刷新的瞬间
*/
void toLoosenRefresh();
/**
* 从松开刷新到下拉刷新的瞬间
*/
void toRullRefresh();
/**toLoading
* 状态为正在刷新
*/
void toRefreshing();
/**
* 状态为正在加载
*/
void toLoading();
/**
* 从下拉刷新拖拽到松开刷新的移动百分比
*
* @param percent 0-1
* @param dY 调用间隔的偏移量
*/
void dragToLoosen(float percent, int dY);
/**
* 头部有高度后移动的百分比
*
* @param percent 0-N
* @param dY 调用间隔的偏移量
*/
void drag(float percent, int dY);
/**
* 停止正在刷新(手动和被动都会执行)
*/
void stopRefresh();
/**
* 停止正在加载(被动执行)
*/
void stopLoad();
}
这是回调的接口
public class Builder {
private PullRefreshListView mLv;
public Builder(PullRefreshListView listView) {
mLv = listView;
}
/**
* 这是头布局
*/
public Builder setHeaderView(View view) {
mLv.setHeaderView(view);
return this;
}
/**
* 这是脚布局
*/
public Builder setFooterView(View view) {
mLv.setFooterView(view);
return this;
}
/**
* 设置动画时间
*/
public Builder setDuration(int duration) {
mLv.setDuration(duration);
return this;
}
/**
* 设置滑动时阻尼系数
*/
public Builder setDamp(int damp) {
mLv.setDamp(damp);
return this;
}
/**
* 关闭加载
*/
public Builder closeLoading() {
mLv.closeLoading();
return this;
}
/**
* 关闭刷新
*/
public Builder closeRefreshing() {
mLv.closeRefreshing();
return this;
}
}
}
这是使用了建造者模式,避免方法过多找不着,因此用建造者把不属于listview的方法封装起来,所以调用的时候,只能通过建造者来调用方法。
下面看看activity如何调用。
public class MainActivity extends AppCompatActivity implements PullRefreshListView.StateCallBack{
private PullRefreshListView lv;
private View header;
private View footer;
private ImageView iv;
private TextView tv;
private ProgressBar pb;
private int refresh;
private int counts;
private Adapter a;
/**
* listview的建造者,里面封装了额外的方法
*/
private PullRefreshListView.Builder builder;
开始的地方没啥好说的。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lv = (PullRefreshListView) findViewById(R.id.lv_pullrefresh);
builder = lv.new Builder(lv);
init();
iv = (ImageView) header.findViewById(R.id.iv_rotate);
tv = (TextView) header.findViewById(R.id.tv_text);
pb = (ProgressBar) header.findViewById(R.id.pb);
}
在这里先找到listview,然后初始化,初始化后再找到头布局里面的一些小控件,
List<String> list;
private void init() {
list = new ArrayList<>();
for (int i=0;i<20;i++){
list.add(String.valueOf(counts++)+" 第"+refresh+"次刷新。");
}
a = new Adapter(this,list);
lv.setAdapter(a);
builder.setHeaderView(header =View.inflate(this,R.layout.header_view,null));
builder.setFooterView(footer = View.inflate(this,R.layout.footer_view,null));
lv.setStateCallBace(this);
}
这里是初始化的地方,留意一下,这里是通过构造者添加头部和脚部的。
builder.setHeaderView(header =View.inflate(this,R.layout.header_view,null));
builder.setFooterView(footer = View.inflate(this,R.layout.footer_view,null));
@Override
public void toLoosenRefresh() {
tv.setText("松开刷新");
}
@Override
public void toRullRefresh() {
tv.setText("下拉刷新");
iv.setVisibility(View.VISIBLE);
pb.setVisibility(View.GONE);
}
private Handler h ;
private Runnable r;
@Override
public void toRefreshing() {
tv.setText("正在刷新");
iv.setVisibility(View.GONE);
pb.setVisibility(View.VISIBLE);
h =new Handler();
h.postDelayed(r =new Runnable() {
@Override
public void run() {
counts = 0;
list.clear();
refresh++;
for (int i=0;i<20;i++){
list.add(String.valueOf(counts++)+" 第"+refresh+"次刷新。");
}
a.notifyDataSetChanged();
builder.closeRefreshing();
}
},5000);
}
@Override
public void toLoading() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
for (int i=0;i<20;i++){
list.add(String.valueOf(counts++)+" 第"+refresh+"次刷新。");
}
a.notifyDataSetChanged();
builder.closeLoading();
}
},2000);
}
@Override
public void dragToLoosen(float percent, int dY) {
iv.setRotation(180*percent);
}
@Override
public void drag(float percent, int dY) {
}
@Override
public void stopRefresh() {
h.removeCallbacks(r);
Toast.makeText(this, "停止刷新可以通过手动滑动(更新状态为非“正在刷新”)停止", Toast.LENGTH_SHORT).show();
}
@Override
public void stopLoad() {
Toast.makeText(this, "停止加载不允许通过手动滑动停止", Toast.LENGTH_SHORT).show();
}
然后,可以在回调方法内,实现一些控件的变化,注意一下这个方法,
@Override
public void dragToLoosen(float percent, int dY) {
iv.setRotation(180*percent);
}
动画效果可以在这里实现,还是比较方便的。
就到这里了,实现了解耦,但写博客的过程发现了一些不足,就是只可以用于打断了,就是说“正在刷新”的时候,如果向下滑动,就应该处理打断逻辑,因为箭头都改变了,向上划的时候也不会滑到“正在刷新”这个字样,所以感觉还是有些不完善的,抽空会修改一下,到时候下拉刷新不允许加载,加载不允许下拉刷新,就当是两个下拉刷新-上拉加载的类吧,一个允许打断,一个不允许打断,哈哈。
所以,如果使用这个自定义的listview,最好还是处理一下打断逻辑吧!!!
下篇:
后篇:自定义(扩展性能强!)的下拉刷新和上拉加载控件