Recently, the ListView pull-down refresh function was needed in the project. At first, I wanted to save trouble and directly found a ready-made one on the Internet, but after trying several versions of the pull-down refresh function on the Internet, I found that the effect was not ideal. Some are incomplete or buggy, and some are too complicated to use. Therefore, I also gave up the idea of looking for ready-made code on the Internet, and made efforts to write a very simple drop-down refresh implementation scheme, and now here to share with you. By the end of this article, you should be able to introduce a pull-down refresh in your own projects in a minute.

one

First, let’s talk about the implementation principle. The solution we will take here is to use the combination View method, first define a layout inherited from the LinearLayout, then add the drop-down head and ListView child elements to the layout, and make the two child elements vertically arranged. When initializing, let the dropdown head drift up off the screen so that all we see is the ListView. Then listen for the ListView touch event, if the current ListView has been scrolled to the top and the finger is still pulling down, it will display the drop-down head, release the refresh operation, and hide the drop-down head. The schematic diagram of the principle is as follows:

Create a new project called PullToRefreshTest and define a pull_to_refresh. XML layout file with pull_to_refresh header as follows:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/pull_to_refresh_head" android:layout_width="fill_parent"  android:layout_height="60dip" > <LinearLayout android:layout_width="200dip" android:layout_height="60dip" android:layout_centerInParent="true" android:orientation="horizontal" > <RelativeLayout android:layout_width="0dip" android:layout_height="60dip" android:layout_weight="3" > <ImageView android:id="@+id/arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@drawable/arrow" /> <ProgressBar android:id="@+id/progress_bar" android:layout_width="30dip" android:layout_height="30dip" android:layout_centerInParent="true" android:visibility="gone" /> </RelativeLayout> <LinearLayout android:layout_width="0dip" android:layout_height="60dip" android:layout_weight="12" android:orientation="vertical" > <TextView android:id="@+id/description" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center_horizontal|bottom" android:text="@string/pull_to_refresh" /> <TextView android:id="@+id/updated_at" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center_horizontal|top" android:text="@string/updated_at" /> </LinearLayout> </LinearLayout> </RelativeLayout>Copy the code

In this layout, we include a drop down indicator arrow, a drop down status text prompt, and a date of the last update. Of course, there’s also a hidden rotating progress bar that only shows up when we’re refreshing.

All strings referenced in the layout are placed in strings.xml, as shown below:

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <string name="app_name">PullToRefreshTest</string> <string name="pull_to_refresh"> Drop down to refresh </string> <string Name ="release_to_refresh"> </string> <string name="refreshing"> Refreshing... </string> <string name="not_updated_yet"> Not updated </string> <string name=" updated_AT "> Last updated before %1$s </string> <string Name ="updated_just_now"> just updated </string> <string name="time_error">Copy the code

two

Then create a new RefreshableView inherited from the LinearLayout like this:

public class RefreshableView extends LinearLayout implements OnTouchListener {
 
	/**
	 * 下拉状态
	 */
	public static final int STATUS_PULL_TO_REFRESH = 0;
 
	/**
	 * 释放立即刷新状态
	 */
	public static final int STATUS_RELEASE_TO_REFRESH = 1;
 
	/**
	 * 正在刷新状态
	 */
	public static final int STATUS_REFRESHING = 2;
 
	/**
	 * 刷新完成或未刷新状态
	 */
	public static final int STATUS_REFRESH_FINISHED = 3;
 
	/**
	 * 下拉头部回滚的速度
	 */
	public static final int SCROLL_SPEED = -20;
 
	/**
	 * 一分钟的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_MINUTE = 60 * 1000;
 
	/**
	 * 一小时的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_HOUR = 60 * ONE_MINUTE;
 
	/**
	 * 一天的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_DAY = 24 * ONE_HOUR;
 
	/**
	 * 一月的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_MONTH = 30 * ONE_DAY;
 
	/**
	 * 一年的毫秒值,用于判断上次的更新时间
	 */
	public static final long ONE_YEAR = 12 * ONE_MONTH;
 
	/**
	 * 上次更新时间的字符串常量,用于作为SharedPreferences的键值
	 */
	private static final String UPDATED_AT = "updated_at";
 
	/**
	 * 下拉刷新的回调接口
	 */
	private PullToRefreshListener mListener;
 
	/**
	 * 用于存储上次更新时间
	 */
	private SharedPreferences preferences;
 
	/**
	 * 下拉头的View
	 */
	private View header;
 
	/**
	 * 需要去下拉刷新的ListView
	 */
	private ListView listView;
 
	/**
	 * 刷新时显示的进度条
	 */
	private ProgressBar progressBar;
 
	/**
	 * 指示下拉和释放的箭头
	 */
	private ImageView arrow;
 
	/**
	 * 指示下拉和释放的文字描述
	 */
	private TextView description;
 
	/**
	 * 上次更新时间的文字描述
	 */
	private TextView updateAt;
 
	/**
	 * 下拉头的布局参数
	 */
	private MarginLayoutParams headerLayoutParams;
 
	/**
	 * 上次更新时间的毫秒值
	 */
	private long lastUpdateTime;
 
	/**
	 * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
	 */
	private int mId = -1;
 
	/**
	 * 下拉头的高度
	 */
	private int hideHeaderHeight;
 
	/**
	 * 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
	 * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
	 */
	private int currentStatus = STATUS_REFRESH_FINISHED;;
 
	/**
	 * 记录上一次的状态是什么,避免进行重复操作
	 */
	private int lastStatus = currentStatus;
 
	/**
	 * 手指按下时的屏幕纵坐标
	 */
	private float yDown;
 
	/**
	 * 在被判定为滚动之前用户手指可以移动的最大值。
	 */
	private int touchSlop;
 
	/**
	 * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
	 */
	private boolean loadOnce;
 
	/**
	 * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
	 */
	private boolean ableToPull;
 
	/**
	 * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。
	 * 
	 * @param context
	 * @param attrs
	 */
	public RefreshableView(Context context, AttributeSet attrs) {
		super(context, attrs);
		preferences = PreferenceManager.getDefaultSharedPreferences(context);
		header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);
		progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
		arrow = (ImageView) header.findViewById(R.id.arrow);
		description = (TextView) header.findViewById(R.id.description);
		updateAt = (TextView) header.findViewById(R.id.updated_at);
		touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
		refreshUpdatedAtValue();
		setOrientation(VERTICAL);
		addView(header, 0);
	}
 
	/**
	 * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。
	 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		super.onLayout(changed, l, t, r, b);
		if (changed && !loadOnce) {
			hideHeaderHeight = -header.getHeight();
			headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
			headerLayoutParams.topMargin = hideHeaderHeight;
			listView = (ListView) getChildAt(1);
			listView.setOnTouchListener(this);
			loadOnce = true;
		}
	}
 
	/**
	 * 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。
	 */
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		setIsAbleToPull(event);
		if (ableToPull) {
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				yDown = event.getRawY();
				break;
			case MotionEvent.ACTION_MOVE:
				float yMove = event.getRawY();
				int distance = (int) (yMove - yDown);
				// 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
				if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
					return false;
				}
				if (distance < touchSlop) {
					return false;
				}
				if (currentStatus != STATUS_REFRESHING) {
					if (headerLayoutParams.topMargin > 0) {
						currentStatus = STATUS_RELEASE_TO_REFRESH;
					} else {
						currentStatus = STATUS_PULL_TO_REFRESH;
					}
					// 通过偏移下拉头的topMargin值,来实现下拉效果
					headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;
					header.setLayoutParams(headerLayoutParams);
				}
				break;
			case MotionEvent.ACTION_UP:
			default:
				if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
					// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
					new RefreshingTask().execute();
				} else if (currentStatus == STATUS_PULL_TO_REFRESH) {
					// 松手时如果是下拉状态,就去调用隐藏下拉头的任务
					new HideHeaderTask().execute();
				}
				break;
			}
			// 时刻记得更新下拉头中的信息
			if (currentStatus == STATUS_PULL_TO_REFRESH
					|| currentStatus == STATUS_RELEASE_TO_REFRESH) {
				updateHeaderView();
				// 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
				listView.setPressed(false);
				listView.setFocusable(false);
				listView.setFocusableInTouchMode(false);
				lastStatus = currentStatus;
				// 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
				return true;
			}
		}
		return false;
	}
 
	/**
	 * 给下拉刷新控件注册一个监听器。
	 * 
	 * @param listener
	 *            监听器的实现。
	 * @param id
	 *            为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。
	 */
	public void setOnRefreshListener(PullToRefreshListener listener, int id) {
		mListener = listener;
		mId = id;
	}
 
	/**
	 * 当所有的刷新逻辑完成后,记录调用一下,否则你的ListView将一直处于正在刷新状态。
	 */
	public void finishRefreshing() {
		currentStatus = STATUS_REFRESH_FINISHED;
		preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
		new HideHeaderTask().execute();
	}
 
	/**
	 * 根据当前ListView的滚动状态来设定 {@link #ableToPull}
	 * 的值,每次都需要在onTouch中第一个执行,这样可以判断出当前应该是滚动ListView,还是应该进行下拉。
	 * 
	 * @param event
	 */
	private void setIsAbleToPull(MotionEvent event) {
		View firstChild = listView.getChildAt(0);
		if (firstChild != null) {
			int firstVisiblePos = listView.getFirstVisiblePosition();
			if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
				if (!ableToPull) {
					yDown = event.getRawY();
				}
				// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
				ableToPull = true;
			} else {
				if (headerLayoutParams.topMargin != hideHeaderHeight) {
					headerLayoutParams.topMargin = hideHeaderHeight;
					header.setLayoutParams(headerLayoutParams);
				}
				ableToPull = false;
			}
		} else {
			// 如果ListView中没有元素,也应该允许下拉刷新
			ableToPull = true;
		}
	}
 
	/**
	 * 更新下拉头中的信息。
	 */
	private void updateHeaderView() {
		if (lastStatus != currentStatus) {
			if (currentStatus == STATUS_PULL_TO_REFRESH) {
				description.setText(getResources().getString(R.string.pull_to_refresh));
				arrow.setVisibility(View.VISIBLE);
				progressBar.setVisibility(View.GONE);
				rotateArrow();
			} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
				description.setText(getResources().getString(R.string.release_to_refresh));
				arrow.setVisibility(View.VISIBLE);
				progressBar.setVisibility(View.GONE);
				rotateArrow();
			} else if (currentStatus == STATUS_REFRESHING) {
				description.setText(getResources().getString(R.string.refreshing));
				progressBar.setVisibility(View.VISIBLE);
				arrow.clearAnimation();
				arrow.setVisibility(View.GONE);
			}
			refreshUpdatedAtValue();
		}
	}
 
	/**
	 * 根据当前的状态来旋转箭头。
	 */
	private void rotateArrow() {
		float pivotX = arrow.getWidth() / 2f;
		float pivotY = arrow.getHeight() / 2f;
		float fromDegrees = 0f;
		float toDegrees = 0f;
		if (currentStatus == STATUS_PULL_TO_REFRESH) {
			fromDegrees = 180f;
			toDegrees = 360f;
		} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
			fromDegrees = 0f;
			toDegrees = 180f;
		}
		RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
		animation.setDuration(100);
		animation.setFillAfter(true);
		arrow.startAnimation(animation);
	}
 
	/**
	 * 刷新下拉头中上次更新时间的文字描述。
	 */
	private void refreshUpdatedAtValue() {
		lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);
		long currentTime = System.currentTimeMillis();
		long timePassed = currentTime - lastUpdateTime;
		long timeIntoFormat;
		String updateAtValue;
		if (lastUpdateTime == -1) {
			updateAtValue = getResources().getString(R.string.not_updated_yet);
		} else if (timePassed < 0) {
			updateAtValue = getResources().getString(R.string.time_error);
		} else if (timePassed < ONE_MINUTE) {
			updateAtValue = getResources().getString(R.string.updated_just_now);
		} else if (timePassed < ONE_HOUR) {
			timeIntoFormat = timePassed / ONE_MINUTE;
			String value = timeIntoFormat + "分钟";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_DAY) {
			timeIntoFormat = timePassed / ONE_HOUR;
			String value = timeIntoFormat + "小时";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_MONTH) {
			timeIntoFormat = timePassed / ONE_DAY;
			String value = timeIntoFormat + "天";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else if (timePassed < ONE_YEAR) {
			timeIntoFormat = timePassed / ONE_MONTH;
			String value = timeIntoFormat + "个月";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		} else {
			timeIntoFormat = timePassed / ONE_YEAR;
			String value = timeIntoFormat + "年";
			updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
		}
		updateAt.setText(updateAtValue);
	}
 
	/**
	 * 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。
	 * 
	 * @author guolin
	 */
	class RefreshingTask extends AsyncTask<Void, Integer, Void> {
 
		@Override
		protected Void doInBackground(Void... params) {
			int topMargin = headerLayoutParams.topMargin;
			while (true) {
				topMargin = topMargin + SCROLL_SPEED;
				if (topMargin <= 0) {
					topMargin = 0;
					break;
				}
				publishProgress(topMargin);
				sleep(10);
			}
			currentStatus = STATUS_REFRESHING;
			publishProgress(0);
			if (mListener != null) {
				mListener.onRefresh();
			}
			return null;
		}
 
		@Override
		protected void onProgressUpdate(Integer... topMargin) {
			updateHeaderView();
			headerLayoutParams.topMargin = topMargin[0];
			header.setLayoutParams(headerLayoutParams);
		}
 
	}
 
	/**
	 * 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。
	 * 
	 * @author guolin
	 */
	class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {
 
		@Override
		protected Integer doInBackground(Void... params) {
			int topMargin = headerLayoutParams.topMargin;
			while (true) {
				topMargin = topMargin + SCROLL_SPEED;
				if (topMargin <= hideHeaderHeight) {
					topMargin = hideHeaderHeight;
					break;
				}
				publishProgress(topMargin);
				sleep(10);
			}
			return topMargin;
		}
 
		@Override
		protected void onProgressUpdate(Integer... topMargin) {
			headerLayoutParams.topMargin = topMargin[0];
			header.setLayoutParams(headerLayoutParams);
		}
 
		@Override
		protected void onPostExecute(Integer topMargin) {
			headerLayoutParams.topMargin = topMargin;
			header.setLayoutParams(headerLayoutParams);
			currentStatus = STATUS_REFRESH_FINISHED;
		}
	}
 
	/**
	 * 使当前线程睡眠指定的毫秒数。
	 * 
	 * @param time
	 *            指定当前线程睡眠多久,以毫秒为单位
	 */
	private void sleep(int time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
 
	/**
	 * 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。
	 * 
	 * @author guolin
	 */
	public interface PullToRefreshListener {
 
		/**
		 * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。
		 */
		void onRefresh();
 
	}
 
}
Copy the code

This class is the most important class in the whole drop-down refresh feature, and the comments are already written in some detail, so I’ll explain them briefly. First, we dynamically add the pull_to_refresh layout we just defined to the RefreshableView constructor as the pull-down head. Then we offset the pull-down head up off the screen in the onLayout method and register the Touch event with the ListView. The onTouch method is then executed every time your finger is swiped across the ListView. The first line of the onTouch method calls the setIsAbleToPull method to determine if the ListView has been scrolled to the top. Only when the ListView has been scrolled to the top will the following code be executed. Otherwise, the ListView is treated as a normal ListView scroll and nothing is done. When the ListView scrolls to the top, if the finger is still dragging down, it will change the offset value of the pull-down head, so that the pull-down head is displayed, and the pull-down distance is set to 1/2 of the distance that the finger is moving, so that the feeling of pulling will be felt. If the pull-down distance is large enough, the refresh operation is performed upon release, if the pull-down distance is not large enough, the pull-down head is simply re-hidden.

The refresh takes place in the RefreshingTask, where the onRefresh method of the PullToRefreshListener interface is called back in the doInBackground method, This is also an interface that you have to implement when you’re using RefreshableView, because the logic for refreshing should be written in the onRefresh method, which we’ll show you later.

In addition, the updateHeaderView method is called each time the dropdown is performed to change the data in the dropdown header, such as the rotation of the arrow direction and the change of the dropdown text description. For a deeper understanding, read the code in RefreshableView.

Now that we have all the features of the drop-down refresh, it’s time to take a look at how to introduce the drop-down refresh into your project. Open or create a new activity_main.xml layout for the application’s main screen and add the following code:

<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"
    tools:context=".MainActivity" >
 
    <com.example.pulltorefreshtest.RefreshableView
        android:id="@+id/refreshable_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
 
        <ListView
            android:id="@+id/list_view"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" >
        </ListView>
    </com.example.pulltorefreshtest.RefreshableView>
 
</RelativeLayout>
Copy the code

As you can see, we have added a ListView to our custom RefreshableView. This means that we have added a drop-down refresh function to the ListView. It’s as simple as that! Then look at the MainActivity of the program. Open or create a new MainActivity and add the following code:

public class MainActivity extends Activity { RefreshableView refreshableView; ListView listView; ArrayAdapter<String> adapter; String[] items = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); refreshableView = (RefreshableView) findViewById(R.id.refreshable_view); listView = (ListView) findViewById(R.id.list_view); adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items); listView.setAdapter(adapter); refreshableView.setOnRefreshListener(new PullToRefreshListener() { @Override public void onRefresh() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } refreshableView.finishRefreshing(); }}, 0); }}Copy the code

As you can see, we registered a listener by calling the setOnRefreshListener method of RefreshableView. When the ListView is refreshing, the listener’s onRefresh method is called back. The refresh logic is handled here. This method also automatically starts the thread and allows you to perform time-consuming operations directly in the onRefresh method, such as requesting the latest data from the server. In this case, I simply put the thread to sleep for three seconds. In addition, at the end of the onRefresh method, you must call the finishRefreshing method of RefreshableView, which is used to notify RefreshableView that the refresh is complete, Otherwise our ListView will always be refreshed.

I don’t know if you noticed, but the setOnRefreshListener method actually takes two parameters, and we just passed in an unremarkable 0. So what does this second argument do? Because RefreshableView is smart, it automatically records the time when the last refresh was completed, and when you pull it down, the drop down header shows how long it has been since the last refresh. This is a very useful feature that saves us from having to manually record and calculate the time ourselves, but there is a problem. If we currently have pull-down refresh in three places in our project, and now refresh in one place, the other two times will change as well! Because the time when the refresh is complete is recorded in the configuration file, the configuration file time that is read in the other two is already changed because the refresh changes the configuration file in one place. So what’s the solution? Pass a different ID to the second parameter of the setOnRefreshListener method for each place where a drop-down refresh is used. In this way, the last refresh completion time is recorded separately and there is no effect on each other.

Ok, so all the code is here, so let’s run it and see what happens.

It looks pretty good. To conclude, there are only three steps to introducing the ListView drop-down refresh feature into your project:

  1. Add your custom RefreshableView to the Activity layout file and include the ListView in it.

  2. Call RefreshableView’s setOnRefreshListener method in the Activity to register the callback interface.

  3. At the end of the onRefresh method, remember to call the RefreshableView finishRefreshing method to notify you that the refresh is complete.

From now on, anywhere in the project, a minute to introduce a pull-down refresh feature is fine.

Well, this is the end of today’s explanation, if you have questions, please leave a message below.

To download the source code, click here

Pay attention to my technical public number “Guo Lin”, high-quality technical articles are not regularly pushed.