一般来说下拉刷新有这么几个状态,就拿QQ的下拉刷新来说吧
首先是往下拉的时候:
然后是下拉超过一定距离的时候:
然后是手指释放的时候刷新:
最后就是刷新成功或者失败的时候:
大概就是以上效果
所以自定义一个下拉刷新控件需要结合onTouchEvent来实现。
首先,自定义下拉刷新控件的布局文件refresh_header.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--下拉刷新控件-->
<LinearLayout
android:id="@+id/ll_pull_down_refresh"
android:padding="8dp"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<FrameLayout
android:layout_gravity="center"
android:layout_width="80dp"
android:layout_height="80dp">
<ImageView
android:id="@+id/iv_arrow"
android:src="@drawable/refresh_arrow"
android:layout_gravity="center"
android:scaleType="fitXY"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ProgressBar
android:visibility="visible"
android:id="@+id/pb_status"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/custom_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
<LinearLayout
android:layout_gravity="center_vertical"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:gravity="center_horizontal"
android:id="@+id/tv_status"
android:text="下拉刷新"
android:textColor="@android:color/holo_blue_light"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:gravity="center_horizontal"
android:layout_marginTop="5dp"
android:id="@+id/tv_time"
android:text="上次更新时间: 2017-10-15"
android:textColor="#55000000"
android:textSize="14sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
上面这个布局分为两块,一块显示箭头以及刷新之后的ProgressBar,另外一块显示文字用于表面当前处于什么样的刷新状态以及上次刷新的事件。其中,箭头是用的图片资源,ProgressBar则是自定义的小圆环,由于刷新的时候ProgressBar一直是旋转的,所以它的布局属性为 android:indeterminateDrawable ,表示状态不确定,然后是它的布局custom_progressbar.xml:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%">
<shape
android:shape="ring"
android:innerRadiusRatio="2.5"
android:thicknessRatio="15"
android:useLevel="false">
<gradient android:startColor="@android:color/holo_blue_bright"
android:centerColor="@android:color/holo_blue_dark"
android:endColor="#ffffff"
android:type="sweep"/>
</shape>
</rotate>
上面的属性中,android:innerRadiusRatio=”2.5”是Float类型。这个值表示内部环的比例,例如,如果android:innerRadiusRatio = ” 5 “,那么内部的半径等于环的宽度除以5。这个值会被android:innerRadius重写。 默认值是9。值越大,圆的宽越小。当时1的时候看不到。
android:thicknessRatio=”15”,也是Float类型。是厚度的比例。例如,如果android:thicknessRatio= ” 2 “,然后厚度等于环的宽度除以2。这个值是被android:innerRadius重写, 默认值是3。
值越大,圆环的环越小。
android:useLevel=”false” ,当值设置为true的时候是若隐若现,false表示持续显示。是Boolean类型。如果用在 LevelListDrawable里,那么就是true。如果通常不出现则为false。
最后的gradient则表示渐变的样式。
下面就是PrgressBar的预览效果:
然后是下拉刷新的预览效果:
接着来定义RefreshListView类,然后把这个控件以头的方式添加进去:
public class RefreshListView extends ListView {
//包含下拉刷新和顶部轮廓图
private LinearLayout headerView;
//下拉刷新控件
private View ll_pull_down_refresh;
private ImageView iv_arrow;
private ProgressBar pb_status;
private TextView tv_now_status;
private TextView tv_last_time;
public RefreshListView(Context context) {
this(context, null);
}
public RefreshListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHearedView(context);
}
private void initHearedView(Context context){
headerView = (LinearLayout) View.inflate(context, R.layout.refresh_header, null);
//下拉刷新控件
ll_pull_down_refresh = headerView.findViewById(R.id.ll_pull_down_refresh);
iv_arrow = (ImageView) headerView.findViewById(R.id.iv_arrow);
pb_status = (ProgressBar) headerView.findViewById(R.id.pb_status);
tv_now_status = (TextView) headerView.findViewById(R.id.tv_now_status);
tv_last_time = (TextView) headerView.findViewById(R.id.tv_last_time);
addHeaderView(headerView);
}
}
接着把之前新闻界面定义的ListView及其布局的ListView都替换成RefreshListView,然后看一下效果:
这个当然是不够的,下面要让圆圈默认隐藏,只在刷新的时候出来。所以要先把布局PrgressBar的android:visibility设为gone。同时,整个刷新控件也应该是默认隐藏,只在下拉的时候出现。
这就需要用到setPadding(0, -headerViewHeight , 0, 0)属性。这里的第二个参数表示设置与顶部的距离,这里传入的是整个刷新控件的高度,并且是负数,就表示隐藏它;如果为0,则表示显示;如果设置为为正数的高度,则表示两倍显示。
ll_pull_down_refresh.measure(0, 0); //开启测量方法
pullDownRefreshHeight = ll_pull_down_refresh.getMeasuredHeight();//获取控件高度
ll_pull_down_refresh.setPadding(0, -pullDownRefreshHeight, 0, 0);//设置控件默认隐藏
将控件隐藏后,就需要去重写onTouchEvent方法来实现具体的细节了。这时候,在RefreshLayout重写onTouchEvent:
private float startY = -1;
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (startY == -1){ //判断Y的坐标是否成功赋值了,没有赋值就再赋值一下
startY = ev.getY();
}
float endY = ev.getY();
float distanceY = endY - startY; //获取手指在Y轴上的的移动距离
if (distanceY > 0){//大于0表示往下滑
int paddingTop = (int) (-pullDownRefreshHeight + distanceY); //获取控件的实时高度
ll_pull_down_refresh.setPadding(0 , paddingTop, 0, 0); //设置控件的实时位置
}
break;
case MotionEvent.ACTION_UP:
startY = -1;
break;
default:
break;
}
return super.onTouchEvent(ev);
}
然后有如下效果
不过这仅仅是实现了第一个状态而已,并且这个状态槽点太多了,接下来会完善更多的细节
首先,定义三个刷新的状态,另外再加一个状态表示当前状态:
//下拉刷新
public static final int PULL_DOWN_REFRESH = 0;
//手松刷新,就是松手了就能刷新的状态
public static final int RELEASE_REFRESH = 1;
//正在刷新
public static final int REFRESHING = 2;
//当前状态
private int currentStatus = PULL_DOWN_REFRESH;
然后在MotionEvent.ACTION_MOVE的时候,通过判断手指滑动的距离来切换并设置控件状态,比如
if (paddingTop < 0 && currentStatus != PULL_DOWN_REFRESH) { //如果手指下滑了,并且滑动的高度还没超过刷新控件的高度,却还不处于下拉刷新状态
currentStatus = PULL_DOWN_REFRESH;//那就设置为下拉刷新状态
refreshViewState();//然后更新控件的状态
} else if (paddingTop > 0 && currentStatus != RELEASE_REFRESH) { //如果手指下滑的距离已经让刷新控件完全显示出来了,并且还不处于手松刷新状态
currentStatus = RELEASE_REFRESH;//那就设置为手松刷新状态
refreshViewState();//更新控件的状态
}
ll_pull_down_refresh.setPadding(0, paddingTop, 0, 0);//根据手指移动的距离去动态的设置下拉刷新控件所在位置
然后在MotionEvent.ACTION_UP的时候,通过判断控件的实时位置来设置具体的事件:
startY = -1;
if (currentStatus == PULL_DOWN_REFRESH) { //如果都松手了却还是处于PULL_DOWN_REFRESH状态,也就是手指滑动距离不超过刷新控件高度
ll_pull_down_refresh.setPadding(0, -pullDownRefreshHeight, 0, 0); //那么继续完全隐藏控件
} else if (currentStatus == RELEASE_REFRESH) { //如果松手了是处于RELEASE_REFRESH状态的
currentStatus = REFRESHING;//那么先把手松刷新状态切换为正在刷新状态
refreshViewState(); //更新控件的状态
ll_pull_down_refresh.setPadding(0, 0, 0, 0); //并且完全的显示控件
然后就是更新控件状态的方法refreshViewState的具体代码,它需要去判断当前所处的状态,然后进行具体的操作:
private void refreshViewState() {
switch (currentStatus){
case PULL_DOWN_REFRESH: //下拉刷新状态
iv_arrow.startAnimation(downAnimation);
tv_now_status.setText("下拉刷新...");
break;
case RELEASE_REFRESH: //手松刷新状态
iv_arrow.startAnimation(upAnimation);
tv_now_status.setText("手松刷新...");
break;
case REFRESHING: //正在刷新状态
tv_now_status.setText("正在刷新...→_→");
pb_status.setVisibility(VISIBLE);
iv_arrow.clearAnimation();
iv_arrow.setVisibility(GONE);
break;
default:
}
}
上面就是根据不同的状态来切换刷新控件的动画效果,以下是处于下拉刷新和松手刷新状态的动画效果:
private Animation upAnimation;
private Animation downAnimation;
private void initAnimation() {
upAnimation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);//处于松手刷新状态的时候,在下拉的基础上逆时针旋转180°
upAnimation.setDuration(500); //动画效果时间
upAnimation.setFillAfter(true); //保留状态
downAnimation = new RotateAnimation(-180, -360, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);//处于下拉刷新状态的时候,自身逆时针旋转180°
downAnimation.setDuration(500);
downAnimation.setFillAfter(true);
}
然后是松手之后,就把箭头隐藏,并且把ProgressBar显示出来,同时清除动画。
接下来看一下效果:
不过这时候如果是处于刷新状态的,下拉还可以继续刷新,所以需要在MotionEvent.ACTION_MOVE的时候加上
if(currentStatus == REFRESHING){
break;
}
然后就只能够刷新一次了。但是这时候还是一直处于刷新状态的,顶部的刷新控件需要在刷新成功或者太久不成功就自动隐藏掉。所以这时候需要在RefreshListView中定义一个接口,去监听控件的刷新
//监听控件的刷新
public interface OnRefreshListener{
//当下拉刷新的时候回调这个方法
public void onPullDownRefresh();
}
然后设置监听:
private OnRefreshListener mOnRefreshListener;
//设置监听刷新,由外界设置
public void setOnRefreshListener(OnRefreshListener l){
this.mOnRefreshListener = l;
}
然后在MotionEvent.ACTION_UP,也就是松手的时候去回调这个接口:
//回调接口
if(mOnRefreshListener != null){
mOnRefreshListener.onPullDownRefresh();
}
然后在使用到了RefreshListView的类NewsDetailPager.class中去设置监听:
listView.setOnRefreshListener(new MyOnRefreshListener());
private class MyOnRefreshListener implements RefreshListView.OnRefreshListener {
@Override
public void onPullDownRefresh() {
getDataByOkhttp(); //联网请求
}
}
只需要在监听中设置重新请求数据即可。然后在联网请求成功的方法中去隐藏刷新的圆圈,同时在失败的时候也要去隐藏刷新的圆圈。所以这时候需要在RefreshListView中写下来这个方法:
public void onRefreshFinish(boolean success){
tv_now_status.setText("下拉刷新...");
currentStatus = PULL_DOWN_REFRESH;
iv_arrow.clearAnimation();
pb_status.setVisibility(GONE);
iv_arrow.setVisibility(VISIBLE);
if(success){
tv_last_time.setText("上次更新时间:" + getSystemTime());
//隐藏下拉刷新控件
ll_pull_down_refresh.setPadding(0, -pullDownRefreshHeight, 0, 0);
} else {
ll_pull_down_refresh.setPadding(0, -pullDownRefreshHeight, 0, 0);
}
}
//获取系统时间
private String getSystemTime() {
java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(new Date());
}
然后联网请求成功的时候,调用:
listView.onRefreshFinish(true);
失败的时候调用:
listView.onRefreshFinish(false);
然后看一下最终的效果:
至此下拉刷新算是做完了,然后就是制作上拉刷新。首先,完成上拉刷新的布局refresh_footer.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:gravity="center">
<ProgressBar
android:visibility="visible"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/custom_progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginLeft="8dp"
android:gravity="center_horizontal"
android:text="加载更多..."
android:textColor="@android:color/holo_blue_light"
android:textSize="25sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
预览效果是这样
然后写一个initFooterView(Context context)方法去初始化控件,并且把这个方法添加到RefreshListView的构造函数中:
private View footView;//上拉刷新的视图
private int footerViewHeight;//上拉刷新控件的高度
private void initFooterView(Context context) {
footView = View.inflate(context, R.layout.refresh_footer, null);
footView.measure(0, 0);
footerViewHeight = footView.getMeasuredHeight();
footView.setPadding(0, -footerViewHeight, 0, 0);//默认隐藏这个控件
addFooterView(footView);//ListView添加footer
}
然后在并且在之前定义的接口中添加上拉刷新的回调方法,
//当加载更多的时候回调这个方法
public void onLoadMore();
监听滑到新闻数据最后一条的时候,去显示这个控件,然后回调接口,接下来就是在initFooterView方法中添加一个设置ListView监听的方法:
//监听ListView的滚动
setOnScrollListener(new MyOnScrollListener());
下面是具体的监听方法:
private boolean isLoadMore = false; //设置一个状态,是否已经加载更多,默认为false
private class MyOnScrollListener implements OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//当静止或者惯性滚动的时候
if(scrollState == OnScrollListener.SCROLL_STATE_IDLE || scrollState == OnScrollListener.SCROLL_STATE_FLING){
//并且是最后一条可见
if (getLastVisiblePosition() >= getCount() -1){
//footerView.setPadding(8, 8, 8, 8);//1.显示加载更多布局
isLoadMore = true;//2.状态改变
//3.回调接口
if(mOnRefreshListener != null){
mOnRefreshListener.onLoadMore();
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
然后在之前的接口中添加onLoadMore方法:
@Override
public void onLoadMore() {
Toast.makeText(context, "没有更多数据啦,总共只有这些", Toast.LENGTH_SHORT).show();
}
上拉刷新的用法和下拉刷新基本上一致,不过有一点需要注意的是,上拉刷新后的数据是要被添加的下拉刷新数据中去的,这样才不会覆盖掉它。不过由于我使用的API没有显示更多数据的接口,这里就不再赘述上拉刷新的其余细节了,只是简单实现一个上拉然后弹出一个Toast的方法
最后下拉刷新还有一个比较蛋疼的BUG,就是滑到最下面的数据后,再往上滑,会立即回到顶部。这是因为之前使用下拉刷新是根据相对位移来判断的,现在往上滑动就相当于是在做一个下拉刷新,所以又重新请求数据了,然后就会移动到顶部去。解决这个问题其实也简单,只需要判断顶部轮播图是否显示即可,如果轮播图显示在屏幕中,就表示允许刷新,如果不显示在屏幕中,则不允许刷新。
那么如何判断轮播图是否显示呢?
其实很简单,ListView在Y轴上的坐标一直都是不变的,不管你是下拉拉出了刷新控件,还是上滑滑倒了新闻列表的底部,它们都包含在ListView中,所以ListView相对顶部的距离一直是不变的。
但是轮播图的距离确是会变化的,只需要判断当ListView在Y轴上的坐标小于或者等于轮播图在Y轴上的坐标的时候,就可以判断轮播图是否完全显示或者隐藏。
这时候可以修改一下,把顶部轮播图以addView的方式添加到ListView中。在RefreshListView中添加方法:
//添加顶部轮播图
public void addTopNewsView(View topNewsView) {
if(topNewsView != null){
this.topNewsView = topNewsView;
headerView.addView(topNewsView);
}
}
然后在NewsDetailPager.class中将
listView.addHeaderView(topNewsView)
改写为:
listView.addTopNewsView(topNewsView);
然后再去RefreshListView中去判断顶部轮播图是否完全显示:
//ListView在Y轴上的坐标
private int listViewOnScreenY = -1;
//判断是否完全显示顶部轮播图
//当ListView在屏幕上Y轴的坐标小于或等于顶部轮播图在Y轴的坐标的时候,顶部轮播图完全显示
private boolean isDisplayTopNews() {
if (topNewsView != null){
//1.得到ListView在屏幕上的坐标
int[] location = new int[2];
if (listViewOnScreenY == -1){
getLocationOnScreen(location);//getLocationOnScreen方法返回X、Y轴坐标,第零个为X,第一个为Y
listViewOnScreenY = location[1];//得到Y轴坐标
}
//2.得到顶部轮播图在屏幕上的坐标
topNewsView.getLocationOnScreen(location);
int topNewsViewOnScreenY = location[1];
// if (listViewOnScreenY <= topNewsViewOnScreenY){
// return true;
// } else {
// return false;
// }
return listViewOnScreenY <= topNewsViewOnScreenY;
} else {
return true;
}
}
然后回到onTouchEvent的MotionEvent.ACTION_MOVE中,添加:
boolean isDisplayTopNews = isDisplayTopNews();//判断顶部轮播图是否完全显示,只有完全显示才会有下拉刷新
if (!isDisplayTopNews){
//加载更多,也就是顶部轮廓图没有完全显示的状态下
break;
}
最终效果: