Android 高仿QQ的下拉刷新 ListView

原创 2013年12月04日 19:38:41

        最近工程需要使用下拉刷新,但是使用网上流传的各种版本均有或多或少的bug,或者效果不完美的地方。在使用QQ的时候,在消息列表界面的下拉刷新,个人感觉效果比较棒,就做了一个高仿版,效果与QQ的基本保持一致,有不足之处,欢迎指正。

源码下载地址:

http://download.csdn.net/detail/yutou58nian/6708851

又重构了一下代码,目前接近完美版!

仅有的一个小问题是,滑开头部,在下拉刷新和松手立即刷新状态来回切换时,滑动的弹性效果会变小。原因是在一直滑动的过程中,根据手指滑动的距离,一直setPadding时,会导致头部的paddingTop值跟实际显示在界面上的效果不一致,暂时还不知道怎么解决。

效果图如下:





        主要实现的特殊效果如下:

1.  下拉时缩减手指滑动距离,实现越拉越难的效果

2.  加载状态时,上推界面遮挡部分头部,头部自动收回

3.  加载状态时,界面依然可以下拉,松手自动收回,只显示头部

4.  加载完成后,有加载完成的状态,停留1秒之后自动收回

5.  ListView中数据长度没有充满屏幕时,可以下拉刷新

6.  ListView中没有数据时,可以下拉刷新

7.  所有的下拉,回弹均有动画效果


具体的实现思路跟网上的是一样的,就是给ListView添加HeadView,默认隐藏,通过监听OnTouch、OnScroll事件实现滑动时的各种效果。

只说说在实现时遇到的几个问题是怎么解决的:

1. 实现越拉越难的效果

在实现拉动越来越难的效果时,通过监听MotionEvent.ACTION_MOVE事件,取手指滑动距离的1/3,代码如下:

			if (currentHeaderState != REFRESH_BACED) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
								+ (int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
			} else if (currentHeaderState == REFRESH_BACED
					&& headVisible == true) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(),
						(int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
			} else if (currentHeaderState == REFRESH_BACED
					&& headVisible == false) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
								+ (int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());

			}

下拉时一共有三种状态:A. 正常状态、B. 加载状态且头部隐藏、C.加载状态且头部显示。

其中A状态和B状态处理方式一样,直接从手指滑动开始计算,拉开滑动距离1/3的效果

C状态时,滑动时位置减去头部的高度,再开始滑动。

直接通过setPadding来实现滑开的效果,且滑动距离缩减1/3,如果手指不松开,来回滑动的话,会导致距离计算不正确,所以在设置回弹效果的时候,要做处理,不然会导致界面收回之后,List中的部分条目也被遮挡。


2. 实现回弹动画

这个因为ListView也是在主界面的线程中,所以可以使用Handler.postDelayed()来实现,每次缩减剩余高度的1/4,5毫秒刷新一次即可。

这里主要实现了两个动画,一个是头部隐藏动画,用于未达到刷新状态,和遮挡部分加载中的头部时的动画

另外一个是头部收回动画,用于下拉高度超出头部高度时,头部的松手回弹动画,代码如下:

Runnable headHideAnimation = new Runnable() {
		public void run() {
			if (mHeaderLinearLayout.getBottom() > 0) {
				int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout
						.getPaddingTop() * 0.75f) - 1;
				if (paddingTop < -mHeaderHeight) {
					paddingTop = -mHeaderHeight;
				}
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), paddingTop,
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				handler.postDelayed(headHideAnimation, 5);
			} else {
				handler.removeCallbacks(headHideAnimation);
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight,
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				setSelection(1);
				headVisible = false;
			}
		}
	};

	Runnable headBackAnimation = new Runnable() {
		public void run() {
			if (mHeaderLinearLayout.getPaddingTop() > 1) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(),
						(int) (mHeaderLinearLayout.getPaddingTop() * 0.75f),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				handler.postDelayed(headBackAnimation, 5);
			} else {
				headVisible = true;
				handler.removeCallbacks(headBackAnimation);
			}
		}
	};
3. 实现ListView中数据没有充满屏幕时的下拉

当ListView中的数据没有充满屏幕的时候,滑动ListView没有内容的部分,监听不到onScrollStateChanged()事件,只能监听到onTouchEvent()、onScroll()这两个事件,如果不做特殊处理的话,会导致下拉之后状态不改变。

所以在onTouchEvent()中的Move事件中将界面的状态由静止改为滑动,即可解决问题。

全部的代码实现如下:

public class RefreshListView extends ListView implements OnScrollListener {

	private float mDownY;
	private float mMoveY;

	private int mHeaderHeight;

	private int mCurrentScrollState;

	private final static int NONE_PULL_REFRESH = 0; // 正常状态
	private final static int ENTER_PULL_REFRESH = 1; // 进入下拉刷新状态
	private final static int OVER_PULL_REFRESH = 2; // 进入松手立即刷新状态
	// 加载状态下拉
	private final static int PUSH_REFRESHING = 3; // 加载状态中,隐藏部分正在加载
	private final static int OVER_PULL_REFRESHING = 4; // 加载状态中,滑开超出titlebar高度
	private int mPullRefreshState = 0; // 记录当前滑动状态
	// 松手后,界面状态
	private final static int REFRESH_BACED = 1; // 反弹结束,刷新中
	private final static int REFRESH_RETURN = 2; // 没有达到刷新界限,返回
	private final static int REFRESH_DONE = 3; // 加载数据结束
	private final static int REFRESH_ORIGINAL = 4; // 最初的状态
	public int currentHeaderState = -1; // 记录当前数据加载状态

	private boolean headVisible = false;

	private LinearLayout mHeaderLinearLayout = null;
	private TextView mHeaderTextView = null;
	private ImageView mHeaderPullDownImageView = null;
	private ImageView mHeaderProgressImage = null;
	private ImageView mHeaderRefreshOkImage = null;
	private RefreshListener mRefreshListener = null;

	private RotateAnimation animation;
	private RotateAnimation reverseAnimation;
	private boolean isBack = false;
	private Handler handler = new Handler();

	public void setOnRefreshListener(RefreshListener refreshListener) {
		this.mRefreshListener = refreshListener;
	}

	public RefreshListView(Context context) {
		this(context, null);
	}

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

	public void init(final Context context) {
		mHeaderLinearLayout = (LinearLayout) LayoutInflater.from(context)
				.inflate(R.layout.refresh_list_header, null);
		addHeaderView(mHeaderLinearLayout);
		mHeaderTextView = (TextView) findViewById(R.id.refresh_list_header_text);
		mHeaderPullDownImageView = (ImageView) findViewById(R.id.refresh_list_header_pull_down);
		mHeaderProgressImage = (ImageView) findViewById(R.id.refresh_list_header_loading);
		mHeaderRefreshOkImage = (ImageView) findViewById(R.id.refresh_list_header_success);

		setSelection(1);
		setOnScrollListener(this);
		measureView(mHeaderLinearLayout);
		mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();

		mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
				-mHeaderHeight, mHeaderLinearLayout.getPaddingRight(),
				mHeaderLinearLayout.getPaddingBottom());

		animation = new RotateAnimation(0, 180,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f);
		animation.setInterpolator(new LinearInterpolator());
		animation.setDuration(150);
		animation.setFillAfter(true);// 箭头翻转动画

		reverseAnimation = new RotateAnimation(180, 0,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f,
				RotateAnimation.RELATIVE_TO_SELF, 0.5f);
		reverseAnimation.setInterpolator(new LinearInterpolator());
		reverseAnimation.setDuration(150);
		reverseAnimation.setFillAfter(true);// 箭头反翻转动画
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			mDownY = ev.getY();
			handler.removeCallbacks(headHideAnimation);
			handler.removeCallbacks(headBackAnimation);
			break;
		case MotionEvent.ACTION_MOVE:
			mMoveY = ev.getY();
			if (mCurrentScrollState == SCROLL_STATE_IDLE) {
				mCurrentScrollState = SCROLL_STATE_TOUCH_SCROLL;
			}
			if (currentHeaderState != REFRESH_BACED) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
								+ (int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
			} else if (currentHeaderState == REFRESH_BACED
					&& headVisible == true) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(),
						(int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
			} else if (currentHeaderState == REFRESH_BACED
					&& headVisible == false) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
								+ (int) ((mMoveY - mDownY) / 3),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());

			}

			break;
		case MotionEvent.ACTION_UP:
			if (mPullRefreshState == OVER_PULL_REFRESH) {
				currentHeaderState = REFRESH_BACED;
				handler.postDelayed(headBackAnimation, 5);
				refreshViewByState();
			} else if (mPullRefreshState == ENTER_PULL_REFRESH) {
				currentHeaderState = REFRESH_RETURN;
				handler.postDelayed(headHideAnimation, 5);
				refreshViewByState();
			} else if (mPullRefreshState == PUSH_REFRESHING) {
				handler.postDelayed(headHideAnimation, 5);
			} else if (mPullRefreshState == OVER_PULL_REFRESHING) {
				handler.postDelayed(headBackAnimation, 5);
			}

			break;
		}
		return super.onTouchEvent(ev);
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		if (currentHeaderState != REFRESH_BACED) {
			if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
					&& firstVisibleItem == 0
					&& (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
							.getBottom() < mHeaderHeight)) {

				mPullRefreshState = ENTER_PULL_REFRESH;
				mHeaderTextView.setText(R.string.app_list_header_refresh_down);
				mHeaderPullDownImageView.setVisibility(View.VISIBLE);
				mHeaderRefreshOkImage.setVisibility(View.GONE);

				if (isBack) {
					isBack = false;
					mHeaderPullDownImageView.clearAnimation();
					mHeaderPullDownImageView.startAnimation(reverseAnimation);
				}
			} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
					&& firstVisibleItem == 0
					&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
				isBack = true;

				if (mPullRefreshState == ENTER_PULL_REFRESH
						|| mPullRefreshState == NONE_PULL_REFRESH) {
					mPullRefreshState = OVER_PULL_REFRESH;
					mHeaderTextView.setText(R.string.app_list_header_refresh);
					mHeaderPullDownImageView.clearAnimation();
					mHeaderPullDownImageView.startAnimation(animation);
				}
			} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
					&& firstVisibleItem != 0) {
				if (mPullRefreshState == ENTER_PULL_REFRESH) {
					mPullRefreshState = NONE_PULL_REFRESH;
				}
			}
		} else {
			if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
					&& firstVisibleItem == 0
					&& (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
							.getBottom() < mHeaderHeight)) {
				mPullRefreshState = PUSH_REFRESHING;
			} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
					&& firstVisibleItem == 0
					&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
				mPullRefreshState = OVER_PULL_REFRESHING;
			}
		}

		if (mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem == 0) {
			setSelection(1);
		}
	}

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		mCurrentScrollState = scrollState;
	}

	@Override
	public void setAdapter(ListAdapter adapter) {
		super.setAdapter(adapter);
		setSelection(1);
	}

	private void measureView(View child) {
		ViewGroup.LayoutParams p = child.getLayoutParams();
		if (p == null) {
			p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
					ViewGroup.LayoutParams.WRAP_CONTENT);
		}

		int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
		int lpHeight = p.height;
		int childHeightSpec;
		if (lpHeight > 0) {
			childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
					MeasureSpec.EXACTLY);
		} else {
			childHeightSpec = MeasureSpec.makeMeasureSpec(0,
					MeasureSpec.UNSPECIFIED);
		}
		child.measure(childWidthSpec, childHeightSpec);
	}

	public void refreshViewByState() {
		switch (currentHeaderState) {
		case REFRESH_BACED:
			mHeaderTextView.setText(R.string.app_list_loading);
			mHeaderProgressImage.setVisibility(View.VISIBLE);
			mHeaderPullDownImageView.clearAnimation();
			mHeaderPullDownImageView.setVisibility(View.GONE);
			mPullRefreshState = NONE_PULL_REFRESH;
			isBack = false;
			if (mRefreshListener != null) {
				mRefreshListener.refreshing();
			}
			break;
		case REFRESH_RETURN:
			mPullRefreshState = NONE_PULL_REFRESH;
			currentHeaderState = REFRESH_ORIGINAL;
			break;
		case REFRESH_DONE:
			mHeaderTextView.setText(R.string.app_list_refresh_done);
			mHeaderProgressImage.setVisibility(View.INVISIBLE);
			mHeaderRefreshOkImage.setVisibility(View.VISIBLE);
			mPullRefreshState = NONE_PULL_REFRESH;
			currentHeaderState = REFRESH_ORIGINAL;
			mCurrentScrollState = SCROLL_STATE_IDLE;
			handler.postDelayed(headHideAnimation, 700);
			break;
		default:
			break;
		}
	}

	Runnable headHideAnimation = new Runnable() {
		public void run() {
			if (mHeaderLinearLayout.getBottom() > 0) {
				int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout
						.getPaddingTop() * 0.75f) - 1;
				if (paddingTop < -mHeaderHeight) {
					paddingTop = -mHeaderHeight;
				}
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), paddingTop,
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				handler.postDelayed(headHideAnimation, 5);
			} else {
				handler.removeCallbacks(headHideAnimation);
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight,
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				setSelection(1);
				headVisible = false;
			}
		}
	};

	Runnable headBackAnimation = new Runnable() {
		public void run() {
			if (mHeaderLinearLayout.getPaddingTop() > 1) {
				mHeaderLinearLayout.setPadding(
						mHeaderLinearLayout.getPaddingLeft(),
						(int) (mHeaderLinearLayout.getPaddingTop() * 0.75f),
						mHeaderLinearLayout.getPaddingRight(),
						mHeaderLinearLayout.getPaddingBottom());
				handler.postDelayed(headBackAnimation, 5);
			} else {
				headVisible = true;
				handler.removeCallbacks(headBackAnimation);
			}
		}
	};

	public interface RefreshListener {
		// 正在下拉刷新
		public void refreshing();
	}
}


头部的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/black"
    android:gravity="center"
    android:orientation="horizontal" >

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="50dp" >

        <ImageView
            android:id="@+id/refresh_list_header_loading"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:layout_marginLeft="@dimen/refresh_title_margin_left"
            android:contentDescription="@string/app_image_helper"
            android:src="@drawable/refresh_loading"
            android:visibility="gone" >
        </ImageView>

        <ImageView
            android:id="@+id/refresh_list_header_pull_down"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:layout_marginLeft="@dimen/refresh_title_margin_left"
            android:contentDescription="@string/app_image_helper"
            android:src="@drawable/refresh_arrow" />

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true" >

            <ImageView
                android:id="@+id/refresh_list_header_success"
                android:layout_width="15dp"
                android:layout_height="15dp"
                android:layout_centerVertical="true"
                android:contentDescription="@string/app_image_helper"
                android:src="@drawable/header_refresh_success"
                android:visibility="gone" >
            </ImageView>

            <TextView
                android:id="@+id/refresh_list_header_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginLeft="4dp"
                android:layout_toRightOf="@id/refresh_list_header_success"
                android:text="@string/app_list_header_refresh_down"
                android:textColor="@android:color/white"
                android:textSize="15sp" />
        </RelativeLayout>
    </RelativeLayout>

</LinearLayout>

代码中注释不多,敬请谅解。

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Android UI设计之<十>自定义ListView,实现QQ空间阻尼下拉刷新和渐变菜单栏效果

好久没有写有关UI的博客了,刚刚翻了一下之前的博客,最近一篇有关UI的博客是在2014年写的:Android UI设计之自定义Dialog,实现各种风格效果的对话框。近来项目有个需求,要做个和QQ空间...

安卓开发-ListView侧拉菜单,置顶等仿QQ侧拉菜单侧拉点击切换图片,侧拉点击事件带上拉加载更多,下拉刷新

项目中需要有listview侧拉出菜单,点击可以置顶,撤销,暂停,删除的功能 在此记录一下。 需求是侧拉可以拉出菜单,然后点击侧拉菜单的每个条目有相应的点击事件处理,而且再次侧拉的图标需要改变,再加...

仿QQ实现ListView中item的左右滑动同时实现ListView的上拉刷新和下拉加载更多

今天在群中有一哥们问到“QQ中ListView的item的左右滑动并支持上拉刷新和下拉加载的控件”,在网上搜索一下发现了PullToRefresh-SwipeMenuListView这个控件,gith...

Android ExpandableListView 仿QQ列表页面,title永远在上面,带下拉刷新,上拉加载。

最近公司有项目需求要求实现这功能,费了一天时间弄了下,决定共享出去。展开后header会固定在最上方   首先网上下载PullToRefreshView    具体内容如下: package ...

Android 仿IOS版QQ实现下拉刷新水滴的效果

基于XListView的基础上,把它的头部刷新换成自己定义的控件,这与IOS版上的QQ刷新类似。效果如下: 水滴效果可以看我的上一篇博客 水滴效果实现一、代码由于这是基于XListView 的,所...

Android 下拉刷新框架实现、仿新浪微博、QQ好友动态滑到底部自动加载

前段时间项目中用到了下拉刷新功能,之前在网上也找到过类似的demo,但这些demo的质量参差不齐,用户体验也不好,接口设计也不行。最张没办法,终于忍不了了,自己就写了一个下拉刷新的框架,这个框架是一个...

Android高效率实现自定义下拉刷新,仿QQ效果

转载请注明出处:http://blog.csdn.net/jakeyangchina/article/details/53224478不知不觉来北京工作快到一年了,从来也没养成写博客的习惯,平时自己总...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)