您的位置:首页 > 移动开发 > Android开发

Android View - 上拉刷新下拉加载ListView

2017-07-28 17:11 495 查看
虽然网上有很多上拉刷新库,效果也很好,只是有时候突然来的Bug不是很好处理,而且有时候还达不到效果,所以今天用ListView实现上拉刷新,下拉加载的效果。直接上码:

本着易扩展的理念,先写一个父类,实现上拉刷新,下拉加载的效果:

package com.johan.library.viewtoolkit.refreshlistview;

import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;

/**
* Created by johan on 2017/7/27.
*/

public abstract class AbsRefreshListView extends ListView implements AbsListView.OnScrollListener {

/** 动画每毫秒移动距离 */
private static final int ANIMATION_STEP = 1;

/** 移动速率 */
private static final float MOVE_RATIO = 0.4f;

/** 刷新View最大高度 */
private static final int REFRESH_VIEW_MAX_HEIGHT = 500;

/** 不没有刷新 */
private static final int STATE_NO_REFRESH = 1;
/** 正在上拉或者下拉 */
private static final int STATE_RELEASE_REFRESH = 2;
/** 正在刷新 */
private static final int STATE_REFRESHING = 3;

/** 状态 */
private int state = STATE_NO_REFRESH;

/** 记录手指滑动的Y值 */
private float currentY;

/** headerView和footerView的原始值 */
private int headerHeight, footerHeight;

/** Header Footer View */
private View headerView, footerView;

/** 记录是否最顶和最底 */
private boolean isTop = true, isBottom;

/** 监听滑动的Listener,因为在AbsRefreshListView已经调用了setOnScrollListener,需要以另一种方式提供用户使用 */
private OnScrollListener onScrollListener;

/** 是否可以上拉和下拉 */
private boolean canPullTop = true, canPullBottom = true;

/** 判断是否在动画 */
private boolean isAnimation;

/** 记录动画时,上一次的值 */
private int lastAnimationValue;

public AbsRefreshListView(Context context) {
super(context);
init();
}

public AbsRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

/**
* 初始化
*/
private void init() {
headerView = initHeaderView();
footerView = initFooterView();
if (headerView != null) {
// 获取headerView的高度
headerView.measure(0, 0);
headerHeight = headerView.getMeasuredHeight();
post(new Runnable() {
@Override
public void run() {
// 设置headerView的高度
setViewHeight(headerView, 0);
}
});
addHeaderView(headerView);
} else {
canPullTop = false;
}
if (footerView != null) {
// 获取footerView的高度
footerView.measure(0, 0);
footerHeight = footerView.getMeasuredHeight();
post(new Runnable() {
@Override
public void run() {
// 设置footerView的高度
setViewHeight(footerView, 0);
}
});
addFooterView(footerView);
} else {
canPullBottom = false;
}
setOnScrollListener(this);
}

/**
* 设置View的高度
* Bug1 : 当height=0时,view会恢复原来的高度
* Bug1解决办法 : 如果height=0时,隐藏view,并设为1
* Bug2 : 当height=0时,分割线还会显示
* Bug2解决办法 : 如果height=0时,隐藏分割线,还好ListView有设置是否显示分割线的方法
* @param view
* @param height
*/
private void setViewHeight(View view, int height) {
int visibility = height == 0 ? View.GONE : View.VISIBLE;
view.setVisibility(visibility);
boolean isShowDivider = height != 0;
if (view == headerView) {
setHeaderDividersEnabled(isShowDivider);
} else {
setFooterDividersEnabled(isShowDivider);
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (params == null) {
params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
height = height == 0 ? 1 : height;
params.height = height;
view.setLayoutParams(params);
}

/**
* 初始化Header,子类实现
* @return
*/
protected abstract View initHeaderView();

/**
* 初始化Footer,子类实现
* @return
*/
protected abstract View initFooterView();

/**
* 重写onTouchEvent实现上拉和下拉功能
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isAnimation) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
currentY = event.getY();
break;
case MotionEvent.ACTION_MOVE :
// 单次移动的距离:手指移动的距离乘以移动速率,为了不让刷新的View显示太快
float moveY = (event.getY() - currentY) * MOVE_RATIO;
currentY = event.getY();
// 上拉
if (isTop && canPullTop) {
// 在最顶,如果上滑,不拦截
if (state == STATE_NO_REFRESH && moveY < 0) return super.onTouchEvent(event);
// 设置headerView高度
updateHeader((int) moveY);
// 如果不处理的话,下拉至刷新状态之后,不放手,然后上滑,发现设置不了headerView的高度
// 因为此时的ListView也会处理ACTION_MOVE事件下滑,所以我们要返回true,表示我们要处理事件
return true;
}
// 下拉
if (isBottom && canPullBottom) {
// 在最底,如果下滑,不拦截
if (state == STATE_NO_REFRESH && moveY > 0) return super.onTouchEvent(event);
// 设置footerView高度
updateFooter((int) -moveY);
// 因为我们处理事件,所以ListView的滑动就停止了
// 所以,虽然我们设置了footerView的高度,但是还是显示不出来
// 因此,需要我们手动设置滑动
scrollBy(0, (int) -moveY);
// 表示处理事件
return true;
}
break;
case MotionEvent.ACTION_UP :
currentY = 0;
if (state == STATE_RELEASE_REFRESH) {
if (headerView.getHeight() >= headerHeight) {
// 上拉刷新
releaseRefresh();
state = STATE_REFRESHING;
onRefreshing(true);
} else if (footerView.getHeight() >= footerHeight) {
// 下拉加载
releaseRefresh();
state = STATE_REFRESHING;
onRefreshing(false);
} else {
// 没有刷新的话,回到原始状态
completeRefresh();
}
}
break;
}
return super.onTouchEvent(event);
}

/**
* 更新headerView
* @param moveY
*/
private void updateHeader(int moveY) {
state = STATE_RELEASE_REFRESH;
int height = headerView.getHeight() + moveY;
height = Math.max(0, Math.min(REFRESH_VIEW_MAX_HEIGHT, height));
setViewHeight(headerView, height);
onProgress(true, height, headerHeight);
}

/**
* 更新footerView
* @param moveY
*/
private void updateFooter(int moveY) {
state = STATE_RELEASE_REFRESH;
int height = footerView.getHeight() + moveY;
height = Math.max(0, Math.min(REFRESH_VIEW_MAX_HEIGHT, height));
setViewHeight(footerView, height);
onProgress(false, footerView.getHeight(), footerHeight);
}

/**
* 松手刷新
*/
private void releaseRefresh() {
final View refreshView = headerView.getHeight() > 1 ? headerView : footerView.getHeight() > 1 ? footerView : null;
int refreshViewHeight = headerView.getHeight() > 1 ? headerHeight : footerView.getHeight() > 1 ? footerHeight : 0;
if (refreshView == null) {
return;
}
if (refreshViewHeight == 0) {
return;
}
if (refreshView.getHeight() < 2) {
return;
}
if (refreshView.getHeight() > refreshViewHeight) {
lastAnimationValue = refreshView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(refreshView.getHeight(), refreshViewHeight);
animator.setDuration((refreshView.getHeight() - refreshViewHeight) / ANIMATION_STEP + 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) animation.getAnimatedValue();
setViewHeight(refreshView, height);
// 因为上拉加载时,我们手动滚动了ListView,现在要恢复滚动的位置
if (isBottom) {
scrollBy(0, height - lastAnimationValue);
lastAnimationValue = height;
}
}
});
animator.start();
}
}

/**
* 完成刷新
* Bug1 : 手指正在滑动,打算取消刷新,而此时,外部调用了completeRefresh完成刷新,把refreshView重置了,
*        造成手指还在滑动,refreshView突然不见
* Bug1解决办法 : 根据 currentY != 0 判断手指是否在滑动,如果手指还在滑动,则不做任何操作,否则重置refreshView
* 注意1 : 不能用 headerView.getHeight() != 0 这种方式判断是否显示有headerView,具体原因请看setViewHeight方法
*         要用 headerView.getHeight() > 1 方式判断
*/
public void completeRefresh() {
if (currentY != 0) return;
isAnimation = true;
final View refreshView = headerView.getHeight() > 1 ? headerView : footerView.getHeight() > 1 ? footerView : null;
if (refreshView == null) {
isAnimation = false;
return;
}
if (refreshView.getHeight() < 2) {
isAnimation = false;
return;
}
lastAnimationValue = refreshView.getHeight();
ValueAnimator animator = ValueAnimator.ofInt(refreshView.getHeight(), 0);
animator.setDuration(refreshView.getHeight() / ANIMATION_STEP + 1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (int) animation.getAnimatedValue();
setViewHeight(refreshView, height);
// 因为上拉加载时,我们手动滚动了ListView,现在要恢复滚动的位置
if (isBottom) {
scrollBy(0, height - lastAnimationValue);
lastAnimationValue = height;
}
if (height == 0) {
state = STATE_NO_REFRESH;
isAnimation = false;
onComplete(refreshView == headerView);
}
}
});
animator.start();
}

/**
* 判断是否最顶或者最低或者都不是
* @param view
* @param firstVisibleItem
* @param visibleItemCount
* @param totalItemCount
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
isTop = true;
isBottom = false;
} else if ((firstVisibleItem + visibleItemCount) == totalItemCount) {
isBottom = true;
isTop = false;
} else {
isTop = false;
isBottom = false;
}
if (onScrollListener != null) {
onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (onScrollListener != null) {
onScrollListener.onScrollStateChanged(view, scrollState);
}
}

/**
* 设置是否可以上拉
* @param canPullTop
*/
public void setCanPullTop(boolean canPullTop) {
this.canPullTop = canPullTop;
}

/**
* 设置是否可以下拉
* @param canPullBottom
*/
public void setCanPullBottom(boolean canPullBottom) {
this.canPullBottom = canPullBottom;
}

/**
* 是否在刷新
* @return
*/
public boolean isRefreshing() {
return state == STATE_REFRESHING;
}

/**
* 因为AbsRefreshListView已经调用了setOnScrollListener方法,不能在外面调用setOnScrollListener,否则会影响AbsRefreshListView
* 请使用该方法,和setOnScrollListener的功能是一样的
* @param onScrollListener
*/
public void setScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}

/**
* 下拉上拉时调用
* @param isTop
* @param currentHeight
* @param viewHeight
*/
protected void onProgress(boolean isTop, int currentHeight, int viewHeight) {

}

/**
* 刷新时调用,子类实现
* @param isTop
*/
protected abstract void onRefreshing(boolean isTop);

/**
* 刷新完成时调用
* @param isTop
*/
protected void onComplete(boolean isTop) {

}

}


代码的注释已经很清楚说明了上拉刷新和下拉加载的原理,还有遇到的一些坑和解决办法,相信你看得懂。

下面是我实现的子类:

package com.johan.library.viewtoolkit.refreshlistview;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.TextView;

import com.johan.library.R;

/**
* Created by johan on 2017/7/27.
*/

public class RefreshListView extends AbsRefreshListView {

private ImageView headerIconView, footerIconView;
private TextView headerContentView, footerContentView;

private RefreshMessage refreshMessage;

private ObjectAnimator rotateAnimator;

private OnRefreshListener onRefreshListener = new OnRefreshListener() {
@Override
public void onPullRefreshing() {}
@Override
public void onLoadRefreshing() {}
};

public RefreshListView(Context context) {
super(context);
refreshMessage = new RefreshMessage();
initView();
}

public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
// 支持attr自定义属性
refreshMessage = new RefreshMessage();
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RefreshListView);
if (array.hasValue(R.styleable.RefreshListView_refresh_pull_icon)) {
refreshMessage.pullIcon = array.getDrawable(R.styleable.RefreshListView_refresh_pull_icon);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_pull_tip)) {
refreshMessage.pullTip = array.getString(R.styleable.RefreshListView_refresh_pull_tip);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_pull_release_refresh_tip)) {
refreshMessage.pullReleaseRefreshTip = array.getString(R.styleable.RefreshListView_refresh_pull_release_refresh_tip);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_pull_refreshing_tip)) {
refreshMessage.pullRefreshingTip = array.getString(R.styleable.RefreshListView_refresh_pull_refreshing_tip);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_load_icon)) {
refreshMessage.loadIcon = array.getDrawable(R.styleable.RefreshListView_refresh_load_icon);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_load_tip)) {
refreshMessage.loadTip = array.getString(R.styleable.RefreshListView_refresh_load_tip);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_load_release_refresh_tip)) {
refreshMessage.loadReleaseRefreshTip = array.getString(R.styleable.RefreshListView_refresh_load_release_refresh_tip);
}
if (array.hasValue(R.styleable.RefreshListView_refresh_load_refreshing_tip)) {
refreshMessage.loadRefreshingTip = array.getString(R.styleable.RefreshListView_refresh_load_refreshing_tip);
}
array.recycle();
initView();
}

@Override
protected View initHeaderView() {
View headerView = LayoutInflater.from(getContext()).inflate(R.layout.header_default, null);
headerIconView = (ImageView) headerView.findViewById(R.id.header_default_icon);
headerContentView = (TextView) headerView.findViewById(R.id.header_default_content);
return headerView;
}

@Override
protected View initFooterView() {
View footerView = LayoutInflater.from(getContext()).inflate(R.layout.footer_default, null);
footerIconView = (ImageView) footerView.findViewById(R.id.footer_default_icon);
footerContentView = (TextView) footerView.findViewById(R.id.footer_default_content);
return footerView;
}

private void initView() {
if (refreshMessage.pullIcon != null) {
headerIconView.setImageDrawable(refreshMessage.pullIcon);
}
headerContentView.setText(refreshMessage.pullTip);
if (refreshMessage.loadIcon != null) {
footerIconView.setImageDrawable(refreshMessage.loadIcon);
}
footerContentView.setText(refreshMessage.loadTip);
}

@Override
protected void onRefreshing(boolean isTop) {
View refreshView = isTop ? headerIconView : footerIconView;
startRotateAnimation(refreshView);
if (isTop) {
onRefreshListener.onPullRefreshing();
headerContentView.setText(refreshMessage.pullRefreshingTip);
} else {
onRefreshListener.onLoadRefreshing();
footerContentView.setText(refreshMessage.loadRefreshingTip);
}
}

@Override
protected void onProgress(boolean isTop, int currentHeight, int viewHeight) {
if (isTop) {
String headerContent = currentHeight >= viewHeight ? refreshMessage.pullReleaseRefreshTip : refreshMessage.pullTip;
headerContentView.setText(headerContent);
rotateIcon(headerIconView, 360 * (currentHeight % viewHeight) / viewHeight);
} else {
String footerContent = currentHeight >= viewHeight ? refreshMessage.loadReleaseRefreshTip : refreshMessage.loadTip;
footerContentView.setText(footerContent);
rotateIcon(footerIconView, 360 * (currentHeight % viewHeight) / viewHeight);
}
endRotateAnimation();
}

@Override
protected void onComplete(boolean isTop) {
endRotateAnimation();
}

private void startRotateAnimation(View view) {
rotateAnimator = ObjectAnimator.ofFloat(view, "rotation", 0, 359.0f);
rotateAnimator.setDuration(500);
rotateAnimator.setRepeatCount(-1);
rotateAnimator.setInterpolator(new LinearInterpolator());
rotateAnimator.start();
}

private void endRotateAnimation() {
if (rotateAnimator != null) {
rotateAnimator.end();
rotateAnimator = null;
}
}

public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
}

public interface OnRefreshListener {
void onPullRefreshing();
void onLoadRefreshing();
}

private void rotateIcon(ImageView imageView, int angle) {
imageView.setPivotX(imageView.getWidth() / 2);
imageView.setPivotY(imageView.getHeight() / 2);
imageView.setRotation(angle);
}

public void setPullTip(String pullTip) {
refreshMessage.pullTip = pullTip;
}

public void setPullReleaseRefreshTip(String pullReleaseRefreshTip) {
refreshMessage.pullReleaseRefreshTip = pullReleaseRefreshTip;
}

public void setPullRefreshingTip(String pullRefreshingTip) {
refreshMessage.pullRefreshingTip = pullRefreshingTip;
}

public void setPullIcon(int pullIcon) {
headerIconView.setImageResource(pullIcon);
}

public void setLoadTip(String loadTip) {
refreshMessage.loadTip = loadTip;
}

public void setLoadReleaseRefreshTip(String loadReleaseRefreshTip) {
refreshMessage.loadReleaseRefreshTip = loadReleaseRefreshTip;
}

public void setLoadRefreshingTip(String loadRefreshingTip) {
refreshMessage.loadRefreshingTip = loadRefreshingTip;
}

public void setLoadIcon(int loadIcon) {
footerIconView.setImageResource(loadIcon);
}

class RefreshMessage {
public String pullTip = "下拉刷新";
public String pullReleaseRefreshTip = "松手刷新";
public String pullRefreshingTip = "正在刷新";
public Drawable pullIcon = null;
public String loadTip = "上拉加载";
public String loadReleaseRefreshTip = "松手加载";
public String loadRefreshingTip = "正在加载";
public Drawable loadIcon = null;
}

}


只要模仿RefreshListView,继承AbsRefreshListView,就很轻松实现自己的上拉刷新下拉加载的ListView。

代码已经上传到我的github:https://github.com/JohanMan/viewtoolkit

因为RefreshListView是已经实现了上拉刷新下拉加载了,可以直接使用。

使用实例:

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"
xmlns:refresh_view="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.johan.viewtoolkit.MainActivity">

<com.johan.library.viewtoolkit.refreshlistview.RefreshListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:dividerHeight="1px"
android:divider="#cccccc"
refresh_view:refresh_pull_icon="@drawable/my_refresh_icon"
refresh_view:refresh_pull_tip="下拉刷新哦"
refresh_view:refresh_pull_release_refresh_tip="松手刷新哦"
refresh_view:refresh_pull_refreshing_tip="正在刷新哈"
/>

</LinearLayout>


activity

final RefreshListView listView = (RefreshListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
listView.setLoadIcon(R.drawable.my_refresh_icon);
listView.setLoadTip("上拉加载哦");
listView.setOnRefreshListener(new RefreshListView.OnRefreshListener() {
@Override
public void onPullRefreshing() {
// 模拟延时3秒
listView.postDelayed(new Runnable() {
@Override
public void run() {
listView.completeRefresh();
}
}, 3000);
}
@Override
public void onLoadRefreshing() {
// 模拟延时3秒
listView.postDelayed(new Runnable() {
@Override
public void run() {
listView.completeRefresh();
}
}, 3000);
}
});
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐