热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

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

这篇文章主要为大家详细介绍了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直通车

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


推荐阅读
  • ASP11:深入解析与应用展望本文详细探讨了 ASP11 中的 `AppRelativeTemplateSourceDirectory` 属性,该属性用于获取或设置包含控件的 Page 或 UserControl 对象的应用程序相对虚拟目录。此外,文章还介绍了 1.0 版本中的 Binding 机制,分析了其在实际开发中的应用和优化方法,为开发者提供了全面的技术指导。 ... [详细]
  • 在基于.NET框架的分层架构实践中,为了实现各层之间的松散耦合,本文详细探讨了依赖注入(DI)和控制反转(IoC)容器的设计与实现。通过合理的依赖管理和对象创建,确保了各层之间的单向调用关系,从而提高了系统的可维护性和扩展性。此外,文章还介绍了几种常见的IoC容器实现方式及其应用场景,为开发者提供了实用的参考。 ... [详细]
  • VC维在机器学习中的应用与解析
    VC维在机器学习中的应用与解析VC维是指在机器学习中,一个假设空间能够正确分类的最大样本数量。具体而言,如果一个假设空间能够将N个样本以所有可能的 \(2^N\) 种方式完全分开,则称该假设空间具有N的VC维。VC维是衡量模型复杂度的重要指标,对于理解模型的泛化能力和过拟合风险具有重要意义。本文详细探讨了VC维的定义、计算方法及其在机器学习中的应用,并通过实例分析展示了其在模型选择和评估中的关键作用。 ... [详细]
  • 解读中台架构:微服务与分布式技术的区别及应用
    中心化与去中心化是长期讨论的话题。中心化架构的优势在于部署和维护相对简单,尤其在服务负载较为稳定的情况下,能够提供高效稳定的性能。然而,随着业务规模的扩大和技术需求的多样化,中心化架构的局限性逐渐显现,如扩展性和故障恢复能力较差。相比之下,微服务和分布式技术通过解耦系统组件,提高了系统的灵活性和可扩展性,更适合处理复杂多变的业务场景。本文将深入探讨中台架构中微服务与分布式技术的区别及其应用场景,帮助读者更好地理解和选择适合自身业务的技术方案。 ... [详细]
  • 掌握PHP编程必备知识与技巧——全面教程在当今的PHP开发中,了解并运用最新的技术和最佳实践至关重要。本教程将详细介绍PHP编程的核心知识与实用技巧。首先,确保你正在使用PHP 5.3或更高版本,最好是最新版本,以充分利用其性能优化和新特性。此外,我们还将探讨代码结构、安全性和性能优化等方面的内容,帮助你成为一名更高效的PHP开发者。 ... [详细]
  • 在第二课中,我们将深入探讨Scala的面向对象编程核心概念及其在Spark源码中的应用。首先,通过详细的实战案例,全面解析Scala中的类和对象。作为一门纯面向对象的语言,Scala的类设计和对象使用是理解其面向对象特性的关键。此外,我们还将介绍如何通过阅读Spark源码来进一步巩固对这些概念的理解。这不仅有助于提升编程技能,还能为后续的高级应用开发打下坚实的基础。 ... [详细]
  • 大家好,我是梅巴哥er。本文将深入探讨Redux框架中的第三个实战案例,具体实现每两秒自动点击按钮以触发颜色变化的功能。该案例中,一个关键点在于是否需要使用异步操作来处理定时任务,我们将详细分析其必要性和实现方式。通过这一实例,读者可以更好地理解Redux在实际项目中的应用及其异步处理机制。 ... [详细]
  • Java Web开发中的JSP:三大指令、九大隐式对象与动作标签详解
    在Java Web开发中,JSP(Java Server Pages)是一种重要的技术,用于构建动态网页。本文详细介绍了JSP的三大指令、九大隐式对象以及动作标签。三大指令包括页面指令、包含指令和标签库指令,它们分别用于设置页面属性、引入其他文件和定义自定义标签。九大隐式对象则涵盖了请求、响应、会话、应用上下文等关键组件,为开发者提供了便捷的操作接口。动作标签则通过预定义的动作来简化页面逻辑,提高开发效率。这些内容对于理解和掌握JSP技术具有重要意义。 ... [详细]
  • 深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用
    深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用 ... [详细]
  • 优化升级版数据采集与赋值方法,专为前文内容设计
    在前一篇文章中,方法的局限性主要体现在需要传递参数,并且参数数量受限。当页面布局与所需参数不匹配时,该方法将无法正常工作。为此,我们推出了优化升级版1.1,旨在解决这些问题并提高灵活性和适用性。 ... [详细]
  • 人人租机作为国内领先的信用免押租赁平台,为企业和个人提供全方位的新租赁服务。通过接入支付宝小程序功能,该平台实现了从零到百的迅猛增长,成为全国首家推出“新租赁小程序”开发服务的阿里巴巴小程序服务商(ISV)。这一创新举措不仅提升了用户体验,还显著增强了平台的市场竞争力。 ... [详细]
  • Java集合框架特性详解与开发实践笔记
    Java集合框架特性详解与开发实践笔记 ... [详细]
  • 去中心化媒体与去中心化内容:两者并非等同概念 ... [详细]
  • 【并发编程】全面解析 Java 内存模型,一篇文章带你彻底掌握
    本文深入解析了 Java 内存模型(JMM),从基础概念到高级特性进行全面讲解,帮助读者彻底掌握 JMM 的核心原理和应用技巧。通过详细分析内存可见性、原子性和有序性等问题,结合实际代码示例,使开发者能够更好地理解和优化多线程并发程序。 ... [详细]
  • D2iQ与Rafay联手打造统一的应用与基础设施管理解决方案
    D2iQ与Rafay合作推出了一种全面的应用和基础设施管理解决方案。本文深入探讨了双方如何通过集成技术实现统一管理,为面临类似挑战的企业提供详细的分析和实用建议,助力其高效管理和优化资源。 ... [详细]
author-avatar
BaoBao佳佳佳
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有