背景:列表控件在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();

}

}

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

思想第一,最后代码。

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

android 下拉刷新 组件,Android实现简单的下拉刷新控件相关推荐

  1. Android之使用VideoView组件播放一个简单的视频

    1.在Android开发中,提供了VideoView组件用来播放视频文件.首先,要使用这个组件来播放视频,必须在布局文件下添加VideoView组件,然后再到Activity里获取这个组件,然后调用这 ...

  2. COM组件开发实践(八)---多线程ActiveX控件和自动调整ActiveX控件大小(下)

    源代码下载:MyActiveX20081229.rar 声明:本文代码基于CodeProject的文章<A Complete ActiveX Web Control Tutorial>修改 ...

  3. Android仿抖音加载框之两颗小球转动控件

    Android仿抖音加载框之两颗小球转动控件 本篇文章已授权微信公众号 hongyangAndroid(鸿洋)独家发布. 效果图 安卓版抖音v2.5加载框: 本控件效果图: 使用方法 源码地址:And ...

  4. .net组件开发系列之武术系列 武术招数 控件生命周期与控件事件机制

    .net组件开发系列之武术系列 武术招数 控件生命周期与控件事件机制一.控件生命周期 先回述上篇,可能表述没有不清晰,也可能跨度大了点,好的,我们来一个循序渐进过程,大家都知道,武术都有招术的,先出什 ...

  5. 新瓶旧酒ASP.NET AJAX(1) - 简单地过一下每个控件(ScriptManager、ScriptManagerProxy

    [索引页] [×××] 新瓶旧酒ASP.NET AJAX(1) - 简单地过一下每个控件(ScriptManager.ScriptManagerProxy.UpdatePanel. UpdatePro ...

  6. uniapp 判断页面是否是横竖屏,解决微信小程序video组件全屏播放视频遮盖自定义播放控件问题

    如果res.deviceOrientation 等于landscape 的话是竖屏,portrait则是横屏.因为用户每旋转一次屏幕就会触发里面的onShow钩子,因此在页面显示或横竖屏变化都会触发这 ...

  7. android 下拉刷新 组件,android系统自带下拉刷新控件的实现

    android系统自带的下拉刷新控件SwipeRefreshLayout位于android.support.v4.widget包下,实现步骤如下: 1.在布局文件中添加该控件,该控件一般作为父控件,而 ...

  8. android组件开关按钮,简单聊聊“开关”这个小控件

    开关虽然只是一个小控件,看起来很简单,但其实它的设计也有着大学问.本文和你一起探讨一下~ 一.开关是什么 开关,英文Switch,常被翻译为开关.滑动开关.切换开关,作为界面中可直接操作的元件,提供两 ...

  9. android应用程序的组件,Android基础之应用程序组件

    Android应用程序由若干个不同类型的组件组合而成,每一个组件具有其特定的安全保护设计方式,它们的安全直接影响到应用程序的安全.Android应用程序组件的主要类型有:活动(Activity),服务 ...

  10. android 图片轮播组件,Android客户端实现图片轮播控件

    本文和大家一起写一个Android图片轮播控件,供大家参考,具体内容如下 1. 轮播控件的组成部分 我们以知乎日报Android客户端的轮播控件为例,分析一下轮播控件的主要组成: 首先我们要有用来显示 ...

最新文章

  1. c语言日期星期程序,C语言程序设计: 输入年月日 然后输出是星期几
  2. 使用pyinstaller打包,subprocess报“句柄无效”错误的解决方法
  3. 2048c语言程序,C语言实现2048小游戏
  4. RedHat7/Centos7 搭建NFS服务器
  5. Mongodb数据库初识
  6. scala的foreach和for
  7. C#控制台程序,发送邮件,可带附件
  8. 停牌17个月 汉能薄膜真的要复牌了?
  9. Linux——给history命令加上用户和时间
  10. 36岁程序员:领导平时称兄道弟,裁员时立刻变脸,看透人性
  11. close wait 过多原因_time_wait 详解和解决方案
  12. 全国中小学生计算机竞赛试题,全国中小学电脑制作活动
  13. Thymeleaf 模板布局三种区别
  14. UI自动化测试工具-AirTest
  15. ngix 全局配置文件和子配置文件 配置项中文注释
  16. FLTK中使用SDL的一种处理方式
  17. Team building | 什么?团建还能这么玩?
  18. warning: Clone succeeded, but checkout failed.
  19. 《工程学导论》读后感
  20. linux查看执行过哪些命令,linux查看执行过的所有命令

热门文章

  1. postman安装使用教程(标贝科技)
  2. android 后台 截屏,Android 截屏方式
  3. [ustc]那些杀手不太冷
  4. JSMInd实现动态思维导图的保存和展示
  5. 创建简单vue项目 / Webpack创建vue项目
  6. lm317扩流电路 (全集)
  7. 《我喜欢生命本来的样子》周国平 .mobi .epub .pdf .azw3 电子版下载 读书笔记
  8. win10硬盘分区怎么分
  9. 安装TortoiseSVN报could not write value to key的错误解决方案
  10. Unity 3D 特效学习记录