下拉刷新实现

下拉刷新实现下拉刷新在 Android 应用开发中是一种很常见的交互方式 在实际开发中都会引用第三方的下拉刷新库来实现 第三方库通常都经过多个应用程序集成测试 有着相对较高的稳定性和可靠性 里面的代码逻辑也相对比较庞杂 对新手相对不太友好 学习起来比较费时费力 本节就通过前面学习的 Android 视图基本原理来实现自定义的下拉刷新库 补白和边距补白 Padding 指的是视图内部的内容与视图边界之间的距离 通常

下拉刷新在Android应用开发中是一种很常见的交互方式,在实际开发中都会引用第三方的下拉刷新库来实现,第三方库通常都经过多个应用程序集成测试,有着相对较高的稳定性和可靠性,里面的代码逻辑也相对比较庞杂,对新手相对不太友好,学习起来比较费时费力,本节就通过前面学习的Android视图基本原理来实现自定义的下拉刷新库。

补白和边距

上图展示了在LinearLayout中设置了负数值的子视图展示情况,可以看到负数的Padding不仅会影响子视图内容的展示还会影响父布局的尺寸大小。我们知道onMeasure()方法负责测量当前视图的宽高值,onLayout()负责将布局中的视图设置到指定的位置,查看LinearLayout竖向布局的尺寸测量代码。

在measureVertical()测量竖向布局高度时会首先计算内部可见的子视图高度总值,子布局的高度还要加上下补白的数值得到heightSize数值,heightSize还有与最小高度作比较,其实大部分情况都能确保最终setMeasureDimension()方法中使用的高度值就是heightSize的值。考虑前面的mPaddingTop设置成负值的情况,负值会减少heightSize最终的计算结果值也就导致LinearLayout的高度减小。接着查阅LinearLayout竖向布局方法的实现,看它如何排放内部的子视图位置。

// LinearLayout测量布局源代码 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { // 计算子控件的总高度mTotalLength // 总高度会加上自己的上下补白,mPaddingTop为负值会减小布局高度 mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); // 设置LinearLayout的高度为heightSize } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop = mPaddingTop; // mPaddingTop为负值也会影响子视图展示位置 int childLeft; for (View view : getChildren()) { childTop += lp.topMargin; // 此处如果topMargin是负值,会影响子视图展示位置 childLeft = paddingLeft + lp.leftMargin; // child.layout(left, top, left + width, top + height); setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += view.getMeasureHeight(); } } 

在layoutVertical()方法中会根据LinearLayout的paddingLeft和子视图的LayoutParams.leftMargin计算当前子视图左边界距离,mPaddingTop和子视图的LayoutParams.topMargin计算子视图的上边界在布局中的位置。考虑前面的marginTop边距设置为负值的情况,由于负值会使得childTop值变小,也就是说子视图距离LinearLayout顶部边界变短,子视图的位置也就更加靠上。

如果测试横向的LinearLayout会发现即使设置了负值mPaddingTop,它的高度也不会发生变化,查阅layoutHorizontal()方法会发现在测量高度的时候并不会把mPaddingTop值计算在内,自然也就不会发生最终的布局高度改变的效果。测试其他四大布局会发现有些负值mPaddingTop能够改变布局高度,有些设置负值根本不会对布局高度产生任何效果,总结来说在setMeasureDimension() 方法中设置的高度值如果计算了mPaddingTop和mPaddingBottom那么负值补白就可以改变布局高度。

刷新视图原理

在下拉刷新中控件的顶部会慢慢地出现下拉视图,下拉视图展示过程中就代表正在执行网络请求操作,等到网络请求成功返回下拉视图会慢慢消失,展示已经刷修改完数据的新界面。下拉视图的展示和消失都是有一个渐进的过程,不是setVisible()那种即刻消失或展示的样式,想要实现这种渐进展示和消失的动画效果就可以利用负值补白来改变下拉视图的高度值,当mPaddingTop为0的时候刷新视图正常展示;当mPaddingTop从0到负下拉视图高度变化时下拉视图组件高度逐渐变成0,也就是逐渐消失;当mPaddingTop从负下拉视图高度到0变化时下拉视图高度逐渐变大,也就是逐渐展示。

// PullRefreshView基础实现代码 public class PullRefreshView extends FrameLayout { private static final String TAG = "PullRefreshView"; private View mHeaderView; private View mContentView; private static final int MAX_PULL_LENGTH = Utils.dp2px(200); private static final long MAX_GOBACK_DURATION = 200; private static final int REFRESH_IDLE = 0; // 静止状态 private static final int REFRESH_PULL = 1; // 手动下拉 private static final int REFRESH_RELEASED = 2; // 下拉松手 private static final int REFRESH_REFRESHING = 3; // 正在刷新 private int mState = REFRESH_IDLE; private int mHeaderHeight; private int mTouchSlop; public interface RefreshListener { void onRefresh(); } private RefreshListener mRefreshListener; private void notifyRefreshStart() { if (mRefreshListener != null) { // 通知开始刷新操作 mRefreshListener.onRefresh(); } } public void notifyRefreshComplete() { if (isRefreshing()) { // 刷新结束,头部视图弹回到不可见 headerGoBack(); } } // 暂时省略其他部分 } 
// HeaderView拉伸回弹实现代码 private void setHeaderPaddingTop(int top) { // HeaderView会随着paddingTop变化逐渐消失或逐渐展示 mHeaderView.setPadding(0, top, 0, 0); mHeaderView.requestLayout(); } private void headerGoBack() { if (!isReleased() && !isRefreshing()) { return; } ValueAnimator valueAnimator = ValueAnimator.ofInt(getHeaderPaddingTop(), -mHeaderHeight); valueAnimator.setDuration(MAX_GOBACK_DURATION); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (int) animation.getAnimatedValue(); setHeaderPaddingTop(value); // 不断地更改头部视图paddingTop } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mState = REFRESH_IDLE; // 头部视图完全不可见时进入REFRESH_IDEL状态 } }); valueAnimator.start(); } 

ScrollView下拉刷新

PullRefreshView接收到的触摸事件一概传递给它的内容控件来处理,不过原生的ScrollView控件内部的触摸事件处理已经固定下来,需要使用ScrollView的子类覆盖dispatchTouchEvent()来修改它默认的处理方式。为此需要在加载PullRefreshView内部的InternalScrollView控件的时候替换系统提供的原生ScrollView。

// PullRefreshView替换内部的用户ScrollView private void initViews() { mContentView = getChildAt(0); FrameLayout.LayoutParams contentParams = (LayoutParams) mContentView.getLayoutParams(); contentParams.width = ViewGroup.LayoutParams.MATCH_PARENT; contentParams.height = ViewGroup.LayoutParams.MATCH_PARENT; removeView(mContentView); // 将原生ScrollView替换成支持下拉刷新的InternalScrollView if (mContentView instanceof ScrollView) { mContentView = new InternalScrollView(getContext(), (ScrollView) mContentView); } mContentView.setLayoutParams(contentParams); addView(mContentView); } // 将PullRefreshView接收到的所有触摸事件都传递给内容控件 public boolean dispatchTouchEvent(MotionEvent event) { return mContentView.dispatchTouchEvent(event); } 

替换后的InternalScrollView作为PullRefreshView内部的mContentView成员对象,系统派发过来的所有MotionEvent事件都由InternalScrollView负责处理。由于原生ScrollView内部只有用户内容布局存在, InternalScrollView需要先将原生的ScrollView内部用户内容对象添加到竖向LinearLayout底部,LinearLayout的上面部分则负责展示下拉视图。在dispatchTouchEvent()方法中首先判断用户是否在做滑动操作,如果是滑动操作是否满足下拉刷新的条件,满足条件就要执行下拉刷新视图展示动画,否则需要调用super.dispatchTouchEvent()实现默认的ScrollView触摸事件处理。

// InternalScrollView实现代码 private class InternalScrollView extends ScrollView { private int mDownY; private int mLastY; private boolean mIsDragging = false; // 是否在做下拉刷新动作 public InternalScrollView(Context context, ScrollView origin) { super(context); setId(origin.getId()); // 将原生ScrollView的id设置给InternalScrollView LinearLayout linearLayout = new LinearLayout(getContext()); linearLayout.setOrientation(LinearLayout.VERTICAL); // 获取原生ScrollView中的用户内容布局 View content = origin.getChildAt(0); origin.removeAllViews(); // 竖向LinearLayout内部包含下拉视图和用户内容布局 linearLayout.addView(mHeaderView); linearLayout.addView(content); // InternalScrollView内部的布局包含下拉刷新视图和用户内容视图 addView(linearLayout); } @Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getActionMasked(); int y = (int) event.getRawY(); switch (action) { case MotionEvent.ACTION_DOWN: mDownY = y; break; case MotionEvent.ACTION_MOVE: int motionY = y - mDownY; // 代表用户滑动的方向 int diff = y - mLastY; // 用户本次滑动与上次滑动的偏差 // 如果当前没有滑动操作而且用户移动距离超出最小滑动 // 距离mTouchSlop,如果用户向下滑动且内容控件的第一 // 条数据处在内容顶部,此时需要准备开始下拉操作;如果 // 用户向上滑动而且头部视图部分可见,准备向上滑动头部视图 if (!mIsDragging && Math.abs(motionY) > mTouchSlop && ((motionY > 0 && isFirstAtTop()) || isFirstAtTop() && motionY < 0 && getHeaderPaddingTop() > -mHeaderHeight)) { mIsDragging = true; } if (mIsDragging) { // 用户正在做滑动操作 mState = REFRESH_PULL; offsetHeader(diff); // 渐进增大或减小下拉视图 if (getHeaderPaddingTop() <= -mHeaderHeight) { // 如果用户手动将下拉视图推到了 // 不可见位置,不再修改下拉视图的大小 mState = REFRESH_IDLE; mIsDragging = false; setHeaderPaddingTop(-mHeaderHeight); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsDragging = false; if (isPulling()) { // 如果用户正在下拉过程中松手 mState = REFRESH_RELEASED; // 如果下拉视图已经全部展示出来需要 // 先退回展示全部,再触发刷新操作 if (shouldRefresh()) { mState = REFRESH_REFRESHING; goBackAndShowRefresh(); // 参考代码4-38 } else { // 如果下拉视图没有全部展示,只下拉了一下部分, // 直接退回去不触发刷新 headerGoBack(); } } break; } mLastY = y; // 如果mIsDragging为true代表用户正在做下拉刷新, // 否则就执行ScrollView内部滚动 return mIsDragging || super.dispatchTouchEvent(event); } // 判定当前用户内容视图的顶部在InternalScrollView的顶部,没有内容被卷起来 // 用户这时向下拉就是要做下拉刷新 public boolean isFirstAtTop() { return mContentView.getScrollY() <= 0; } } 
// HeaderView拉伸过长弹回到实际高度展示 private void goBackAndShowRefresh() { if (!isRefreshing()) { return; } int paddingTop = getHeaderPaddingTop(); // paddingTop为零的时候下拉视图完全展示,超出0时需要先回到0 if (paddingTop > 0) { ValueAnimator valueAnimator = ValueAnimator.ofInt(paddingTop, 0); valueAnimator.setDuration(MAX_GOBACK_DURATION); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int value = (int) animation.getAnimatedValue(); setHeaderPaddingTop(value); } }); valueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // 下拉视图paddingTop为零时触发刷新操作 notifyRefreshStart(); } }); valueAnimator.start(); } else { notifyRefreshStart();// 下拉视图paddingTop为零时触发刷新操作 } } 

代码中paddingTop大于零就代表用户将HeaderView下拉的比实际高度要高出paddingTop的长度,需要先将HeaderView缩回到paddingTop为零的正常高度再触发刷新操作。到目前为止ScrollView的下拉刷新就成功触发了网络请求,等到网络请求成功后会通知刷新操作已完成并调用headGoBack()实现下拉视图渐进消失操作,ScrollView的一次下拉刷新交互就完成了。

ListView下拉刷新

在初始化PullRefreshView内部控件时如果子控件是ListView类型,需要将它替换成InternalListView自定义控件,InternalListView需要控件会在调用addHeaderView()方法添加HeaderView为头部视图,这样当头部视图的高度发生变化的时候ListView内部的用户内容控件也会随之变化位置。

// InternalListView实现代码 private class InternalListView extends ListView { private int mDownY; private int mLastY; private boolean mIsDragging = false; public InternalListView(Context context, ListView origin) { super(context); ListView.LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); FrameLayout frameLayout = new FrameLayout(getContext()); frameLayout.addView(mHeaderView); frameLayout.setLayoutParams(layoutParams); addHeaderView(frameLayout); // 将下拉视图添加成ListView的头部视图 setId(origin.getId()); } // 与InternalScrollView的处理基本一样 public void dispatchTouchEvent(MotionEvent e) { .... } public boolean isFirstAtTop() { // 判定ListView头部没有内容被卷起 if (getChildCount() < 2) { return false; } View view = getChildAt(1); // 第二个View,其实就是第一个用户内容View并且展示的是第一条用户数据 return view.getTop() < mTouchSlop && getFirstVisiblePosition() <= 1; } } 

RecycleView下拉刷新

RecyclerView是Android Design包中提供的用于替换ListView和GridView等动态视图的控件,通过设置不同的LayoutManager对象就可以实现展示成ListView样式还是GridView样式,这里仅仅讨论ListView样式展示的RecyclerView的下拉刷新实现。RecyclerView自带了ViewHolder机制实现,但不包含添加头部视图和底部视图的功能,想要像ListView那样通过添加头部视图来实现下拉刷新就需要先实现RecyclerView的头部和底部视图添加功能。

// InternalRecyclerView实现代码 private class InternalRecyclerView extends BaseRecyclerView { private boolean mIsDragging = false; private View mHeaderView; public InternalRecyclerView(Context context, RecyclerView origin) { super(context); setId(origin.getId()); setLayoutManager(new LinearLayoutManager(context)); mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_header, this, false); addHeaderView(mHeaderView); } // 与InternalScrollView的处理基本一样 public void dispatchTouchEvent(MotionEvent e) { .... } public boolean isFirstAtTop() { return !canScrollVertically(-1); } } 
// HeaderWrapperAdapter实现代码 public class HeaderWrapperAdapter extends RecyclerView.Adapter 
  
    { private List 
   
     mHeaders; // HeaderView列表 private List 
    
      mFooters; // FooterView列表 private BaseRecyclerAdapter 
     
       mAdapter; // 用户Adapter对象 private static final int HEADER_VIEW_TYPE = 0x8888; // HeaderView类型 private static final int FOOTER_VIEW_TYPE = 0x9999; // FooterView类型 HeaderWrapperAdapter(List 
      
        headers, List 
       
         footers, BaseRecyclerAdapter adapter) { this.mHeaders = headers; this.mFooters = footers; this.mAdapter = adapter; } @Override public BaseRecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { int realType = getType(viewType), position = getPosition(viewType); if (realType == HEADER_VIEW_TYPE) { return new HeaderViewHolder(mHeaders.get(position)); } else if (realType == FOOTER_VIEW_TYPE) { return new HeaderViewHolder(mFooters.get(position – mAdapter.getItemCount() - mHeaders.size())); } else { return mAdapter.onCreateViewHolder(viewGroup, realType); } } @Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作 public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder, int position) { if (position >= mHeaders.size() && position < mHeaders.size() + mAdapter.getItemCount()) { mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size()); } } @Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作 public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder, int position, @NonNull List 
         payloads) { if (position >= mHeaders.size() && position < mHeaders.size() + mAdapter.getItemCount()) { mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size()); } } @Override public int getItemCount() { // RecyclerView内的元素个数,头视图、尾视图和用户视图总个数 return mHeaders.size() + mAdapter.getItemCount() + mFooters.size(); } @Override public int getItemViewType(int position) { // 根据position决定视图类型 if (position < mHeaders.size()) { return makeTypePos(HEADER_VIEW_TYPE, position); } else if (position < mHeaders.size() + mAdapter.getItemCount()) { return makeTypePos(mAdapter.getItemViewType( position - mHeaders.size()), position); } else { return makeTypePos(FOOTER_VIEW_TYPE, position); } } private int makeTypePos(int type, int pos) { // viewType和position绑定到int中 return (type << 16) + pos; } private int getType(int typePos) { // 从int高16为获取viewType return typePos >>> 16; } private int getPosition(int typePos) { // 从int低16位获取position return typePos & 0xffff; } private static final class HeaderViewHolder extends BaseRecyclerViewHolder { HeaderViewHolder(@NonNull View itemView) { super(itemView); } } }  
        
       
      
     
    
  
// 支持Header和Fooer的BaseRecyclerView实现代码 public class BaseRecyclerView extends RecyclerView { private List 
  
    mHeaders = new ArrayList<>(); private List 
   
     mFooters = new ArrayList<>(); private HeaderWrapperAdapter mWrapperAdapter; private Adapter mAdapter; @Override public void setAdapter(@Nullable Adapter adapter) { if (adapter instanceof BaseRecyclerAdapter) { mAdapter = adapter; mWrapperAdapter = new HeaderWrapperAdapter(mHeaders, mFooters, (BaseRecyclerAdapter) adapter); super.setAdapter(mWrapperAdapter); } else { super.setAdapter(adapter); } } public void addHeaderView(View headerView) { if (mWrapperAdapter != null) { mWrapperAdapter.notifyItemInserted(mHeaders.size() - 1); } } public void removeHeaderView(View headerView) { int index = mHeaders.indexOf(headerView); if (index < 0) { return; } mHeaders.remove(headerView); if (mWrapperAdapter != null) { mWrapperAdapter.notifyItemRemoved(index); } } // 省略底部视图添加、删除代码 } 
    
  

代码中BaseRecyclerView继承自RecyclerView同时覆盖了setAdapter()方法,在设置适配器时会自动将BaseRecyclerAdapter封装到HeaderWrapperAdapter中,当用户调用addHeaderView()方法时实际上会把HeaderView添加到HeaderWrapperAdapter中,此时只要调用notifyItemInserted()就能够将添加的HeaderView展示出来。BaseRecyclerView通过addHeaderView() 添加下拉刷新的HeaderView后,在下拉刷新中使用canScrollVertical()判定顶部没有卷起内容,其他的用户事件处理与ScrollView基本相同,这样RecyclerView就实现了下拉刷新功能。

下拉刷新组件Demo

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/213321.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午6:06
下一篇 2026年3月18日 下午6:07


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号