本文主要的是介绍如何实现弹性的listview,以及上拉和下拉功能的实现,其实对一般的View也是适用的,稍微修改一下就可以啦。里面涉及一些对事件分发的处理,有兴趣的可以看一下这个链接,
http://blog.youkuaiyun.com/newhope1106/article/details/53363208。
源码地址:
https://github.com/newhope1106/flexibleListView,有兴趣可以试着用一下。
效果图:
1.使用介绍
(1)首先在xml中定义
<cn.appleye.flexiblelistview.FlexibleListView
android:id="@+id/flexible_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
(2)在代码中实现回调就可以实现上拉和下拉功能
mFlexibleListView = (FlexibleListView) findViewById(R.id.flexible_list_view);
mFlexibleListView.setOnPullListener(new FlexibleListView.OnPullListener(){
@Override
public void onPullDown() {
//下拉刷新
}
@Override
public void onPullUp() {
//上拉加载更多
}
});
2.具体实现
抛开代码细节,要实现弹性效果和上拉以及下拉功能需要了解以下几点
(1)什么是弹性效果?列表滑到底部或者顶部之后,还可以继续滑动一定距离,然后再慢慢的恢复到底部或者顶部,恢复的过程有一个弹性的效果。
(2)什么时候触发?上面可以看到,滑到底部或者顶部之后开始触发
(3)滑动多少距离开始恢复?定义好一个距离,合适就好
(4)恢复的过程的弹性效果怎么实现?网上都有很多弹性公式
(5)什么时候调用上拉或下拉回调?当上拉或下拉到一定距离手指离开开始调用
下面看一下具体代码怎么实现的。
/**
* 弹性ListView,实现了上拉和下拉功能
* @author newhope1106 2016-11-02
*/
public class FlexibleListView extends ListView implements OnTouchListener{
/**初始可拉动Y轴方向距离*/
private static final int MAX_Y_OVER_SCROLL_DISTANCE = 100;
private Context mContext;
/**实际可上下拉动Y轴上的距离*/
private int mMaxYOverScrollDistance;
private float mStartY = -1;
/**开始计算的时候,第一个或者最后一个item是否可见的*/
private boolean mCalcOnItemVisible = false;
/**是否开始计算*/
private boolean mStartCalc = false;
/**用户自定义的OnTouchListener类*/
private OnTouchListener mTouchListener;
/**上拉和下拉监听事件*/
private OnPullListener mPullListener;
private int mScrollY = 0;
private int mLastMotionY = 0;
private int mDeltaY = 0;
/**是否在进行动画*/
private boolean mIsAnimationRunning = false;
/**手指是否离开屏幕*/
private boolean mIsActionUp = false;
public FlexibleListView(Context context){
super(context);
mContext = context;
super.setOnTouchListener(this);
initBounceListView();
}
public FlexibleListView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
super.setOnTouchListener(this);
initBounceListView();
}
public FlexibleListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
initBounceListView();
}
private void initBounceListView(){
final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
final float density = metrics.density;
mMaxYOverScrollDistance = (int) (density * MAX_Y_OVER_SCROLL_DISTANCE);
}
/**
* 覆盖父类的方法,设置OnTouchListener监听对象
* @param listener 用户自定义的OnTouchListener监听对象
* */
public void setOnTouchListener(OnTouchListener listener) {
mTouchListener = listener;
}
/**
* 设置上拉和下拉监听对象
* @param listener 上拉和下拉监听对象
* */
public void setOnPullListener(OnPullListener listener){
mPullListener = listener;
}
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
mScrollY = y;
}
/**
* 在滑动的过程中onTouch的ACTION_DOWN事件可能丢失,在这里进行初始值设置
* */
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
break;
default:
break;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
/*用户自定义的触摸监听对象消费了事件,则不执行下面的上拉和下拉功能*/
if(mTouchListener!=null && mTouchListener.onTouch(v, event)) {
return true;
}
/*在做动画的时候禁止滑动列表*/
if(mIsAnimationRunning) {
return true;//需要消费掉事件,否者会出现连续很快下拉或上拉无法回到初始位置的情况
}
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
}
case MotionEvent.ACTION_MOVE:{
if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
mStartCalc = true;
mCalcOnItemVisible = false;
mStartY = event.getY();
}
final int y = (int) event.getY();
mDeltaY = mLastMotionY - y;
mLastMotionY = y;
if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
if(mDeltaY * mScrollY > 0) {
mDeltaY = 0;
}
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:{
mIsActionUp = true;
float distance = event.getY() - mStartY;
checkIfNeedRefresh(distance);
startBoundAnimate();
}
}
return false;
}
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
if(mDeltaY == 0 || mIsActionUp) {
return;
}
scrollBy(0, mDeltaY/2);
}
/**弹性动画*/
private void startBoundAnimate() {
mIsAnimationRunning = true;
final int scrollY = mScrollY;
int time = Math.abs(500*scrollY/mMaxYOverScrollDistance);
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(time);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
float fraction = animator.getAnimatedFraction();
scrollTo(0, scrollY - (int) (scrollY * fraction));
if((int)fraction == 1) {
scrollTo(0, 0);
resetStatus();
animator.removeUpdateListener(this);
}
}
});
animator.start();
}
private void resetStatus() {
mIsAnimationRunning = false;
mStartCalc = false;
mCalcOnItemVisible = false;
}
/**
* 根据滑动的距离判断是否需要回调上拉或者下拉事件
* @param distance 滑动的距离
* */
private void checkIfNeedRefresh(float distance) {
if(distance > 0 && getFirstVisiblePosition() == 0) { //下拉
View view = getChildAt(0);
if(view == null) {
return;
}
float realDistance = distance;
if(!mCalcOnItemVisible) {
realDistance = realDistance - view.getHeight();//第一个item的高度不计算在内容
}
if(realDistance > mMaxYOverScrollDistance) {
if(mPullListener != null){
mPullListener.onPullDown();
}
}
} else if(distance < 0 && getLastVisiblePosition() == getAdapter().getCount()-1) {//上拉
View view = getChildAt(getChildCount()-1);
if(view == null) {
return;
}
float realDistance = -distance;
if(!mCalcOnItemVisible) {
realDistance = realDistance - view.getHeight();//最后一个item的高度不计算在内容
}
if(realDistance > mMaxYOverScrollDistance) {
if(mPullListener != null){
mPullListener.onPullUp();
}
}
}
}
public interface OnPullListener{
/**
* 下拉
* */
void onPullDown();
/**
* 上拉
* */
void onPullUp();
}
}
代码不长,只有200多行,比较简单,也不涉及资源问题。
首先我们初始化一个最大距离:
mMaxYOverScrollDistance,
同时控件自己实现OnTouchListener的接口,所有的功能基本都是在onTouch实现的,我们先简要的描述一下思路。
当手指按下屏幕的时候,检查此时第一个或者最后一个item是否可见,如果不可见,当滑动手指的时候,检查此时是否第一个或最后一个item是否可见,在滑动列表时,如果已经超过了listview顶部或底部的位置,通过改变其偏移量mScrollY,让其可以再在原来的基础上继续滑动,但是当滑动到一定距离之后,禁止其改变偏移量,此时不能再继续滑动了,当手指离开屏幕之后,再弹性回到顶部或底部位置,根据滑动的距离,来判断是否需要进行下拉或上拉操作。为什么,ACTION_DOWN和ACTION_UP中都有这个检测,主要是为了在最后计算距离的时候判断是否需要减去第一个item的高度,当然读者也可以把它去掉,item高度不大的情况下,不会影响体验。下面看代码。
case MotionEvent.ACTION_DOWN:{
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
}
在ACTION_DOWN操作的时候,通过resetStatus(),初始化状态,然后检查第一个item或者最后一个item是否显示,mStartCalc表示开始计算距离,mCalcOnItemVisible表示是否第一个item或者最后一个item可见的,如果是mStartCalc置为true,mCalcOnItemVisible置为true,同时开始记录当前位置坐标。
case MotionEvent.ACTION_MOVE:{
if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
mStartCalc = true;
mCalcOnItemVisible = false;
mStartY = event.getY();
}
final int y = (int) event.getY();
//获取滑动的偏移量
mDeltaY = mLastMotionY - y;
mLastMotionY = y;
if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
if(mDeltaY * mScrollY > 0) {
mDeltaY = 0;
}
}
break;
}
如果在ACTION_DOWN中没有开始计算,那么在ACTION_MOVE中判断是否第一个或最后一个item可见,如果是,则将mStartCalc置为true,mCalcOnItemVisible置为false。将本次的位置和上次的y周位置进行比较,获取偏移量。在滑动的过程中,都会调用onOverScrolled接口,然后调用scrollBy(实质上是调用scrollTo)接口,从而实现列表滑动。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
//滑动偏移量等于0或者手指离开屏幕都不在滑动列表
if(mDeltaY == 0 || mIsActionUp) {
return;
}
scrollBy(0, mDeltaY/2);
}
上述的ACTION_MOVE中会判断当前listview的偏移量(mScrollY)是否超过最大距离,否则将滑动的偏移量(mDeltaY)置为0,不让其在onOverScrolled中滑动。上述的mDeltaY/2,作用是不让其滑动太快,自然一些。
case MotionEvent.ACTION_UP:{
mIsActionUp = true;
float distance = event.getY() - mStartY;
checkIfNeedRefresh(distance);
startBoundAnimate();
}
当手指离开屏幕的时候,会调用ACTION_UP,此时将mIsActionUp置为true,同时计算当前位置的坐标和初始计算的位置坐标,然后得出滑动的距离(往返滑动的情况不计算,只计算初始和终止位置),
checkIfNeedRefresh用于判断是否需要上拉或者下拉操作,根据distance的正负可以知道是上滑还是下滑,如果有必要,减去第一个或最后一个item的高度,得到listview实际滑动的距离,然后和最大距离进行比较,来判断是否需要上拉加载更多,下拉刷新。
最后通过一个动画
startBoundAnimate实现弹性恢复的效果,动画过程中不允许其滑动。
/*在做动画的时候禁止滑动列表*/
if(mIsAnimationRunning) {
return true;//需要消费掉事件,否者会出现连续很快下拉或上拉无法回到初始位置的情况
}
一下有几个注意点,onTouch一般情况下返回false,表示不消费事件,不能影响ListView的正常滑动。上拉或者下拉的时候,这里并没有做Loading效果,读者可以自行添加一个footerView或者HeaderView来实现。
这里都是在View的接口里面实现的,因此实际上不限于ListView,其他的继承自View的控件,都可以采用这种方法,如果只想用弹性效果,那么也没有必要实现上拉和下拉的效果,直接在xml中定义即可。
还有一点需要注意的是,有时滑动太快,会把ACTION_DOWN事件给忽略掉,因此需要在onInterceptTouchEvent做ACTION_DOWN事件的处理,可以把OnTouch方法中的ACTION_DOWN去掉。