Android实现简单的下拉刷新控件

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

效果图:

原理分析:

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

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

通过分析,我们确定一种实现方案:我们自定义一个容器,容器里面包含两个部分。

1. 顶部刷新头。
2. 列表区域。

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

把下拉刷新分为5中状态,通过不同状态间的切换实现下拉刷新能力。

状态间的流程图如下:

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

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

代码实现:

/**
 * @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直通车

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android RecyclerView实现下拉刷新和上拉加载

    RecyclerView已经出来很久了,许许多多的项目都开始从ListView转战RecyclerView,那么,上拉加载和下拉刷新是一件很有必要的事情. 在ListView上,我们可以通过自己添加addHeadView和addFootView去添加头布局和底部局实现自定义的上拉和下拉,或者使用一些第三方库来简单的集成,例如Android-pulltorefresh或者android-Ultra-Pull-to-Refresh,后者的自定义更强,但需要自己实现上拉加载. 而在下面我们将用两种方式

  • android开发教程之实现listview下拉刷新和上拉刷新效果

    复制代码 代码如下: public class PullToLoadListView extends ListView implements OnScrollListener { private static final String TAG = PullToLoadListView.class.getSimpleName(); private static final int STATE_NON = 0; private static final int STATE_PULL_TO_REFRE

  • Android下拉刷新ListView——RTPullListView(demo)

    下拉刷新在越来越多的App中使用,已经形成一种默认的用户习惯,遇到列表显示的内容时,用户已经开始习惯性的拉拉.在交互习惯上已经形成定性.之前在我的文章<IOS学习笔记34-EGOTableViewPullRefresh实现下拉刷新>中介绍过如何在IOS上实现下拉刷新的功能.今天主要介绍下在Android上实现下拉刷新的Demo,下拉控件参考自Github上开源项目PullToRefresh,并做简单修改.最终效果如下:                         工程结构如下: 使用过程中

  • Android PullToRefreshLayout下拉刷新控件的终结者

    说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能.有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩展性,太单调了.看了QQ2014的列表下拉刷新,发现挺好看的,我喜欢,贴一下图看一下qq的下拉刷新效果: 不错吧?嗯,是的.一看就知道实现方式不一样.咱们今天就来实现一个下拉刷新控件.由于有时候不仅仅是ListView需要下拉刷新,ExpandableListView和GridView也有这个需求,

  • Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能(附源码)

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

  • Android自定义SwipeRefreshLayout高仿微信朋友圈下拉刷新

    上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://www.jb51.net/article/89310.htm 上一篇里最后提到,SwipeRefreshLayout的可定制性是比较差的,看源码会发现跟样式相关的几个类都是private的而且方法是写死的,只暴露出了几个颜色设置的方法.这样使得SwipeRefreshLayout的使用比较简单,主要就是设置一个监听器在onRefresh方法里完成刷新逻辑.讲道理Swip

  • Android官方下拉刷新控件SwipeRefreshLayout使用详解

    可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介: SwipeRefreshLayout组件只接受一个子组件:即需要刷新的那个组件.它使用一个侦听机制来通知拥有该组件的监听器有刷新事件发生,换句话说我们的Activity必须实现通知的接口.该Activity负责处理事件刷新和刷新相应的视图.一旦监听者接收到该事件,就决定了刷新过程中应处理的地方.如果要展示一个"刷新动画",它必须

  • Android中使用RecyclerView实现下拉刷新和上拉加载

    推荐阅读:使用RecyclerView添加Header和Footer的方法                       RecyclerView的使用之HelloWorld RecyclerView 是Android L版本中新添加的一个用来取代ListView的SDK,它的灵活性与可替代性比listview更好.本文给大家介绍如何为RecyclerView添加下拉刷新和上拉加载,过去在ListView当中添加下拉刷新和上拉加载是非常方便的利用addHeaderView和addFooterVie

  • Android实现上拉加载更多以及下拉刷新功能(ListView)

    首先为大家介绍Andorid5.0原生下拉刷新简单实现. 先上效果图: 相对于上一个19.1.0版本中的横条效果好看了很多.使用起来也很简单. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" and

  • Android下拉刷新上拉加载控件(适用于所有View)

    前面写过一篇关于下拉刷新控件的文章下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅如此,我已经把它改成了对所有View都通用!可以随心所欲使用这两个功能~~ 我做了一个大集合的demo,实现了ListView.GridView.ExpandableListView.ScrollView.WebView.ImageView.TextView的下拉刷新和上拉加载.后面会提供demo的

随机推荐