高仿QQ聊天消息列表的下拉刷新效果


转载请注明出处:

最近下载了一个关于仿QQ下拉刷新效果的源码,功能实现还不错,就花了点时间阅读了下源码,学习之后在源码的基础上又进行一定的优化,感觉效果还不错,就拿出来和大家分享一下。

一、下拉刷新的原理:

这边采用的是自定义RefreshView继承ListView,在listview中加入顶部布局,通过监听listView的滚动状态进行顶部布局的显示隐藏。


为ListView添加顶部布局也就是原理图的下拉头部分,创建一个布局refresh_list_header.xml

<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>


2、自定义一个MyRefreshListview继承ListView。重写构造方法,初始化顶部布局并添加进ListView中

<pre name="code" class="java">@SuppressLint("InflateParams")
	private void initView(Context context) {
		mHeaderView = LayoutInflater.from(context).inflate(
				R.layout.refresh_list_header, null);
		mHeaderTextView = (TextView) mHeaderView
				.findViewById(R.id.refresh_list_header_text);
		mHeaderPullDownImageView = (ImageView) mHeaderView
				.findViewById(R.id.refresh_list_header_pull_down);
		mHeaderLoadingImage = (ImageView) mHeaderView
				.findViewById(R.id.refresh_list_header_loading);
		mHeaderRefreshOkImage = (ImageView) mHeaderView
				.findViewById(R.id.refresh_list_header_success);
		
		<span style="color:#ff6666;background-color: rgb(255, 255, 255);">addHeaderView(mHeaderView);</span>
<pre name="code" class="java" style="font-size: 14px;">                measureView(mHeaderView);
		mHeaderHeight = mHeaderView.getMeasuredHeight();
		topPadding(-mHeaderHeight);
this.setOnScrollListener(this);}

 
           上面代码中在红色字体之前把布局添加到ListView的头部,此时运行程序时,就可以看到我们添加的布局了,但正常情况头部是隐藏起来的,别急,接下我们就把头部隐藏起来,可以细心的你可能就会发现上面已经有调用隐藏头部布局的方法了,不错就是topPadding()这个方法。 

/**
	 * 顶部布局距离顶部的内边距设置
	 * 
	 * @param topPadding
	 */
	private void topPadding(int topPadding) {
		mHeaderView.setPadding(mHeaderView.getPaddingLeft(), topPadding,
				mHeaderView.getPaddingRight(), mHeaderView.getPaddingBottom());
	}
代码很简单,就是设置距离顶部的内边距,左右和下保持默认就可以了。(注:当内边距设置为负数时,布局就会隐藏了),不过当你再次运行时发现奇迹没有发生,顶部布局还是能看到并没有隐藏,这是为什么呢?哈哈,这是因为 mHeaderView.getMeasuredHeight();这个方法返回过来的值是0,所以不会隐藏,因为父布局不知道顶部布局所需要的空间是多少,这里我们就要告诉父布局所需要的空间大小。

/**
	 * 告诉父布局自己所占用的空间
	 * 
	 * @param view
	 */
	private void measureView(View view) {
		ViewGroup.LayoutParams p = view.getLayoutParams();
		if (p == null) {
			p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
					ViewGroup.LayoutParams.WRAP_CONTENT);
		}
		int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
		int tmpHeight = p.height;
		int height;
		if (tmpHeight > 0) {
			height = MeasureSpec
					.makeMeasureSpec(tmpHeight, MeasureSpec.EXACTLY);
		} else {
			height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
		}
		view.measure(width, height);
	}
 这样再次运行代码就不会看到顶部布局了,因为他已经隐藏掉了。

3、实现下拉刷新效果:
    首先分析下,下拉刷新时,我们在下拉的时候有哪几种情况,回想使用扣扣的时候,正常情况下,我们可以下拉后释放刷新;正在刷新时,我们下拉,还在刷新;刷新完成后,我们下拉时显示刷新完成状态。所以经过我们一番思考当我们下拉的时候会遇到三种情况,正常情况,正在刷新,刷新完成。

/** 顶部布局的三种逻辑状态 */
	private static final int HEADER_VIEW_NORMAL = 0;// 布局正常状态
	private static final int HEADER_VIEW_REFRESHING = 1;// 布局刷新状态
	private static final int HEADER_VIEW_REFRESHED = 2;// 布局刷新完成状态
	private int currentViewState = HEADER_VIEW_NORMAL;// 当前的view状态
在下拉过程中我们又会进来哪种中状态呢顶部布局隐藏、下拉没有超过下拉界限、下拉超过下拉界限,又有三种状态。
/** 顶部布局高度的三种状态 */
	private static final int HEADER_HIDE = 0;// 隐藏状态
	private static final int HEADER_SHOW = 1;// 正常显示状态
	private static final int HEADER_OVER_SHOW = 2;// 过度显示状态
从上面分析可知,我们会在每种不同的情况下,正常,正在刷新,刷新成功时,进行下拉都会经行顶部布局下拉过程的三种状态,所以我们也要为每种情况下,下拉要经历的三种状态设置标志

/** 以下9种状态分别是布局的正常状态、刷新状态和刷新完成的三种状态的子状态 */
	// 未加载状态
	private final static int NONE_PULL_REFRESH = 0; // 未加载,隐藏全部头部
	private final static int HALF_PULL_REFRESH = 1; // 未加载,隐藏部分头部
	private final static int OVER_PULL_REFRESH = 2; // 未加载,滑开头部
	// 加载状态下拉
	private final static int NONE_PULL_REFRESHING = 3; // 加载状态中,隐藏全部头部
	private final static int HALF_PULL_REFRESHING = 4; // 加载状态中,隐藏部分头部
	private final static int OVER_PULL_REFRESHING = 5; // 加载状态中,滑开头部
	// 加载完成状态
	private final static int NONE_PULL_REFRESHED = 6; // 加载完成,隐藏全部头部
	private final static int HALF_PULL_REFRESHED = 7; // 加载完成,隐藏部分头部
	private final static int OVER_PULL_REFRESHED = 8; // 加载完成,滑开头部
	private int currentRefreshState = NONE_PULL_REFRESH; // 记录当前刷新状态
这样我们就基本把下拉刷新的所有状态都包含进去了。

接下来就是监听滑动屏幕时,让顶部布局根据具体情况进行不同状态切换了。

监听屏幕滑动

@Override
	public boolean onTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			<span style="color:#ff6666;">mDownY</span> = (int) ev.getY();
			isTouch = true;
			break;
		case MotionEvent.ACTION_MOVE:
			<span style="color:#ff9966;">mMoveY</span> = (int) ev.getY();
			isTouch = true;
		<span style="background-color: rgb(255, 255, 255);">	<span style="color:#3333ff;">if (firstVisibleItem == 0 && isPullRefresh) {
				if (!isHeaderVisible) {
					topPadding(-mHeaderHeight + (mMoveY - mDownY) / 4);
				} else {
					topPadding((mMoveY - mDownY) / 4);
				}
			}</span></span>
			break;
		case MotionEvent.ACTION_UP:
			isTouch = false;
			handleHeaderViewByRelease();
			break;
		}
		return super.onTouchEvent(ev);
	}
    代码不是很长,这里简单解释下,mDownY记录我们按下时的Y坐标,mMoveY记录我们滑动时的Y坐标。蓝色部分的代码中firstVisibleItem判断listView是否已经滑动顶部了,isPullRefresh是判断是否开启下拉刷新功能,当满足情况时,就可以下拉顶部布局了,isHeaderViesible是记录当没有触摸屏幕的时候,顶部布局是否处于显示状态,为什么说是没有触摸屏幕时呢,因为当你下拉的时候,虽然布局是能看到,但此时你正处于操作的过程中,不属于正常的状态。当顶部布局不显示时,我们的下拉距离要减去布局的原始高度,因为你是一点一点的拉出来的嘛,但当显示的时候就是我们下拉的距离,这里取我们下拉的四分之一主要是减慢下拉速度,可以随意设置自己喜欢的值,不要被这迷惑。


当我们下拉的时候顶部布局的下拉过程的状态也在不停变化,我们要获得不同状态下的标志

/**
	 * 获取顶部布局下拉的不同显示状态标识
	 * 
	 * @return
	 */
	private int getHeaderShowState() {
		if (mHeaderView.getBottom() >= 0
				&& mHeaderView.getBottom() < mHeaderHeight + 50) {
			return HEADER_SHOW;
		} else if (mHeaderView.getBottom() >= mHeaderHeight + 50) {
			return HEADER_OVER_SHOW;
		} else {
			return HEADER_HIDE;
		}
	}
   这里需要注意下getBottom()=getTop()+getHeight();哈哈,不懂得可以问下度娘,这里我就不说了;50是我附加的阀值,可以根据自己的兴趣设置。

@Override
	public void onScroll(AbsListView view, int firstVisibleItem,
			int visibleItemCount, int totalItemCount) {
		this.firstVisibleItem = firstVisibleItem;
		<span style="color:#ff0000;">if (firstVisibleItem == 0
				&& currentScrollState == SCROLL_STATE_TOUCH_SCROLL) {
			changeHeaderViewByViewState();
		}</span>
		// 防止猛抛时第一个item隐藏掉
		if (currentScrollState == SCROLL_STATE_FLING && firstVisibleItem == 0) {
			setSelection(1);
		}
	}
 根据滚动状态更改相应的顶部布局显示。
/**
	 * 根据布局状态改变布局显示内容
	 */
	private void changeHeaderViewByViewState() {
		switch (currentViewState) {
		case HEADER_VIEW_NORMAL:// 正常状态
			handleNorMalState();
			break;
		case HEADER_VIEW_REFRESHING:// 刷新状态
			handleRefreshingState();
			break;
		case HEADER_VIEW_REFRESHED:// 刷新完成状态
			handleRefreshedState();
			break;
		}
	}
代码很简单没什么可说的

/**
	 * 处理正常状态
	 */
	private void handleNorMalState() {
		if (getHeaderShowState() == HEADER_SHOW) {// 下拉高度状态
			if (currentRefreshState == NONE_PULL_REFRESH) {//
				currentRefreshState = HALF_PULL_REFRESH;
				mHeaderTextView.setText(PULL_TO_REFRESH);
				mHeaderLoadingImage.setVisibility(View.GONE);
				mHeaderPullDownImageView.setVisibility(View.VISIBLE);
				mHeaderRefreshOkImage.setVisibility(View.GONE);
			}
			if (isPullBack && currentRefreshState == OVER_PULL_REFRESH) {// 判断是否是返回
				currentRefreshState = HALF_PULL_REFRESH;
				mHeaderTextView.setText(PULL_TO_REFRESH);
				isPullBack = false;
				mHeaderPullDownImageView.clearAnimation();
				mHeaderPullDownImageView.startAnimation(downAnimation);
			}
		} else if (getHeaderShowState() == HEADER_OVER_SHOW) {// 下拉越界
			if (currentRefreshState != OVER_PULL_REFRESH) {
				currentRefreshState = OVER_PULL_REFRESH;
				isPullBack = true;
				mHeaderTextView.setText(RELEASE_TO_REFRESH);
				mHeaderPullDownImageView.clearAnimation();
				mHeaderPullDownImageView.startAnimation(upAnimation);
			}
		} else {// 全部隐藏
			currentRefreshState = NONE_PULL_REFRESH;
		}
	}
这段代码是处理正常状态下,下拉刷新的情况,在正常状态下根据不同的下拉情况设置正常状态下的 currentRefreshState 值并更改相应的布局并下拉越界返回时,进行箭头的方向变化的动画

/**
	 * 处理刷新状态
	 */
	private void handleRefreshingState() {
		if (getHeaderShowState() == HEADER_SHOW) {// 下拉高度状态
			if (currentRefreshState == NONE_PULL_REFRESHING
					|| currentRefreshState == OVER_PULL_REFRESHING) {//
				currentRefreshState = HALF_PULL_REFRESHING;
			}
		} else if (getHeaderShowState() == HEADER_OVER_SHOW) {// 下拉越界
			currentRefreshState = OVER_PULL_REFRESHING;
		} else {// 全部隐藏
			currentRefreshState = NONE_PULL_REFRESHING;
		}
	}

	/**
	 * 处理刷新完成状态
	 */
	private void handleRefreshedState() {
		if (getHeaderShowState() == HEADER_SHOW) {// 下拉高度状态
			if (currentRefreshState == NONE_PULL_REFRESHED
					|| currentRefreshState == OVER_PULL_REFRESHED) {//
				currentRefreshState = HALF_PULL_REFRESHED;
			}
		} else if (getHeaderShowState() == HEADER_OVER_SHOW) {// 下拉越界
			currentRefreshState = OVER_PULL_REFRESHED;
		} else {// 全部隐藏
			currentRefreshState = NONE_PULL_REFRESHED;
		}
	}
同上,这里因为正在刷新和刷新完成的顶部布局显示效果是不变的所以这里只是更改状态标识。

这样我们的触摸的下拉状态就完成了,接下来就是释放后的状态处理了


/**
	 * 释放后根据标志处理不同状态
	 */
	private void handleHeaderViewByRelease() {
		switch (currentRefreshState) {
		case NONE_PULL_REFRESH:    //隐藏状态
		case NONE_PULL_REFRESHING:
		case NONE_PULL_REFRESHED:
			resetHeaderPadding(); //重置
			break;
		case HALF_PULL_REFRESH:
			handler.postDelayed(headHideAnimation, 5);//递归的逐渐隐藏布局
			break;
		case OVER_PULL_REFRESH:
			changeHeaderViewToRefreshing();
			handler.postDelayed(headBackAnimation, 5);//递归的返回到之前的显示状态
			<span style="color:#ff0000;">refreshData();//刷新数据</span>
			break;
		case OVER_PULL_REFRESHING:
			handler.postDelayed(headBackAnimation, 5);
			break;
		case HALF_PULL_REFRESHING:
		case HALF_PULL_REFRESHED:
		case OVER_PULL_REFRESHED:
			handler.postDelayed(headHideAnimation, 5);
			break;
		}

	}


private void resetHeaderPadding() {
		isHeaderVisible = false;
		topPadding(-mHeaderHeight);
	}

/**
	 * 释放后顶部布局逐步返回到之前的状态
	 */
	Runnable headBackAnimation = new Runnable() {
		public void run() {
			if (mHeaderView.getPaddingTop() > 1) {
				topPadding((int) (mHeaderView.getPaddingTop() * 0.75f));
				handler.postDelayed(headBackAnimation, 5);
			}
		}
	};

/**
	 * 释放后逐步隐藏顶部布局
	 */
	Runnable headHideAnimation = new Runnable() {
		public void run() {
			if (mHeaderView.getBottom() > 0 && getFirstVisiblePosition() == 0) {
//每次返回的长度
				int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderView
						.getPaddingTop() * 0.75f) - 1;
				if (paddingTop < -mHeaderHeight) {
					paddingTop = -mHeaderHeight;
				}
				topPadding(paddingTop);
				handler.postDelayed(headHideAnimation, 5);
			} else {
//当隐藏时,重置
				resetHeaderPadding();
//防止过猛,隐藏掉第二个item(此时头部布局是第一个item所以这里设置成1而不是0)
				setSelection(1);
				if (currentViewState == HEADER_VIEW_REFRESHED) {//当前状态是刷新完成则重置顶部布局
					changeHeaderToOriginal();
				} else if (currentViewState == HEADER_VIEW_REFRESHING) {
					currentRefreshState = NONE_PULL_REFRESHING;
				} else if (currentViewState == HEADER_VIEW_NORMAL) {
					currentRefreshState = NONE_PULL_REFRESH;
				}
			}
		}
	};
/**
	 * 将状态设置为初始状态
	 */
	private void changeHeaderToOriginal() {
		isPullBack = false;
		isHeaderVisible = false;
		mHeaderTextView.setText(PULL_TO_REFRESH);
		mHeaderPullDownImageView.clearAnimation();
		mHeaderPullDownImageView.setVisibility(View.VISIBLE);
		mHeaderLoadingImage.setVisibility(View.GONE);
		mHeaderRefreshOkImage.setVisibility(View.GONE);
		currentViewState = HEADER_VIEW_NORMAL;
		currentRefreshState = NONE_PULL_REFRESH;
	}


到这里基本就把下拉刷新的效果就做完了,接下来就是添加一个回调接口,用于数据刷新


/**
	 * 回调接口
	 * 
	 * @author liu
	 */
	public interface OnRefreshListener {
		void pullRefresh(Handler handler);
	}

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

/**
	 * 开启子线程刷新数据
	 */
	private void refreshData() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				if (mRefreshListener != null) {
					mRefreshListener.pullRefresh(handler);
				}
			}
		}).start();
	}
这里在内部开启了一个子线程用于数据刷新,handler用于传递刷新成功还是失败的状态值。


private Handler handler = new Handler() {
		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			case REFRESHEDA_SUCCESS:
				changeHeaderToRefershed(true);
				break;
			case REFRESHEDA_FAILE:
				changeHeaderToRefershed(false);
				break;
			}
		};
	};


/**
	 * 将布局显示状态改为刷新完成状态
	 */
	private void changeHeaderToRefershed(boolean isSuccess) {
		if (isSuccess)
			mHeaderTextView.setText(REFRESH_SUCCESS);
		else
			mHeaderTextView.setText(REFRESH_FAILE);
		mHeaderPullDownImageView.clearAnimation();
		mHeaderPullDownImageView.setVisibility(View.GONE);
		mHeaderLoadingImage.setVisibility(View.GONE);
		mHeaderRefreshOkImage.setVisibility(View.VISIBLE);
		currentRefreshState = OVER_PULL_REFRESHED;
		currentViewState = HEADER_VIEW_REFRESHED;
		isHeaderVisible = true;
		if (!isTouch) {
			handler.postDelayed(headHideAnimation, 500);
		}
	}


这样仿扣扣的下拉刷新效果就写完了接下来就是简单的使用了

定义一个Activity为其添加布局

<RelativeLayout 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="@android:color/white" >

    <com.acha.android.ui.helper.MyRefreshListView
        android:id="@+id/pull_refresh_listview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:cacheColorHint="@android:color/transparent" >
    </com.acha.android.ui.helper.MyRefreshListView>

</RelativeLayout>

package com.acha.android.ui;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.acha.android.ui.helper.MyRefreshListView;
import com.acha.android.ui.helper.MyRefreshListView.OnRefreshListener;

public class MyActivity extends Activity implements OnRefreshListener {

	private MyRefreshListView refreshListView;
	private List<String> messages;
	private MessageAdapter messageAdapter;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_me);
		initView();
		initListData();
		initListView();
	}

	private void initView() {
		refreshListView = (MyRefreshListView) findViewById(R.id.pull_refresh_listview);
		refreshListView.setOnRefreshListener(this);
		refreshListView.setPullToRefresh(true);
	}

	private void initListData() {
		messages = new ArrayList<String>();
		for (int i = 0; i < 15; i++) {
			messages.add("Old Data " + i);
		}
	}

	private void initListView() {
		messageAdapter = new MessageAdapter();
		refreshListView.setAdapter(messageAdapter);
	}

	class MessageAdapter extends BaseAdapter {

		@Override
		public int getCount() {
			return messages.size();
		}

		@Override
		public Object getItem(int position) {
			return messages.get(position);
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		public class ViewHolder {
			public Button messagePButton;
			public TextView messageTextTV;
			public TextView messageTimeTV;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder holder = null;
			if (convertView == null) {
				convertView = LayoutInflater.from(getApplicationContext())
						.inflate(R.layout.message_list_item, null);
				holder = new ViewHolder();
				holder.messageTextTV = (TextView) convertView
						.findViewById(R.id.message_tv);
				convertView.setTag(holder);
			} else {
				holder = (ViewHolder) convertView.getTag();
			}

			holder.messageTextTV.setText(messages.get(position));

			return convertView;
		}
	}

	@Override
	public void pullRefresh(Handler handler) {
		long time1 = System.currentTimeMillis();
		long time2;
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		time2 = System.currentTimeMillis();
		if (time2 - time1 < 5)
			handler.sendEmptyMessage(MyRefreshListView.REFRESHEDA_SUCCESS);
		else
			handler.sendEmptyMessage(MyRefreshListView.REFRESHEDA_FAILE);
	}

}


到这里高仿QQ聊天消息列表的下拉刷新效果就将完了,不要急着走开,到了大家最喜欢的源码推荐环节,请猛向这里戳:


源码下载




  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值