Android 简单易上手的下拉刷新控件

android 同时被 2 个专栏收录
3 篇文章 0 订阅
2 篇文章 0 订阅

背景:列表控件在Android App开发中用到的场景很多。在以前我们用ListView,GridView,现在应该大多数开发者都已经在选择使用RecyclerView了,谷歌给我们提供了这些方便的列表控件,我们可以很容易的使用它们。但是在实际的场景中,我们可能还想要更多的能力,比如最常见的列表下拉刷新,上拉加载。上拉刷新和下拉加载应该是列表的标配吧,基本上有列表的地方都要具体这个能力。虽然刷新这个功能已经有各种各样的第三方框架可以选择,但是毕竟不是自己的嘛,今天我们就来实现一个自己的下拉刷新控件,多动手才能更好的理解。

效果图:

效果图.gif

原理分析:

在coding之前,我们先分析一下原理,原理分析出来之后,我们才可以确定实现方案。
先上一张图,来个直观的认识:
布局样式.png

在列表上面有个刷新头,随着手指向下拉,逐渐把顶部不可见的刷新头拉到屏幕中来,用户能看到刷新的状态变化,达到下拉刷新的目的。

通过分析,我们确定一种实现方案:我们自定义一个容器,容器里面包含两个部分。
1. 顶部刷新头。
2. 列表区域。

确定好布局容器之后,我们来分析刷新头的几种状态,
下拉刷新状态.png

把下拉刷新分为5中状态,通过不同状态间的切换实现下拉刷新能力。
状态间的流程图如下:
刷新流程图.png

整个下拉刷新的流程就如图中所示。

流程清楚了之后,接下来就是编写代码实现了。

代码实现:

/**
 * @author luowang8
 * @date 2020-08-21 10:54
 * @desc 下拉刷新控件
 */
public class PullRefreshView extends LinearLayout {
	
	
	/**
	 * 头部tag
	 */
	public static final String HEADER_TAG = "HEADER_TAG";
	
	/**
	 * 列表tag
	 */
	public static final String LIST_TAG   = "LIST_TAG";
	
	/**
	 * tag
	 */
	private static final String TAG = "PullRefreshView";
	
	/**
	 * 默认初始状态
	 */
	private @State
	int mState = State.INIT;
	
	/**
	 * 是否被拖拽
	 */
	private boolean mIsDragging = false;
	
	/**
	 * 上下文
	 */
	private Context mContext;
	
	
	/**
	 * RecyclerView
	 */
	private RecyclerView mRecyclerView;
	
	/**
	 * 顶部刷新头
	 */
	private View mHeaderView;
	
	/**
	 * 初始Y的坐标
	 */
	private int mInitMotionY;
	
	/**
	 * 上一次Y的坐标
	 */
	private int mLastMotionY;
	
	/**
	 * 手指触发滑动的临界距离
	 */
	private int mSlopTouch;
	
	/**
	 * 触发刷新的临界值
	 */
	private int mRefreshHeight = 200;
	
	/**
	 * 滑动时长
	 */
	private int mDuring = 300;
	
	/**
	 * 用户刷新监听器
	 */
	private OnRefreshListener mOnRefreshListener;
	
	/**
	 * 刷新文字提示
	 */
	private TextView mRefreshTip;
	
	/**
	 * 是否可拖拽, 因为在刷新头自由滑动和刷新状态的时候,
	 * 我们应该保持界面不被破坏
	 */
	private boolean mIsCanDrag = true;
	
	/**
	 * 头部布局
	 */
	private LayoutParams mHeaderLayoutParams;
	
	/**
	 * 列表布局
	 */
	private LayoutParams mListLayoutParams;
	
	/**
	 * 属性动画
	 */
	private ValueAnimator mValueAnimator;
	
	
	
	/// 分割 ///
	
	
	/**
	 * @param context
	 */
	public PullRefreshView(Context context) {
		this(context, null);
	}
	
	/**
	 * @param context
	 * @param attrs
	 */
	public PullRefreshView(Context context, @Nullable AttributeSet attrs) {
		this(context, attrs, 0);
	}
	
	/**
	 * @param context
	 * @param attrs
	 * @param defStyleAttr
	 */
	public PullRefreshView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		
		mContext = context;
		
		initView();
	}
	
	public RecyclerView getRecyclerView() {
		return mRecyclerView;
	}
	
	/**
	 * 设置RecyclerView
	 *
	 * @param recyclerView
	 */
	public void addRecyclerView(RecyclerView recyclerView) {
		
		if (recyclerView == null) {
			return;
		}
		
		View view = findViewWithTag(LIST_TAG);
		if (view != null) {
			removeView(view);
		}
		
		this.mRecyclerView = recyclerView;
		this.mRecyclerView.setTag(LIST_TAG);
		addView(recyclerView, mListLayoutParams);
	}
	
	/**
	 * 设置自定义刷新头部
	 * @param headerView
	 */
	public void addHeaderView(View headerView) {
		
		if (headerView == null) {
			return;
		}
		
		View view = findViewWithTag(HEADER_TAG);
		if (view != null) {
			removeView(view);
		}
		
		this.mHeaderView = headerView;
		this.mHeaderView.setTag(HEADER_TAG);
		addView(mHeaderView, mHeaderLayoutParams);
	}
	
	/**
	 * @param onRefreshListener
	 */
	public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
		mOnRefreshListener = onRefreshListener;
	}
	
	/**
	 * 初始化View
	 */
	private void initView() {
		
		setOrientation(LinearLayout.VERTICAL);
		
		Context context = getContext();
		/** 1、添加刷新头Header */
		mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header, null);
		mHeaderView.setTag(HEADER_TAG);
		mRefreshTip = mHeaderView.findViewById(R.id.content);
		mHeaderLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
				DensityUtil.dip2px(mContext, 500)
		);
		this.addView(mHeaderView, mHeaderLayoutParams);
		
		/** 2、添加内容RecyclerView */
		mRecyclerView = new RecyclerView(context);
		mRecyclerView.setTag(LIST_TAG);
		mListLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
		this.addView(mRecyclerView, mListLayoutParams);
		
		/** 3、一开始的时候要让Header看不见,设置向上的负paddingTop */
		setPadding(0, -DensityUtil.dip2px(mContext, 500), 0, 0);
		
		ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
		mSlopTouch = viewConfiguration.getScaledTouchSlop();
		
		setState(State.INIT);
		
	}
	
	/**
	 * 设置状态,每个状态下,做不同的事情
	 *
	 * @param state 状态
	 */
	private void setState(@State int state) {
		
		switch (state) {
			case State.INIT:
				initState();
				break;
			
			case State.DRAGGING:
				dragState();
				break;
			
			case State.READY:
				readyState();
				break;
			
			case State.REFRESHING:
				refreshState();
				break;
			
			case State.FLING:
				flingState();
				break;
			
			default:
				break;
		}
		
		mState = state;
	}
	
	/**
	 * 处理初始化状态方法
	 */
	private void initState() {
		
		// 只有在初始状态时,恢复成可拖拽
		mIsCanDrag = true;
		mIsDragging = false;
		mRefreshTip.setText("下拉刷新");
	}
	
	/**
	 * 处理拖拽时方法
	 */
	private void dragState() {
		mIsDragging = true;
	}
	
	/**
	 * 拖拽距离超过header高度时,如何处理
	 */
	private void readyState() {
		mRefreshTip.setText("松手刷新");
	}
	
	/**
	 * 用户刷新时,如何处理
	 */
	private void refreshState() {
		if (mOnRefreshListener != null) {
			mOnRefreshListener.onRefresh();
		}
		
		mIsCanDrag = false;
		mRefreshTip.setText("正在刷新,请稍后...");
	}
	
	/**
	 * 自由滚动时,如何处理
	 */
	private void flingState() {
		mIsDragging = false;
		mIsCanDrag = false;
		
		/** 自由滚动状态可以从两个状态进入:
		 *  1、READY状态。
		 *  2、其他状态。
		 *
		 *  !滑动均需要平滑滑动
		 *  */
		if (mState == State.READY) {
			
			Log.e(TAG, "flingState: 从Ready状态开始自由滑动");
			// 从准备状态进入,刷新头滑到 200 的位置
			
			smoothScroll(getScrollY(), -mRefreshHeight);
		}
		else {
			
			Log.e(TAG, "flingState: 松手后,从其他状态开始自由滑动");
			// 从刷新状态进入,刷新头直接回到最初默认的位置
			// 即: 滑出界面,ScrollY 变成 0
			smoothScroll(getScrollY(), 0);
		}
		
	}
	
	/**
	 *  光滑滚动
	 * @param startPos 开始位置
	 * @param targetPos 结束位置
	 */
	private void smoothScroll(int startPos, final int targetPos) {
		
		// 如果有动画正在播放,先停止
		if (mValueAnimator != null && mValueAnimator.isRunning()) {
			mValueAnimator.cancel();
			mValueAnimator.end();
			mValueAnimator = null;
		}
		
		// 然后开启动画
		mValueAnimator = ValueAnimator.ofInt(getScrollY(), targetPos);
		mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator valueAnimator) {
				int value = (int) valueAnimator.getAnimatedValue();
				scrollTo(0, value);
				
				if (getScrollY() == targetPos) {
					if (targetPos != 0) {
						setState(State.REFRESHING);
					}
					else {
						setState(State.INIT);
					}
				}
			}
		});
		
		mValueAnimator.setDuration(mDuring);
		mValueAnimator.start();
	}
	
	/**
	 * 是否准备好触发下拉的状态了
	 */
	private boolean isReadyToPull() {
		
		if (mRecyclerView == null) {
			return false;
		}
		
		LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
		
		if (manager == null) {
			return false;
		}
		
		if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {
			View child  = mRecyclerView.getChildAt(0);
			int  height = child.getHeight();
			if (height > mRecyclerView.getHeight()) {
				return child.getTop() == 0 && manager.findFirstVisibleItemPosition() == 0;
			}
			else {
				return manager.findFirstCompletelyVisibleItemPosition() == 0;
			}
		}
		
		return false;
	}
	
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		
		int action = ev.getAction();
		
		Log.e(TAG, "onInterceptTouchEvent: action = " + action);
		
		if (!mIsCanDrag) {
			return true;
		}
		
		if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
			mIsDragging = false;
			return false;
		}
		
		if (mIsDragging && action == MotionEvent.ACTION_MOVE) {
			return true;
		}
		
		switch (action) {
			case MotionEvent.ACTION_MOVE:
				int diff = (int) (ev.getY() - mLastMotionY);
				if (Math.abs(diff) > mSlopTouch && diff > 1 && isReadyToPull()) {
					mLastMotionY = (int) ev.getY();
					mIsDragging = true;
				}
				break;
			
			case MotionEvent.ACTION_DOWN:
				if (isReadyToPull()) {
					setState(State.INIT);
					mInitMotionY = (int) ev.getY();
					mLastMotionY = (int) ev.getY();
				}
				break;
			
			default:
				break;
		}
		
		return mIsDragging;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		
		int action = event.getAction();
		
		Log.e(TAG, "onTouchEvent: action = " + action);
		
		if (!mIsCanDrag) {
			return false;
		}
		
		switch (action) {
			case MotionEvent.ACTION_DOWN:
				if (isReadyToPull()) {
					setState(State.INIT);
					mInitMotionY = (int) event.getY();
					mLastMotionY = (int) event.getY();
				}
				break;
			
			case MotionEvent.ACTION_MOVE:
				
				if (mIsDragging) {
					mLastMotionY = (int) event.getY();
					setState(State.DRAGGING);
					
					pullScroll();
					return true;
				}
				
				break;
			
			case MotionEvent.ACTION_UP:
			case MotionEvent.ACTION_CANCEL:
				mIsDragging = false;
				setState(State.FLING);
				break;
			
			default:
				break;
			
		}
		
		return true;
	}
	
	/**
	 * 下拉移动界面,拉出刷新头
	 */
	private void pullScroll() {
		/** 滚动值 = 初始值 - 结尾值 */
		int scrollValue = (mInitMotionY - mLastMotionY) / 3;
		
		if (scrollValue > 0) {
			scrollTo(0, 0);
			return;
		}
		
		if (Math.abs(scrollValue) > mRefreshHeight
				&& mState == State.DRAGGING) {
			// 约定:如果偏移量超过 200(这个值,表示是否可以启动刷新的临界值,可任意定),
			// 那么状态变成 State.READY
			Log.e(TAG, "pullScroll: 超过了触发刷新的临界值");
			setState(State.READY);
		}
		
		scrollTo(0, scrollValue);
	}
	
	/**
	 * 刷新完成,需要调用方主动发起,才能完成将刷新头收起
	 */
	public void refreshComplete() {
		mRefreshTip.setText("刷新完成!");
		setState(State.FLING);
	}
	
	@IntDef({
			        State.INIT
			        , State.DRAGGING
			        , State.READY
			        , State.REFRESHING
			        , State.FLING,
	        })
	@Retention(RetentionPolicy.SOURCE)
	public @interface State {
		
		/**
		 * 初始状态
		 */
		int INIT = 1;
		
		/**
		 * 手指拖拽状态
		 */
		int DRAGGING = 2;
		
		/**
		 * 就绪状态,松开手指后,可以刷新
		 */
		int READY = 3;
		
		/**
		 * 刷新状态,这个状态下,用户用于发起刷新请求
		 */
		int REFRESHING = 4;
		
		/**
		 * 松开手指,顶部自然回弹的状态,有两种表现
		 * 1、手指释放时的高度大于刷新头的高度。
		 * 2、手指释放时的高度小于刷新头的高度。
		 */
		int FLING = 5;
	}
	
	/**
	 * 用户刷新状态的操作
	 */
	public interface OnRefreshListener {
		void onRefresh();
	}
	
}

实现的逻辑并不复杂,新手都能看懂,先理解了整个流程,代码就是水到渠成的事。
思想第一,最后代码。

完整DEMO直通车:https://github.com/wwluo14/PullToRefresh

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

rooman_luo

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值