• 最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9255575


    最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到。因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下。相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能。


    首先讲一下实现原理。这里我们将采取的方案是使用组合View的方式,先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。原理示意图如下:




    那我们现在就来动手实现一下,新建一个项目起名叫PullToRefreshTest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:

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

    在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。


    布局中所有引用的字符串我们都放在strings.xml中,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <string name="app_name">PullToRefreshTest</string>
    	<string name="pull_to_refresh">下拉可以刷新</string>
    	<string name="release_to_refresh">释放立即刷新</string>
    	<string name="refreshing">正在刷新…</string>
    	<string name="not_updated_yet">暂未更新过</string>
    	<string name="updated_at">上次更新于%1$s前</string>
    	<string name="updated_just_now">刚刚更新</string>
    	<string name="time_error">时间有问题</string>
        
    </resources>
    然后新建一个RefreshableView继承自LinearLayout,代码如下所示:
    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();
    
    	}
    
    }

    这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在RefreshableView的构造函数中动态添加了刚刚定义的pull_to_refresh这个布局作为下拉头,然后在onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了touch事件。之后每当手指在ListView上滑动时,onTouch方法就会执行。在onTouch方法中的第一行就调用了setIsAbleToPull方法来判断ListView是否滚动到了最顶部,只有滚动到了最顶部才会执行后面的代码,否则就视为正常的ListView滚动,不做任何处理。当ListView滚动到了最顶部时,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,下拉的距离设定为手指移动距离的1/2,这样才会有拉力的感觉。如果下拉的距离足够大,在松手的时候就会执行刷新操作,如果距离不够大,就仅仅重新隐藏下拉头。


    具体的刷新操作会在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh方法,这也是大家在使用RefreshableView时必须要去实现的一个接口,因为具体刷新的逻辑就应该写在onRefresh方法中,后面会演示使用的方法。


    另外每次在下拉的时候都还会调用updateHeaderView方法来改变下拉头中的数据,比如箭头方向的旋转,下拉文字描述的改变等。更加深入的理解请大家仔细去阅读RefreshableView中的代码。


    现在我们已经把下拉刷新的所有功能都完成了,接下来就要看一看如何在项目中引入下拉刷新了。打开或新建activity_main.xml作为程序主界面的布局,加入如下代码:
    <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>
    可以看到,我们在自定义的RefreshableView中加入了一个ListView,这就意味着给这个ListView加入了下拉刷新的功能,就是这么简单!
    然后我们再来看一下程序的主Activity,打开或新建MainActivity,加入如下代码:
    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);
    	}
    
    }

    可以看到,我们通过调用RefreshableView的setOnRefreshListener方法注册了一个监听器,当ListView正在刷新时就会回调监听器的onRefresh方法,刷新的具体逻辑就在这里处理。而且这个方法已经自动开启了线程,可以直接在onRefresh方法中进行耗时操作,比如向服务器请求最新数据等,在这里我就简单让线程睡眠3秒钟。另外在onRefresh方法的最后,一定要调用RefreshableView中的finishRefreshing方法,这个方法是用来通知RefreshableView刷新结束了,不然我们的ListView将一直处于正在刷新的状态。


    不知道大家有没有注意到,setOnRefreshListener这个方法其实是有两个参数的,我们刚刚也是传入了一个不起眼的0。那这第二个参数是用来做什么的呢?由于RefreshableView比较智能,它会自动帮我们记录上次刷新完成的时间,然后下拉的时候会在下拉头中显示距上次刷新已过了多久。这是一个非常好用的功能,让我们不用再自己手动去记录和计算时间了,但是却存在一个问题。如果当前我们的项目中有三个地方都使用到了下拉刷新的功能,现在在一处进行了刷新,其它两处的时间也都会跟着改变!因为刷新完成的时间是记录在配置文件中的,由于在一处刷新更改了配置文件,导致在其它两处读取到的配置文件时间已经是更改过的了。那解决方案是什么?就是每个用到下拉刷新的地方,给setOnRefreshListener方法的第二个参数中传入不同的id就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。


    好了,全部的代码都在这里了,让我们来运行一下,看看效果吧。



    效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入ListView下拉刷新功能只需三步:


    1. 在Activity的布局文件中加入自定义的RefreshableView,并让ListView包含在其中。

    2. 在Activity中调用RefreshableView的setOnRefreshListener方法注册回调接口。

    3. 在onRefresh方法的最后,记得调用RefreshableView的finishRefreshing方法,通知刷新结束。


    从此以后,在项目的任何地方,一分钟引入下拉刷新功能妥妥的。


    好了,今天的讲解到此结束,有疑问的朋友请在下面留言。


    源码下载,请点击这里


    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

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

    前段时间项目中用到了下拉刷新功能,之前在网上也找到过类似的demo,但这些demo的质量参差不齐,用户体验也不好,接口设计也不行。最张没办法,终于忍不了了,自己就写了一个下拉刷新的框架,这个框架是一个通用的框架,效果和设计感觉都还不错,现在分享给各位看官。

    致谢:

    1. 感谢lk6233160同学提出的问题,旋转View时调用setRotation方法只能是在API Level11(3.0)以上才能用,这个问题的解决办法是给ImageView设置一个Matrix,把Matrix上面作用一个旋转矩阵,但是如果不是ImageView的话,可能实现起来比较麻烦,再次谢谢lk6233160同学。

    2. 谢谢如毛毛风提出的问题,向下滑动后,再向上滑动到头,只能再松手后才能再次下拉。这个问题的回复请参考评论。

    技术交流群:

    QQ:197990971(人员已满)


    1. 关于下拉刷新

    下拉刷新这种用户交互最早由twitter创始人洛伦•布里切特(Loren Brichter)发明,有理论认为,下拉刷新是一种适用于按照从新到旧的时间顺序排列feeds的应用,在这种应用场景中看完旧的内容时,用户会很自然地下拉查找更新的内容,因此下拉刷新就显得非常合理。大家可以参考这篇文章:有趣的下拉刷新,下面我贴出一个有趣的下拉刷新的案例。

    图一、有趣的下拉刷新案例(一)


    图一、有趣的下拉刷新案例(二)


    2. 实现原理

    上面这些例子,外观做得再好看,他的本质上都一样,那就是一个下拉刷新控件通常由以下几部分组成:
    【1】Header
    Header通常有下拉箭头,文字,进度条等元素,根据下拉的距离来改变它的状态,从而显示不同的样式
    【2】Content
    这部分是内容区域,网上有很多例子都是直接在ListView里面添加Header,但这就有局限性,因为好多情况下并不一定是用ListView来显示数据。我们把要显示内容的View放置在我们的一个容器中,如果你想实现一个用ListView显示数据的下拉刷新,你需要创建一个ListView旋转到我的容器中。我们处理这个容器的事件(down, move, up),如果向下拉,则把整个布局向下滑动,从而把header显示出来。
    【3】Footer
    Footer可以用来显示向上拉的箭头,自动加载更多的进度条等。

    以上三部分总结的说来,就是如下图所示的这种布局结构:
    图三,下拉刷新的布局结构

    关于上图,需要说明几点:
    1、这个布局扩展于LinearLayout,垂直排列
    2、从上到下的顺序是:Header, Content, Footer
    3、Content填充满父控件,通过设置top, bottom的padding来使Header和Footer不可见,也就是让它超出屏幕外
    4、下拉时,调用scrollTo方法来将整个布局向下滑动,从而把Header显示出来,上拉正好与下拉相反。
    5、派生类需要实现的是:将Content View填充到父容器中,比如,如果你要使用的话,那么你需要把ListView, ScrollView, WebView等添加到容器中。
    6、上图中的红色区域就是屏的大小(严格来说,这里说屏幕大小并不准确,应该说成内容区域更加准确)

    3. 具体实现

    明白了实现原理与过程,我们尝试来具体实现,首先,为了以后更好地扩展,设计更加合理,我们把下拉刷新的功能抽象成一个接口:

    1、IPullToRefresh<T extends View>

    它具体的定义方法如下:
    1. public interface IPullToRefresh<T extends View> {
    2.     public void setPullRefreshEnabled(boolean pullRefreshEnabled);
    3.     public void setPullLoadEnabled(boolean pullLoadEnabled);
    4.     public void setScrollLoadEnabled(boolean scrollLoadEnabled);
    5.     public boolean isPullRefreshEnabled();
    6.     public boolean isPullLoadEnabled();
    7.     public boolean isScrollLoadEnabled();
    8.     public void setOnRefreshListener(OnRefreshListener<T> refreshListener);
    9.     public void onPullDownRefreshComplete();
    10.     public void onPullUpRefreshComplete();
    11.     public T getRefreshableView();
    12.     public LoadingLayout getHeaderLoadingLayout();
    13.     public LoadingLayout getFooterLoadingLayout();
    14.     public void setLastUpdatedLabel(CharSequence label);
    15. }
    这个接口是一个泛型的,它接受View的派生类,因为要放到我们的容器中的不就是一个View吗?

    2、PullToRefreshBase<T extends View>
    这个类实现了IPullToRefresh接口,它是从LinearLayout继承过来,作为下拉刷新的一个抽象基类,如果你想实现ListView的下拉刷新,只需要扩展这个类,实现一些必要的方法就可以了。这个类的职责主要有以下几点:
    • 处理onInterceptTouchEvent()和onTouchEvent()中的事件当内容的View(比如ListView)正如处于最顶部,此时再向下拉,我们必须截断事件,然后move事件就会把后续的事件传递到onTouchEvent()方法中,然后再在这个方法中,我们根据move的距离再进行scroll整个View。
    • 负责创建Header、Footer和Content View在构造方法中调用方法去创建这三个部分的View,派生类可以重写这些方法,以提供不同式样的Header和Footer,它会调用createHeaderLoadingLayout和createFooterLoadingLayout方法来创建Header和Footer创建Content View的方法是一个抽象方法,必须让派生类来实现,返回一个非null的View,然后容器再把这个View添加到自己里面。
    • 设置各种状态:这里面有很多状态,如下拉、上拉、刷新、加载中、释放等,它会根据用户拉动的距离来更改状态,状态的改变,它也会把Header和Footer的状态改变,然后Header和Footer会根据状态去显示相应的界面式样。
    3、PullToRefreshBase<T extends View>继承关系
    这里我实现了三个下拉刷新的派生类,分别是ListView、ScrollView、WebView三个,它们的继承关系如下:

    图四、PullToRefreshBase类的继承关系

    关于PullToRefreshBase类及其派和类,有几点需要说明:
    • 对于ListView,ScrollView,WebView这三种情况,他们是否滑动到最顶部或是最底部的实现是不一样的,所以,在PullToRefreshBase类中需要调用两个抽象方法来判断当前的位置是否在顶部或底部,而其派生类必须要实现这两个方法。比如对于ListView,它滑动到最顶部的条件就是第一个child完全可见并且first postion是0。这两个抽象方法是:
    1. /**
    2. * 判断刷新的View是否滑动到顶部
    3. *
    4. * @return true表示已经滑动到顶部,否则false
    5. */
    6. protected abstract boolean isReadyForPullDown();
    7. /**
    8. * 判断刷新的View是否滑动到底
    9. *
    10. * @return true表示已经滑动到底部,否则false
    11. */
    12. protected abstract boolean isReadyForPullUp();
    • 创建可下拉刷新的View(也就是content view)的抽象方法是
    1. /**
    2. * 创建可以刷新的View
    3. *
    4. * @param context context
    5. * @param attrs 属性
    6. * @return View
    7. */
    8. protected abstract T createRefreshableView(Context context, AttributeSet attrs);
    4、LoadingLayout
    LoadingLayout是刷新Layout的一个抽象,它是一个抽象基类。Header和Footer都扩展于这个类。这类抽象类,提供了两个抽象方法:
    • getContentSize
    这个方法返回当前这个刷新Layout的大小,通常返回的是布局的高度,为了以后可以扩展为水平拉动,所以方法名字没有取成getLayoutHeight()之类的,这个返回值,将会作为松手后是否可以刷新的临界值,如果下拉的偏移值大于这个值,就认为可以刷新,否则不刷新,这个方法必须由派生类来实现。
    • setState
    这个方法用来设置当前刷新Layout的状态,PullToRefreshBase类会调用这个方法,当进入下拉,松手等动作时,都会调用这个方法,派生类里面只需要根据这些状态实现不同的界面显示,如下拉状态时,就显示出箭头,刷新状态时,就显示loading的图标。
    可能的状态值有:RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA

    LoadingLayout及其派生类的继承关系如下图所示:

    图五、LoadingLayout及其派生类的类图

    我们可以随意地制定自己的Header和Footer,我们也可以实现如图一和图二中显示的各种下拉刷新案例中的Header和Footer,只要重写上述两个方法getContentSize()和setState()就行了。HeaderLoadingLayout,它默认是显示箭头式样的布局,而RotateLoadingLayout则是显示一个旋转图标的式样。

    5、事件处理
    我们必须重写PullToRefreshBase类的两个事件相关的方法onInterceptTouchEvent()和onTouchEvent()方法。由于ListView,ScrollView,WebView它们是放到PullToRefreshBase内部的,所在事件先是传递到PullToRefreshBase#onInterceptTouchEvent()方法中,所以我们应该在这个方法中去处理ACTION_MOVE事件,判断如果当前ListView,ScrollView,WebView是否在最顶部或最底部,如果是,则开始截断事件,一旦事件被截断,后续的事件就会传递到PullToRefreshBase#onInterceptTouchEvent()方法中,我们再在ACTION_MOVE事件中去移动整个布局,从而实现下拉或上拉动作。

    6、滚动布局(scrollTo)
    如图三的布局结构可知,默认情况下Header和Footer是放置在Content View的最上面和最下面,通过设置padding来让他跑到屏幕外面去了,如果我们将整个布局向下滚动(scrollTo)一定距离,那么Header就会被显示出来,基于这种情况,所以在我的实现中,最终我是调用scrollTo来实现下拉动作的。

    总的说来,实现的重要的点就这些,具体的一些细节在实现在会碰到很多,可以参考代码。

    4. 如何使用

    使用下拉刷新的代码如下
    1. @Override
    2. public void onCreate(Bundle savedInstanceState) {
    3. super.onCreate(savedInstanceState);
    4. mPullListView = new PullToRefreshListView(this);
    5. setContentView(mPullListView);
    6. // 上拉加载不可用
    7. mPullListView.setPullLoadEnabled(false);
    8. // 滚动到底自动加载可用
    9. mPullListView.setScrollLoadEnabled(true);
    10. mCurIndex = mLoadDataCount;
    11. mListItems = new LinkedList<String>();
    12. mListItems.addAll(Arrays.asList(mStrings).subList(0, mCurIndex));
    13. mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);
    14. // 得到实际的ListView
    15. mListView = mPullListView.getRefreshableView();
    16. // 绑定数据
    17. mListView.setAdapter(mAdapter);
    18. // 设置下拉刷新的listener
    19. mPullListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
    20. @Override
    21. public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) {
    22. mIsStart = true;
    23. new GetDataTask().execute();
    24. }
    25. @Override
    26. public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) {
    27. mIsStart = false;
    28. new GetDataTask().execute();
    29. }
    30. });
    31. setLastUpdateTime();
    32. // 自动刷新
    33. mPullListView.doPullRefreshing(true, 500);
    34. }
    这是初始化一个下拉刷新的布局,并且调用setContentView来设置到Activity中。
    在下拉刷新完成后,我们可以调用onPullDownRefreshComplete()和onPullUpRefreshComplete()方法来停止刷新和加载

    5. 运行效果

    这里列出了demo的运行效果图。

    图六、ListView下拉刷新,注意Header和Footer的样式


    图七、WebView和ScrollView的下拉刷新效果图


    6. 源码下载

    实现这个下拉刷新的框架,并不是我的原创,我也是参考了很多开源的,把我认为比较好的东西借鉴过来,从而形成我的东西,我主要是参考了下面这个demo:
    https://github.com/chrisbanes/Android-PullToRefresh 这个demo写得不错,不过他这个太复杂了,我们都知道,一旦复杂了,万一我们要添加一些需要,自然也要费劲一些,我其实就是把他的简化再简化,以满足我们自己的需要。


    转载请说明出处
    谢谢!!!


    7. Bug修复


    已知bug修复情况如下,发现了代码bug的看官也可以给我反馈,谢谢~~~

    1,对于ListView的下拉刷新,当启用滚动到底自动加载时,如果footer由隐藏变为显示时,出现显示异常的情况
    这个问题已经修复了,修正的代码如下:
    • PullToRefreshListView#setScrollLoadEnabled方法,修正后的代码如下:
    1. @Override
    2. public void setScrollLoadEnabled(boolean scrollLoadEnabled) {
    3. if (isScrollLoadEnabled() == scrollLoadEnabled) {
    4. return;
    5. }
    6. super.setScrollLoadEnabled(scrollLoadEnabled);
    7. if (scrollLoadEnabled) {
    8. // 设置Footer
    9. if (null == mLoadMoreFooterLayout) {
    10. mLoadMoreFooterLayout = new FooterLoadingLayout(getContext());
    11. mListView.addFooterView(mLoadMoreFooterLayout, null, false);
    12. }
    13. mLoadMoreFooterLayout.show(true);
    14. } else {
    15. if (null != mLoadMoreFooterLayout) {
    16. mLoadMoreFooterLayout.show(false);
    17. }
    18. }
    19. }
    • LoadingLayout#show方法,修正后的代码如下:
    1. /**
    2. * 显示或隐藏这个布局
    3. *
    4. * @param show flag
    5. */
    6. public void show(boolean show) {
    7. // If is showing, do nothing.
    8. if (show == (View.VISIBLE == getVisibility())) {
    9. return;
    10. }
    11. ViewGroup.LayoutParams params = mContainer.getLayoutParams();
    12. if (null != params) {
    13. if (show) {
    14. params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
    15. } else {
    16. params.height = 0;
    17. }
    18. requestLayout();
    19. setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    20. }
    21. }
    在更改LayoutParameter后,调用requestLayout()方法。
    • 图片旋转兼容2.x系统
    我之前想的是这个只需要兼容3.x以上的系统,但发现有很多网友在使用过程中遇到过兼容性问题,这次抽空将这个兼容性一并实现了。
           onPull的修改如下:
    1. @Override
    2. public void onPull(float scale) {
    3. if (null == mRotationHelper) {
    4. mRotationHelper = new ImageViewRotationHelper(mArrowImageView);
    5. }
    6. float angle = scale * 180f; // SUPPRESS CHECKSTYLE
    7. mRotationHelper.setRotation(angle);
    8. }

    ImageViewRotationHelper主要的作用就是实现了ImageView的旋转功能,内部作了版本的区分,实现代码如下:

    1. /**
    2. * The image view rotation helper
    3. *
    4. * @author lihong06
    5. * @since 2014-5-2
    6. */
    7. static class ImageViewRotationHelper {
    8. /** The imageview */
    9. private final ImageView mImageView;
    10. /** The matrix */
    11. private Matrix mMatrix;
    12. /** Pivot X */
    13. private float mRotationPivotX;
    14. /** Pivot Y */
    15. private float mRotationPivotY;
    16. /**
    17. * The constructor method.
    18. *
    19. * @param imageView the image view
    20. */
    21. public ImageViewRotationHelper(ImageView imageView) {
    22. mImageView = imageView;
    23. }
    24. /**
    25. * Sets the degrees that the view is rotated around the pivot point. Increasing values
    26. * result in clockwise rotation.
    27. *
    28. * @param rotation The degrees of rotation.
    29. *
    30. * @see #getRotation()
    31. * @see #getPivotX()
    32. * @see #getPivotY()
    33. * @see #setRotationX(float)
    34. * @see #setRotationY(float)
    35. *
    36. * @attr ref android.R.styleable#View_rotation
    37. */
    38. public void setRotation(float rotation) {
    39. if (APIUtils.hasHoneycomb()) {
    40. mImageView.setRotation(rotation);
    41. } else {
    42. if (null == mMatrix) {
    43. mMatrix = new Matrix();
    44. // 计算旋转的中心点
    45. Drawable imageDrawable = mImageView.getDrawable();
    46. if (null != imageDrawable) {
    47. mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f);
    48. mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f);
    49. }
    50. }
    51. mMatrix.setRotate(rotation, mRotationPivotX, mRotationPivotY);
    52. mImageView.setImageMatrix(mMatrix);
    53. }
    54. }
    55. }

    最核心的就是,如果在2.x的版本上,旋转ImageView使用Matrix。

    • PullToRefreshBase构造方法兼容2.x
    在三个参数的构造方法声明如下标注:
        @SuppressLint(“NewApi”)
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)



    大家如果还有什么问题,欢迎留言~~~


            </div>
                </div>
    
    展开全文
  • (1)在我刚学android的时候,用的是XListView,在github上搜索有 MarkMjw/PullToRefresh ,根据Maxwin的XListView改造而来,完善下拉刷新上拉加载更多的功能并实现自动刷新以及自动加载等功能, 并增加对ScrollView.....

    BeautifulRefreshLayout


    众多优秀的下拉刷新(除了我写的之外T_T)


    说起下拉刷新,好像经历一段历史的洗礼。。。

    (1)在我刚学android的时候,用的是XListView,在github上搜索有 MarkMjw/PullToRefresh ,根据Maxwin的XListView改造而来,完善下拉刷新上拉加载更多的功能并实现自动刷新以及自动加载等功能, 并增加对ScrollView的支持。 原XListView

    参考链接: 

    https://github.com/Maxwin-z/XListView-Android(听说原作者停止维护了)




    (2)然后又学了 chrisbanes/Android-PullToRefresh 的那个库,这个库牛逼到要死,支持ListView、ExpandableListView、GridView、WebView、ScrollView、HorizontalScrollView、ViewPager、ListFragment、、、


    自己也侮辱了这个库,改的乱七八糟

    https://github.com/androidcjj/ComicReader/tree/master/YinHunPulltoRefreshLibrary ,增加了支持瀑布流刷新的功能和下拉动画效果的。。。。

    (3)那时候看了知乎的客户端,下拉刷新很炫,查了下是用什么实现的,最终知道是用 chrisbanes/ActionBar-PullToRefresh 的库可以实现那种效果,又去学了,啊哈哈,然而过些日子也没见人用了,啊哈哈哈

    (4)这时候google也有自己的下拉控件SwipeRefreshLayout,刚出来的效果,一条加载直线,个人觉得,一般到要死。 stormzhang/SwipeRefreshLayoutDemo 写了demo。


    android 5.0之后效果是个加载圆圈,还可以接受了,现在很多应用都用这个

    (5)这时又看到了 baoyongzhang/android-PullRefreshLayout ,This component like SwipeRefreshLayout, it is more beautiful than SwipeRefreshLayout.就是比google的跟漂亮。呵呵!

    (6)同时,这里也要提下 liaohuqiu/android-Ultra-Pull-To-Refresh ,已经强大到什么控件都能适用刷新了,相信你也听过了

    (7)看过最有创意的下拉刷新FlyRefresh,一只飞机飞啊飞,然而并没有什么卵用…..


    (8)material设计已经深入到开发者的心里,然后 我看到了A pull-down-to-refresh layout inspired by Lollipop overscrolled effects allan1st/JellyRefreshLayout ,我这个也是看人家的代码实现的,十分感谢,啊哈哈


    (9)最近看的下拉刷新也是挺奇怪的,就比如 recruit-lifestyle/BeerSwipeRefresh 啤酒下拉刷新和 recruit-lifestyle/WaveSwipeRefreshLayout 水滴下拉刷新…



    (10)这个是网友推荐的 SuperSwipeRefreshLayout ,顾名思义,是扩展自SwipeRefreshLayout。

    (11)这个是Yalantis公司开发的下拉刷新,其中它的很多东西动画这块,做的非常漂亮Yalantis/Phoenix

    Taurus又是飞机飞啊飞…

    (12) tuesda/CircleRefreshLayout 的下拉刷新,动画做的很不错,设计图来源 

    https://dribbble.com/shots/1797373-Pull-Down-To-Refresh


    (13) BeautifulRefreshLayoutForFood

    看到一个很漂亮的美食下拉刷新 (来源地址) ,可惜技术水平菜菜的,只能模仿一下下,啊哈哈。。。源码下载地址download

    [图片上传失败...(image-ea20fa-1525245291742)]

    (14) BeautifulRefreshLayoutForNaruto

    闲着蛋疼,写了个血轮眼下拉刷新的,虽然火影漫画完结了,但是动画还在继续,真的是挂漫天飞。。。


    (15) bingoogolapple/BGARefreshLayout-Android 多种下拉刷新效果、上拉加载更多、可配置自定义头部广告位…

    (16) BeautifulRefreshLayoutForGirl

    这是一个小清新的下拉刷新,纯代码绘制,无需任何图片,Recyclerview中item展示的妹子图来自 http://gank.io/

    设计图来源,当然我实现的效果做了一些修改,希望你喜欢,呵呵。。。源码下载地址download pull to refresh by Michael Lanning

    截图

    (17) BeautifulRefreshLayoutForRain

    这是一个下雨刷新,你没听错,确实一刷新就下雨,为什么会出现,只是我的好奇心而做的东西,见怪不怪了,呵呵。。。


    (18) Android-MaterialRefreshLayout 是我最近写的, it is more beautiful and powerful than SwipeRefreshLayout , 下拉刷新拥有侵入式,非侵入式,覆盖式,非覆盖式,自动刷新,上拉加载更多,自动加载等功能……



    (19) WaveRefreshForAndroid 这个是基于 Android-PullToRefresh 修改的而成的水波纹下拉刷新…可能作者主攻ios,所以ios的效果看起来好看点WaveRefresh…


    (20)WaterDropListView如果你用过ios的qq,那应该很熟悉这种刷新了,效果还是蛮不错的,就是不知道为什么android版本的qq不用,呵呵。。。


    (21) SwipeRefreshRecyclerView 一个展示 RecyclerView 下拉刷新和上拉加载更多的 Demo 完全使用原生控件实现非侵入式的下拉刷新和加载更多。 同时也是 Ailurus 练习 RxJava 的小例子。 使用HeaderViewRecyclerAdapter完成了加载更多的进度展示

    (22)阿拉灯神灯写的两个下拉库 AutoHomeRefreshListView 仿汽车之家下拉刷新和 MeiTuanRefreshListView 仿美团下拉刷新。。。

    (23)XRecyclerViewa RecyclerView that implements pullrefresh and loadingmore featrues.you can use it like a standard RecyclerView

    (24) ChromeLikeSwipeLayout 这个效果蛮赞的,细节做的很好。。。。Pull down, and execute more action!


    (25)下拉玩消格子游戏HitBlockRefresh, 一边下拉刷新,一边打砖块.

    温馨提示:如需RecyclerView加载更多,请参考: 

    https://github.com/android-cjj/Android-RecyclerViewWithFooter

    如果有好的下拉刷新的可以和我一块交流

    原文链接
    展开全文
  • 此为示例代码,详解讲解请参考 http://blog.csdn.net/sinyu890807/article/details/9255575
  • android下拉刷新android-Ultra-Pull-To-Refresh第三方库功能很强大,基本能满足下拉刷新的要求,如果你需要加在更多,可以看一下cube-sdk第三方库,他们的关系式android-Ultra-Pull-To-Refresh是从cube-sdk中独立...

    android下拉刷新android-Ultra-Pull-To-Refresh第三方库功能很强大,基本能满足下拉刷新的要求,如果你需要加在更多,可以看一下cube-sdk第三方库,他们的关系式android-Ultra-Pull-To-Refresh是从cube-sdk中独立出来的。

    直接讲怎么用,不废话。

    1.下载代码:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh 

    里面有两个文件夹:ptr-demo和ptr-lib

    其中ptr-demo是例子,你可以参考里面的代码,开发者没有给出例子代码讲解,建议开发者可以给几个。

    ptr-lib是个库函数,里面有自定义的组件可以直接拿过来用,比如在布局中的 <in.srain.cube.views.ptr.PtrFrameLayout>就是在这里:

    在你的项目中直接引用,不知道怎么又能用的可以百度一下eclipse怎么引用外部项目库。

    2、就是ptr-demo导入eclipse中时有错误,缺少资源库,官网给说了什么maven但我没用过,可以用cube-sdk的core库,就是下载cube-sdk导入eclipse中。导入core库,有两个错误,一个少了逗号直接解决 ,一个就是官网也说了具体看这里:http://www.liaohuqiu.net/cn/posts/compile-ultra-ptr-in-eclipse/

    • getAllocationByteCount()

      需要版本SDK >= 19



    就是在eclipse中设置大于19.

    这个cube-sdk只是用来跑例子代码的测试的,主要是应用ptr-lib这个库。

    3、先跑个代码:建个hello工程:

    布局代码:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f1f1f1">
    
        <in.srain.cube.views.ptr.PtrClassicFrameLayout
            android:id="@+id/ptr"
            xmlns:cube_ptr="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            cube_ptr:ptr_duration_to_close="200"
            cube_ptr:ptr_duration_to_close_header="1000"
            cube_ptr:ptr_keep_header_when_refresh="true"
            cube_ptr:ptr_pull_to_fresh="false"
            cube_ptr:ptr_ratio_of_header_height_to_refresh="1.2"
            cube_ptr:ptr_resistance="1.7">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:clickable="true"
                android:gravity="center"
                android:text="I am a TextView"
                android:textSize="30sp" />
    
        </in.srain.cube.views.ptr.PtrClassicFrameLayout>
    </RelativeLayout>
    java代码中:

    @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ptr = (PtrClassicFrameLayout) findViewById(R.id.ptr);
            ptr.setPtrHandler(new PtrHandler() {
                @Override
                public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
                    return true;
                }
                @Override
                public void onRefreshBegin(final PtrFrameLayout frame) {
                    frame.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            frame.refreshComplete();
                        }
                    }, 2000);
                }
            });
        }



    注意到 PtrClassicFrameLayout是经典头部,不用自己添加头部,最常用的。只要添加内容就行了。就是textview.

    如果要完成头部多样化,就要用到 PtrFrameLayout( PtrClassicFrameLayout继承前者)添加头部 ptr.setHeaderView(header);

    代码如下:

    <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" >
    
        <in.srain.cube.views.ptr.PtrFrameLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:cube_ptr="http://schemas.android.com/apk/res-auto"
            android:id="@+id/ptr"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            cube_ptr:ptr_duration_to_close="200"
            cube_ptr:ptr_duration_to_close_header="1000"
            cube_ptr:ptr_keep_header_when_refresh="true"
            cube_ptr:ptr_pull_to_fresh="true"
            cube_ptr:ptr_ratio_of_header_height_to_refresh="1.2"
            cube_ptr:ptr_resistance="1.7" >
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="@string/hello_world" />
        </in.srain.cube.views.ptr.PtrFrameLayout>
    
    </RelativeLayout>

    java代码:

    @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            final StoreHouseHeader header = new StoreHouseHeader(this);
            header.setPadding(0,  LocalDisplay.dp2px(15), 0, 0);
            header.initWithString("hello");
            ptr = (PtrFrameLayout) findViewById(R.id.ptr);
            ptr.setHeaderView(header);
            ptr.addPtrUIHandler(header);
            ptr.setPtrHandler(new PtrHandler() {
                @Override
                public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
                    return true;
                }
                @Override
                public void onRefreshBegin(final PtrFrameLayout frame) {
                    frame.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            frame.refreshComplete();
                        }
                    }, 2000);
                }
            });
        }

    效果图:


    注意ptr-lib中默认的字体颜色是白色,你可以修改成其他颜色,就是在in.srain.cube.views.ptr.header中修该mtextcolor。

    其中你可能需要看一下源代码解析,这边文章很好,http://www.open-open.com/lib/view/open1436405920005.html。

    看了这以后就不那么糊涂了。上面代码中会缺少LocalDisplay.dp2px(15),这是core库中的,你可以从core中拷贝一下 LocalDisplay.java 到自己的项目中,这样就不用引入core库了。

    展开全文
  • 关于下拉刷新相信其实大家已经接触过也使用过不少了,网上有很多的第三方下拉刷新框架而且Android也有一个原生的下拉刷新,比如博主本人在开发过程中,以上两种我都有使用过,感受各不相同,网上第三方的下拉刷新...

    正如标题所示,今天主要带大家如何写一款自定义的下拉刷新控件,顺带讲解下拉刷新中最主要的头等大事:如何解决滑动冲突问题。好了,下面就让我们开始吧!

    关于下拉刷新相信其实大家已经接触过也使用过不少了,网上有很多的第三方下拉刷新框架而且Android也有一个原生的下拉刷新,比如博主本人在开发过程中,以上两种我都有使用过,感受各不相同,网上第三方的下拉刷新框架功能强大,特效炫酷,缺点是占用空间也很大,代码庞杂,Android原生的下拉刷新控件占用内存小,但是特效朴素。两者各有自己的不足和优势。

    后来本人就开始自己动手尝试开发下拉刷新,自己的开发的东西好处就是自己可以随意定制,而且可以随意的做出修改。这篇博文主要是给和我一样想法和认同的小伙伴准备的,如果你感觉网上的第三方下拉刷新框架,或者Android原生的下拉刷新控件可以满足你,那么你已经没有必要看这篇博文。

    好了,下面我们就开始来做下拉刷新把~

    首先老规矩,我们先分析实现下拉刷新的思路:

    所谓的下拉刷新就是手指在页面向下滑动会逐渐的拉出顶部的隐藏布局,那么该怎么让隐藏在顶部的布局随着手指下滑逐渐拉出呢?这里给大家一个参考,例如QQ的下拉刷新,我们在这里要实现的就是QQ的这种效果。

    首先很快就能想到,既然要求随着手指滑动逐渐拉出,那么一定要做的就是监听手势事件!当我们监听手指向下滑动的时候,我们就在MOVE事件中不断的动态改变UI布局,使下滑时和UI改变同步。这里确定了需要监听的事件,下面我们来分析一下布局UI上的代码:首先我们确定顶部的下拉刷新控件在一开始应该是隐藏起来的,只有触发了时间它才会出现。它出现的方式是从顶部开始,随之手势的滑动逐渐向下扩展,那么也就是说它的高度起码是逐渐变化的,当然这里我们不会考虑宽度的事情。那么这里我们就可以基本的确定它的父布局是一个LinearLayout,同时父布局的LinearLayout设置的是竖向排列,因为当它的高度逐渐变大的时候,它下面的控件,或者说子布局便会被顶下去,给人在视觉上的效果就是它被拉了出来!

    分析到这里,我们基本可以确定,看似神秘的高大上下拉刷新,其实它的构成主要有下面三个部分:

    1.监听手势事件;

    2.父布局是Linear Layout,排列方式为竖向;

    3.大多数情况下需要处理一下滑动冲突问题。

     我们先来说第三个部分:处理滑动冲突问题!

    为什么要先讲滑动冲突问题?原因很简单,因为它是整个下拉刷新组成中最重要的一块了。首先我们要先明白什么是滑动冲突?

    滑动冲突从字面上就可以看出来,指滑动事件相互冲突,主要是在一个布局中,如果存在两个或者多个的控件需要去进行滑动操作,监听他们的滑动事件,那么这里就会产生一个滑动冲突。具体表现为无法滑动或者滑动卡顿。当然不单单只有滑动才会导致滑动冲突,点击也是同样的。比如你在一个滚动控件的子item的布局内你又加了一个点击事件,那么你就会发现这个item的点击事件无法被监听到。为什么?很简单,我们都知道,点击事件也是一种滑动监听事件,因为事件被它的父控件滚动控件给拦截了,自然无法到达里面子item,所以item的点击事件无效。这也是一种滑动冲突。

    一般用到下拉刷新的地方都是加载大量的数据需要滚动去展示,可能是ScrollView,或者是RecyclerView,他们都是可以上下滑动的。当下拉需要刷新的时候需要监听滚动控件的滑动事件,因为我们要确定只有滚动控件滑动到顶部的时候,继续向下滑动我们才能把顶部布局拉出来,这里需要监听第二个滑动事件,用来处理顶部布局的高度。所以在这里已经出现了滑动冲突,因为存在两个滑动事件,一个是布局的滑动监听,一个就是布局内部滚动控件的滑动事件监听。

    下面我们就开始去如何解决滑动冲突问题:

    关于如何解决滑动冲突问题,我们首先就要去了解Android滑动事件的分发机制。

    具体的源码分析我相信网上有好多,这里我不会去给你分析源码,毕竟滑动事件的分发机制源码比较复杂,分析起来会比较麻烦,有兴趣的小伙伴可以自行搜索去查看源码分析。这里我只是给大家大致的说一下分发机制的执行策略,以及我们该怎么写代码来保证事件被正确的分发下去。

    这里给大家准备一份伪代码:

    public boolean dispatchTouchEvent(MotionEvent ev){
         boolean result=false;
         if(onInterceptTouchEvent(ev)){
              result=onTouchEvent();
         }else{
              result=child.dispatchTouchEvent(ev);
         }
    
         return result;
    }

    上面的伪代码很好的展示了滑动事件的分发机制。

    首先事件是从上向下传递的,顶层的布局首先拿到这个事件,事件进入顶层布局的dispatchTouchEvent()方法中,然后调用了onInterceptTouchEvent()方法来判断这个事件需不需要拦截下来,需要拦截下来就返回true,不要拦截就返回false,如果需要拦截,那么就会调用onTouchEvent()方法来消耗这个事件,也就是说这个事件机会得到执行,如果不拦截的话,那么就会把事件交给它的子布局,通过调用子布局的dispatchTouchEvent()方法,把这个事件继续传递下去,直到找到它真正的消耗者。

    滑动冲突的原因就很明显了,因为一次产生的事件只有一个,但是有两个控件的onInterceptTouchEvent()方法都表示需要拦截下来,那么自然就会导致一方消耗了事件而另外一方没有事件可以消耗,自然就会产生了冲突。

    解决的思路也很简单,那就是让他们两个和平相处,什么时候谁需要事件就把事件给谁,谁不需要事件就不给它。

    具体在程序中该怎么做这个事件合理分发,我们结合代码细致的讲一下:

    首先我们需要自定义一个布局,在这里面进行事件的分发,代码为:

    
    /**
     * Created by 王将 on 2018/7/26.
     */
    
    public class MyLinearLayout extends LinearLayout {
    
        int downY = 0;
        int scrollY=0;
    
        public MyLinearLayout(Context context) {
            super(context);
    
        }
    
        public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
        @Override
        public void setScrollY(int scrollY) {
            this.scrollY = scrollY;
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept=false;
    
    
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    downY=(int)ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (scrollY==0){
                        if (ev.getY()>downY){
    
                            intercept=true;
                        }else {
    
                            intercept=false;
                        }
                    }
                    break;
            }
    
            return intercept;
        }
    
        public int getDownY(){
            return downY;
        }
    }
    

    这里我们自定义一个类MyLinearLayout,它继承了LinearLayout ,主要的事件分发在它的onInterceptTouchEvent()方法中:

    @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept=false;
    
    
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    downY=(int)ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (scrollY==0){
                        if (ev.getY()>downY){
    
                            intercept=true;
                        }else {
    
                            intercept=false;
                        }
                    }
                    break;
            }
    
            return intercept;
        }

     我们可以看到,首先我们定义了一个布尔型的变量intercept,初始值为false,代表的是linearLayout是否需要拦截下事件。

    然后进入状态判断,首先我们记录下手指按下的Y坐标,然后进入MOVE状态,我们首先判断scrollY是否为0,这里的scrollY指的是滚动控件滚动的高度,如果滚动高度为0,那么我们就确定此时已经滚动到顶部,然后进入接下来的判断,判断手指是否向下滑动,也就是下拉动作。如果出现了下拉动作,那么也就是说他想拉出顶部的刷新布局,那么我们就决定拦截下来,把intercept设置为true,由我们的Layout消耗滑动事件;如果是想上滑动,也就是上拉操作,那么表示他想上拉滚动控件以便查看下面的信息,那么我们就不拦截,把intercept设置为false,由滚动控件去消耗滑动事件。最后返回我们是否要拦截的决定。

    就这样我们解决了滑动冲突问题。

    下面我们去看一下具体的布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.slidingconflicttest.MyLinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:id="@+id/parent_id">
    
        <View
            android:id="@+id/view_id"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:background="#9f5353"/>
    
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/scroll_id">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#d4c981"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#a9d481"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#51976d"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ab755b"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#2dc496"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#f2d517"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#8192d4"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#47acd1"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ab81d4"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#d553e1"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#ed5d92"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#312d0f"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#27de13"/>
                <View
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:background="#d4c981"/>
            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>
    
    </com.example.slidingconflicttest.MyLinearLayout>
    

    在这里,我为了方便简单,所以顶部的刷新布局我用了一个View来代替,滚动控件我使用的是NestedScrollView,便于监听滚动位置。在实际的开发中,大家需要把这个View换成自己的刷新布局就好了。

    在布局文件代码中,我们可以看出MyLinearLayout是顶层的父布局,NestedScrollView是MyLinearLayout布局中的子空间,在我刚才讲解的滑动冲突处理中,处理的就是他们两个的滑动冲突。因为MyLinearLayout是顶层的父布局,所以滑动事件会首先传递给它,理所当然我们需要在MyLinearLayout的onInterceptTouchEvent()方法中做出具体的事件分发操作。

    下面我们开始写主活动中具体的逻辑操作:

    public class MainActivity extends AppCompatActivity implements NestedScrollView.OnScrollChangeListener, View.OnTouchListener {
    
        NestedScrollView nestedScrollView;
        View view;
        MyLinearLayout linearLayout;
    
        int moveY,height=0,nowHeight=0;
    
        LinearLayout.LayoutParams layoutParams;
    
        boolean isBacking=false;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            nestedScrollView=(NestedScrollView) findViewById(R.id.scroll_id);
            view=(View) findViewById(R.id.view_id);
    
            linearLayout=new MyLinearLayout(this);
            linearLayout=(MyLinearLayout) findViewById(R.id.parent_id);
    
            nestedScrollView.setOnScrollChangeListener(this);
    
            linearLayout.setOnTouchListener(this);
    
            layoutParams=(LinearLayout.LayoutParams) view.getLayoutParams();
    
        }
    
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
    
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveY=(int) event.getY();
    
                    if (isBacking){
                        isBacking=false;
                    }
                    height=nowHeight+moveY-linearLayout.getDownY();
    
                    layoutParams.height=height;
                    view.setLayoutParams(layoutParams);
    
                    break;
                case MotionEvent.ACTION_UP:
                    isBacking=true;
                    new BackTop().execute();
                    break;
            }
    
            return true;
        }
    
        @Override
        public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
            linearLayout.setScrollY(v.getScrollY());
        }
    
        class BackTop extends AsyncTask{
    
            @Override
            protected Object doInBackground(Object[] objects) {
    
                while (isBacking){
    
                    publishProgress(height);
    
                    SystemClock.sleep(3);
    
                    height--;
                    nowHeight=height;
                    if (height==0){
                        break;
                    }
    
                }
    
                return true;
            }
    
            @Override
            protected void onProgressUpdate(Object[] values) {
    
                layoutParams.height=(int) values[0];
    
                view.setLayoutParams(layoutParams);
    
            }
    
            @Override
            protected void onPostExecute(Object o) {
    
            }
        }
    }

    代码中,我们分别设置了nestedScrollView的监听事件和linearLayout的监听事件。

    我们先去看一下nestedScrollView的监听事件:

    @Override
        public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
            linearLayout.setScrollY(v.getScrollY());
        }

    非常简单,就只有一句代码,设置linearLayout中scrollY变量的值。scrollY上面我们已经说过了,代表的是NestedScrollView 的滚动位置,在代码中也可以看到,我们传入的参数为NestedScrollView.getScrollY()方法,getScrollY()返回的就是滚动的高度。

    下面我们看linearLayout的监听事件:

    @Override
        public boolean onTouch(View v, MotionEvent event) {
    
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_MOVE:
                    moveY=(int) event.getY();
    
                    if (isBacking){
                        isBacking=false;
                    }
                    height=nowHeight+moveY-linearLayout.getDownY();
    
                    layoutParams.height=height;
                    view.setLayoutParams(layoutParams);
    
                    break;
                case MotionEvent.ACTION_UP:
                    isBacking=true;
                    new BackTop().execute();
                    break;
            }
    
            return true;
        }

    我们明确一下,只有MylinearLayout确定要拦截并消耗滑动事件,才会执行它的onTouch()方法。所以在它的onTouch()方法中,处理的逻辑主要为随着手指下滑的过程逐渐设置顶部刷新布局的高度。

    细心的你可能看出来了,我们没有在ACTION_DOWN状态下写任何的处理,这里的原因是因为根本不会进入到ACTION_DOWN的状态。为什么?滑动事件肯定会有ACTION_DOWN状态的啊!别急,不知道你忘没忘之前MyLinearLayout的onInterceptTouchEvent()方法,忘了你可以再去看一下,之所以这里不会进入ACTION_DOWN状态,那是因为程序在MyLinearLayout的onInterceptTouchEvent()方法已经消费过ACTION_DOWN状态了,所以当然不会再消费一次了!

    ACTION_DOWN状态下的处理代码为:

    downY=(int)ev.getY();

    标记了手指按下的Y坐标!

    然后我们在ACTION_MOVE状态下,通过调用MyLinearLayout的getDownY()方法来获取了手指按下的Y坐标。下面我们就去分析下主要的ACTION_MOVE状态执行的代码:

    case MotionEvent.ACTION_MOVE:
                    moveY=(int) event.getY();
    
                    if (isBacking){
                        isBacking=false;
                    }
                    height=nowHeight+moveY-linearLayout.getDownY();
    
                    layoutParams.height=height;
                    view.setLayoutParams(layoutParams);
    
                    break;
    

    首先我们获取手指触摸的Y坐标值,然后我们做了一个判断,这里先不讲这个判断,我们接着往下看。通过目前的Y坐标值减去起初按下时候的Y坐标值,获取手指在屏幕上向下移动的距离,然后把这个距离设置为顶部刷新布局的高度,这里通过layoutParams来动态设置布局高度的。其中还有一个nowHeight,我们也是先不关注它,下面讲到。就这样,我们实现了随着手指向下滑动顶部的刷新布局逐渐被拉出的效果。

    下面我们进入松开手指后的操作吧,代码;

    case MotionEvent.ACTION_UP:
                    isBacking=true;
                    new BackTop().execute();
                    break;

    这里我们开启了一个线程。这个线程的主要功能是做什么的呢?显而易见,是重新把拉出来的顶层刷新布局给弹回去的~

    BackTop的代码具体如下:

    class BackTop extends AsyncTask{
    
            @Override
            protected Object doInBackground(Object[] objects) {
    
                while (isBacking){
    
                    publishProgress(height);
    
                    SystemClock.sleep(3);
    
                    height--;
                    nowHeight=height;
                    if (height==0){
                        break;
                    }
    
                }
    
                return true;
            }
    
            @Override
            protected void onProgressUpdate(Object[] values) {
    
                layoutParams.height=(int) values[0];
    
                view.setLayoutParams(layoutParams);
    
            }
    
            @Override
            protected void onPostExecute(Object o) {
    
                 isBack=false;
            }
        }

    BackTop继承自AsyncTask框架,我们这里只重写了doInBackground()方法和onProgressUpdate()方法。通过在doInBackground()方法中不断的回调onProgressUpdate()方法来达到更新UI的效果。这里我们是完整的把拉出来的布局给弹了回去,你可以看到height一直减到了0。在实际的下拉刷新中,松开手指首先会回弹到一个固定的高度,比如回弹到200px,然后开始执行刷新操作,刷新结束后,再把布局给完全弹回去。这里因为我们并没有可刷新的东西,主要是给大家讲一下怎么做下拉刷新,所以我这里采用了直接完全弹回去。

    在BackTop中你也可以设置回弹的速度,回弹的速度主要收到系统沉睡的时间影响,这里我写的是SystemClock.sleep(3);沉睡3毫秒,你设置沉睡时间越大,那么它回弹的速度就越慢,沉睡时间越小,回弹速度越快。

    好了,下面就讲一下刚才忽略的isBack变量和nowHeight变量。

    首先先说一下可能存在这样的情况,在执行刷新的时候,用户依旧去下拉页面,那么这个时候如果不去处理这种情况,在刷新的时候继续下拉页面就会导致页面大变形的情况。你可能会说谁会这么无聊啊,已经在刷新了还要下拉,你还别说,真的有,可能人家在等待刷新的时候无聊再拉拉页面。即使没有这种情况,我们依旧要把这种情况考虑进去,毕竟这是作为一名合格的开发人员的职业素养。

    所以,我设置了一个布尔变量,让它来代表目前进行的状态。true为正在回弹的时候,false为没有回弹的时候。

    当正在回弹的时候,继续下拉就会进入ACTION_MOVE状态,我们判断出当前是正在回弹的状态,然后就把isBack设置为false,注意,这里设置成false后,回弹的线程就立刻停止了,为什么?回去看一下代码:

     @Override
            protected Object doInBackground(Object[] objects) {
    
                while (isBacking){
    
                    publishProgress(height);
    
                    SystemClock.sleep(3);
    
                    height--;
                    nowHeight=height;
                    if (height==0){
                        break;
                    }
    
                }
    
                return true;
            }
    

    因为在线程方法中,我们拿isBack当while循环的判断条件,当isBack为false时,不符合循环条件,线程方法自然就会停止了。

    我们接着看ACTION_MOVE状态下的代码,height值的计算加上了一个newHeight值,这个newHeight值就是在线程程序中不断地被赋值,目的只有一个,在回弹的过程中不断地记录每一刻height的数值。为什么要这样做?

    试想一下,如果没有这个newHeight值,正在回弹的时候再次下拉会出现什么状况?你会发现刷新布局竟然重新从顶端被拉下!原因很好理解,因为moveY-linearLayout.getDownY()的值是从0开始逐渐的增大的!

    没有记录height的数值,例如,当height的值减到200px,再次下拉,height的值就会直接从200变成了0,重新开始递增。这样de UI效果是不符合正常思维观念的,因为你绝对不希望从0开始拉出,而是在它回弹到的那个地方被再次拉出。所以我们需要记录下height的数据变化,当正在回弹的时候再次下拉,这个时候moveY-linearLayout.getDownY()加上它的回弹数值,才是真正的高度!

    进行到这里,讲解已经基本结束了。你可以运行一下看看效果。还有一些小地方,比如拉出到一定的高度,就改变提示信息,比如刚开始拉出来提示信息是“下拉刷新”,拉出300的px高度后,就把提示信息改为“松开立即刷新”。这些都非常容易实现,只需要在ACTION_MOVE状态下加上判断height的大小就可以了。还有一些炫酷的刷新布局,这些都是需要聪明的你自己去设计了,我在这里主要讲解的是下拉刷新的一些关键难题。

    好了,本文进行到这里基本就要结尾了。希望我的这篇博文能够给你启示和帮助,激发你无尽的创新能力。

    有需要引用本文的地方请标明出处,谢谢!

    展开全文
  • 下面App基本都有下拉刷新的功能,以前基本do
  • 今天要研究的是android v4自带的下拉刷新SwipeRefreshLayout,本来很久很久以前就想去研究一下它的,总是说自己没时间,其实时间挤挤还是有的,不废话了,先看一下最后用SwipeRefreshLayout实现的仿微信下拉刷新的...
  • 上一篇文章介绍了高仿京东的沉浸式状态栏,可是跟京东首页的头部轮播图相比,依然有三处缺憾:1、京东的...3、页面下拉到顶后,继续下拉会拉出带有“下拉刷新”字样的布局,此时松手则会触发页面的刷新动作;上面第一点
  • Android 自定义下拉刷新上拉加载实现的方式是SwipeRefreshLayout + RecyclerView 的VIewType首先看效果:总的思路:布局文件<android.support.v4.widget.SwipeRefreshLayout android:layout_marginTop="?attr/...
  • ListView中的下拉刷新是非常常见的,也是经常使用的,看到有很多同学想要,那我就整理一下,供大家参考。那我就不解释,直接上代码了。 这里需要自己重写一下ListView,重写代码如下: package net.loonggg.listview; ...
  • 1. 自定义ListView 自定义CListView继承ListView,实现AbsListView.OnScrollListener接口。 CListView添加...CListView捕捉手势操作,监听滚动事件并设置刷新事件。 public class CListView ex...
  • 视频教程传送门:... 完整代码在最下面。。 头布局xml: ...LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"... android:layout_width="... android:layout_h...
  • 在安卓开发中,我们会经常遇到上拉加载和下拉刷新的功能,通过网络请求拿到数据然后添加到控件上,之前自己也在网上搜索过一些文章,但基本上用处不大,要么是效果不一样,要么是贴的代码不完整,从而导致功能无法...
  • 这个下拉刷新效果分为两个部分:  step1:快递小哥和快递包裹的缩放效果,看上去就像是快递小哥跑过来一手拿过快递的样子  step2:快递小哥拿到包裹后,开启暴走模式!玩命送快递 PS:不得不赞一下京东的...
  • 1.首先写一个关于下拉刷新时显示的布局pull_to_refresh.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="matc
  • 下拉刷新、上拉加载、二级刷新、淘宝二楼、RefreshLayout、OverScroll,Android智能下拉刷新框架,支持越界回弹、越界拖动,具有极强的扩展性,集成了几十种炫酷的Header和 Footer。github地址
1 2 3 4 5 ... 20
收藏数 25,885
精华内容 10,354