Android添加自定义下拉刷新布局阻尼滑动悬停弹动画效果

目录
  • Android 对现有布局添加下拉刷新
  • 一、简述
    • 1、下拉阶段
    • 2、下拉松手阶段
  • 二、现有布局
  • 三、添加下拉刷新
    • 1、一个响应下拉操作的父容器控件
      • (1)onInterceptTouchEvent
      • (2)onTouchEvent
    • 2、下拉刷新头部区域
    • 3、将下拉刷新头部 及 内容区域 引入到 响应下拉操作的父容器控件中
    • 4、回弹悬停动画
    • 5、回弹到顶部的动画
    • 6、在某些时机下,进行回调
  • 四、遇到的问题
  • 如何解决呢

Android 对现有布局添加下拉刷新

先直接上效果,如下GIF所示

一、简述

对现有布局添加一个下拉刷新,并且这个动画的效果如上GIF所示

1、下拉阶段

下拉过程中,有阻尼滑动效果

2、下拉松手阶段

(1)、进行高度判断,若大于指定的高度后,先回弹到指定的高度后,做悬停动画效果,再然后做回弹动画回弹到原始位置

(2)、若没有大于指定的高度,则直接回弹到原始位置

(3)刷新的时机,可以自由选择,例如在松手时,即发起刷新逻辑。

二、现有布局

如前面的GIF所示,蓝色区域是内容区域,即是添加下拉刷新前的现有布局

三、添加下拉刷新

从GIF图可以看出,添加下拉刷新,需要两个控件:一个响应下拉操作的父容器控件、一个是刷新头部控件

下拉刷新的主要思路:

页面布局:将响应下拉操作的父容器控件包裹红色下拉刷新头部区域 和 蓝色内容区域,其中蓝色内容区域覆盖在红色下拉刷新头部区域的上面。

下拉操作:下拉时,动态地改变红色下拉刷新头部区域的高度,以及动态改变蓝色内容区域的marginTop值

然后,就是动画操作,也是动态地改变红色下拉刷新头部区域的高度 和 蓝色内容区域的marginTop值。

1、一个响应下拉操作的父容器控件

为写起来简单,直接继承RelativeLayout,重点重写onInterceptTouchEvent 和 onTouchEvent方法。

(1)onInterceptTouchEvent

拦截事件方法:

首先,判断该事件是否需要拦截;

然后,若拦截该事件:在down事件时,将之前操作红色下拉刷新头部区域 及 蓝色内容区域都重置下

然后,在move事件时,判断当前移动的距离是否 > mTouchSlop(表示滑动的最小距离) ,当大于时,认为此时产生了拖拽滑动

最后,在up\cancel事件时,将拖拽标志 重置回来

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (不拦截事件的判断条件) {
        return false;
    }
    if (若此时正在执行动画,则拦截该事件) {
        return true;
    }
    final int action = event.getActionMasked();//获取触控手势
    switch (action) {
    case MotionEvent.ACTION_DOWN:
        // 重置操作
        updateHeightAndMargin(0);
        mIsDragging = false;
        // 手指按下的距离
        this.mDownY = event.getY();
        break;
    case MotionEvent.ACTION_MOVE:
        final float y = event.getY();
        final float yDiff = y - this.mDownY;
        if (yDiff > mTouchSlop) {
            //判断是否时产生了拖拽
            mIsDragging = true;
        }
        break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
        mIsDragging = false;
        break;
    default:
        break;
    }
    return mIsDragging;
}

(2)onTouchEvent

触摸事件处理方法:

若此时没有发生拖拽,或者此时正在动画中: 不处理该事件

当在move事件时:计算阻尼滑动距离,然后更新给红色的下拉刷新头部区域 及 蓝色的内容区域

当在up/cancel事件时: 开启动画逻辑

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!mIsDragging || mIsAnimation) {
        return super.onTouchEvent(event);
    }
    //获取触控手势
    final int action = event.getActionMasked();
    switch (action) {
    case MotionEvent.ACTION_MOVE: {
        //获取移动距离
        float eventY = event.getY();
        float yDiff = eventY - mDownY;
        float scrollTop = yDiff * 0.5;
        //计算实际需要被拖拽产生的移动百分比
        mDragPercent = scrollTop / mDp330;
        if (mDragPercent < 0) {
            return false;
        }
        //计算阻尼滑动的距离
        int targetY = (int) (computeTargetY(scrollTop, mDragPercent, mDp330) + 0.5f);
        updateHeightAndMargin(targetY);
        break;
    }
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL: {
        final float upDiffY = event.getY() - mDownY;
        final float overScrollTop = upDiffY * DEFAULT_DRAG_RATE;
        mIsDragging = false;
        if (overScrollTop > mDp54) {
            animateToHover();
        } else {
            animateToPeak();
        }
        mExtraDrag = 0;
        mPullRefreshBehavior.onUp();
        return false;
    }
    default:
        break;
    }
    return true;
}

阻尼滑动的计算方式:

/*计算阻尼滑动距离*/
public int computeTargetY(float scrollTop, float dragPercent, float maxDragDistance) {
    float boundedDragPercent = Math.min(1.0f, Math.abs(dragPercent));
    float extraOS = Math.abs(scrollTop) - maxDragDistance;
    float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, maxDragDistance * 2) / maxDragDistance);
    float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f;
    float extraMove = (maxDragDistance) * tensionPercent / 2;
    return (int) ((maxDragDistance * boundedDragPercent) + extraMove);
}

更新红色头部区域(mPullRefreshHeadView)高度 及 蓝色的内容区域(mTarget)

private void updateHeightAndMargin(int offsetTop) {
    if (mPullRefreshHeadView == null || mTarget == null) {
        return;
    }
    // 更新下拉刷新的头部高度
    ViewGroup.LayoutParams headViewLayoutParams = mPullRefreshHeadView.getLayoutParams();
    if (headViewLayoutParams != null) {
        headViewLayoutParams.height = Math.max(offsetTop, mDp54);
    }
    // 更新 mTarget view 的 topMargin
    MarginLayoutParams targetLayoutParams = (MarginLayoutParams) mTarget.getLayoutParams();
    if (targetLayoutParams != null) {
        targetLayoutParams.topMargin = offsetTop;
    }
    mOffsetTop = offsetTop;
    mPullRefreshBehavior.onMove(mOffsetTop);
    // 刷新界面
    requestLayout();
}

2、下拉刷新头部区域

这里可以根据自己的需求去构建下拉刷新头部区域的布局,例如添加Lottie动画等

代码示例,是比较简单的一个 Textview + 背景展示下

public class PullRefreshHeadView extends RelativeLayout {
    private View mHeaderView;
    public PullRefreshHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        Resources resources = context.getResources();
        mHeaderView =  LayoutInflater.from(context).inflate(R.layout.vivoshop_classify_pull_refresh_head, this, false);
        LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, context.getResources().getDimensionPixelSize(R.dimen.dp54));
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
        params.bottomMargin = resources.getDimensionPixelSize(R.dimen.dp9);
        addView(mHeaderView, params);
    }
}

3、将下拉刷新头部 及 内容区域 引入到 响应下拉操作的父容器控件中

布局:响应下拉操作的父容器控件包裹着下拉刷新头部及内容区域

<?xml version="1.0" encoding="utf-8"?>
<com.qlli.pulllayout.PullRefreshLayout
    android:id="@+id/pull_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:background="@color/teal_700">
      <com.qlli.pulllayout.PullRefreshHeadView
          android:id="@+id/pull_header"
          android:layout_width="match_parent"
          android:layout_height="@dimen/dp54"
          android:background="@color/red"/>
      <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:background="@color/color_415fff"
          android:gravity="center"
          android:clickable="true">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/white"
                android:textSize="20sp"
                android:text="这里是内容区域, 下拉试试看"/>
      </RelativeLayout>
</com.qlli.pulllayout.PullRefreshLayout>

在响应下拉操作的父容器控件初始化时,在onFinishInflate中将下拉刷新头部、内容区域分别进行赋值

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    ensureTargetView();
}
//寻找需要控制滑动的内容区域的父容器
private void ensureTargetView() {
    if (mTarget != null || getChildCount() <= 0) {
        return;
    }
    for (int index = 0; index < getChildCount(); index++) {
        View child = getChildAt(index);
        if (child instanceof PullRefreshHeadView) {
            mPullRefreshHeadView = (PullRefreshHeadView) child;
            continue;
        }
        if (child != mPullRefreshHeadView) {
            mTarget = child;
            break;
        }
    }
}

4、回弹悬停动画

回弹悬停动画是指:先回弹到指定位置,然后开始悬停一段时间后,再开启一个新的动画

回弹动作:是指将 下拉刷新头部 及 内容区域 回弹至指定位置,可以在一个时间段中,通过监听0到100变化的,进而动态计算改变下拉刷新头部及内容区域的高度并更新

悬停动作:在回弹结束后,其实此时悬停是指回弹动画结束后,就保持当前位置不动了,此时使用Handler发一个延时任务去执行 一个新的回弹动画(将下拉刷新及内容区域回弹至原始位置),这个中间的过程给出的视觉效果是一个悬停的效果

private ValueAnimator mHoverAnimator;//回弹悬停动画
private final Handler mHoverHandler = new Handler(Looper.getMainLooper());
private void animateToHover() {
    // 这里是内容区域marginTop的距离
    final int startPosition = mOffsetTop;
    // 这里是动画结束的位置,要保留一个下拉刷新头部高度距离
    final int totalDistance = startPosition - mDp54;
    // 设置悬停动画的一些初始化东西
    if (mHoverAnimator == null) {
        mHoverAnimator = ValueAnimator.ofFloat(0f, 100f);
        mHoverAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    } else {
        mHoverAnimator.removeAllUpdateListeners();
        mHoverAnimator.removeAllListeners();
        mHoverAnimator.end();
    }
    // 在动画监听过程中,通过updateHeightAndMargin移动下拉刷新及内容区域的距离
    mHoverAnimator.addUpdateListener(animation -> {
        Object value = animation.getAnimatedValue();
        if (value instanceof Float) {
            float percent = ((float) value) / 100f;
            int targetTop = startPosition - (int) (totalDistance * percent);
            updateHeightAndMargin(targetTop);
        }
    });
    // 监听此动画开始 和 结束点
    mHoverAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            mIsAnimation = true;
        }
        // 在该动画结束后,在1.6s后,做一个回弹动画,因此在1.6s的时间内就是一个悬停效果
        // 可以在这个悬停的期间干些事情,例如播放Lottie动画等
        @Override
        public void onAnimationEnd(Animator animation) {
            mHoverHandler.removeCallbacksAndMessages(null);
            mHoverHandler.postDelayed(() -> {
                if (isAttachedToWindow()) {
                    // 例如在这个播放Lottie动画
                    ensureTargetView();
                    // 回弹动画
                    animateToPeak();
                }
            }, 1600);
        }
    });
    // 此动画设置一下时间
    float animationPercent = Math.min(1.0f, Math.abs(totalDistance) * 1.0f / mDp54);
    long duration = Math.abs((long) (ANIMATION_DURATION_300 * animationPercent));
    mHoverAnimator.setDuration(duration);
    mHoverAnimator.start();
}

5、回弹到顶部的动画

这个回弹到顶部的操作是指:将下拉刷新头部 及 内容区域 在一定时间内 回到顶部

private ValueAnimator mPeakAnimator;//回弹动画
private void animateToPeak() {
    float startDragPercent = mDragPercent;
    //松手后开始从此位置滑动
    final int totalDistance = mOffsetTop;
    if (mPeakAnimator == null) {
        mPeakAnimator = ValueAnimator.ofFloat(0f, 100f);
        mPeakAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    } else {
        mPeakAnimator.removeAllListeners();
        mPeakAnimator.removeAllUpdateListeners();
        mPeakAnimator.end();
    }
    mPeakAnimator.addUpdateListener(animation -> {
        Object value = animation.getAnimatedValue();
        if (value instanceof Float) {
            float percent = ((float) value) / 100f;
            int targetTop = (int) (totalDistance * (1.0f - percent));
            updateHeightAndMargin(targetTop);
        }
    });
    mPeakAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            mIsAnimation = true;
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            mIsAnimation = false;
            updateHeightAndMargin(0);
        }
    });
    float ratio = Math.abs(startDragPercent);
    // 滑动到顶部的时间
    mPeakAnimator.setDuration((long) (800 * ratio));
    mPeakAnimator.start();
}

6、在某些时机下,进行回调

可以结合自己的需求写一个接口,例如下面这样:

public interface PullRefreshBehavior {
    // 移动的高度
    void onMove(int height);
    // 手指抬起
    void onUp();
    // 悬停
    void onHover();
    // 回弹
    void onSpringBack();
    // 完成
    void onComplete();
}

然后在下拉操作的过程中 去选择性地调用 上面接口中的方法,这样在实现该接口的具体实现类中,就能根据当前下拉操作的不同时机来去做一些想做的事情

四、遇到的问题

  • 1、在下拉操作时,在onInterceptTouchEvent方法时仅响应down事件,move事件不响应

导致该问题的主要原因是:响应下拉操作的父容器内包裹的子控件没有消耗down事件,所以后续收不到move事件

  • 2、看下ViewGroup中的事件分发这段代码

可以看到下面代码中: 是down事件,或者 mFirstTouchTarget != null

若父容器包裹的子控件没有消耗down事件,则mFirstTouchTarget == null,那么当move事件到来是,即不满足条件,则不会调用到 onInterceptTouchEvent方法。

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

如何解决呢

在子控件中,加一个消耗down事件的操作即可,例如在子控件布局中,添加一个clickable属性为 true 即可

因为可点击事件,是消耗down事件的

 <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:background="@color/color_415fff"
          android:gravity="center"
          android:clickable="true">

以上就是Android添加自定义下拉刷新布局阻尼滑动悬停弹动画效果的详细内容,更多关于Android添加下拉刷新布局的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android布局控件View ViewRootImpl WindowManagerService关系

    目录 1. View,ViewRoot和WindowManager简单介绍 1.1 View和ViewGroup 1.2 ViewRootImpl 1.3 WindowManager 2. ViewRootImpl的起源 2.1 ViewRootImpl创建时机 2.2 ViewRootImpl通知注册Window 3.ViewRootImpl与WindowManagerService的通信 3.1 WindowSession 3.2 IWindow 4. ViewRootImpl与View 1

  • Android修行手册之ConstraintLayout布局使用详解

    目录 实践过程 示例一 示例二 实践过程 近期创建的项目默认是带有的,如果没有去build.gradle文件中查看有没有引入 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 截止2022年8月最新版是2.1.4 示例一 想要实现这个效果: 使用RelativeLayout无法实现,更不要说其他的了.哪怕红块底部对齐利用margin负数的形式做出来了,但是这就得是提前固定宽高,可固定高度又得考虑适配的事,或者干脆这

  • Android嵌套线性布局玩法坑解决方法

    目录 前言 详解 为什么会让性能降低的怎么严重呢? 前言 嵌套线性布局大家应该都用的非常熟悉,毕竟这玩意理解起来也是真的简单,而且如果熟悉的话这玩意开发起来的效率也是真的快,不用一下一下拖动. 但是这个玩意有个非常的问题,就是性能问题,而且人家性能问题是指数级别增加的,怎么回事呢,因为你如果一层一层的嵌套布局的话,系统在绘制的时候就是指数级别的绘制次数,如果你只是嵌套了俩层那都还能接受的玩,如果你一个界面控件很多,然后你又嵌套几层线性布局,那这个时候性能就十分低下了. 详解 看下面的代码,就是一

  • Android布局ConstraintLayout代码修改约束及辅助功能

    目录 实践过程 代码修改约束 辅助功能 Guideline Barrier Flow Placeholder Group和Layer 实践过程 代码修改约束 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/an

  • Android进阶CoordinatorLayout协调者布局实现吸顶效果

    目录 引言 1 CoordinatorLayout功能介绍 1.1 CoordinatorLayout的依赖交互原理 1.2 CoordinatorLayout的嵌套滑动原理 2 CoordinatorLayout源码分析 2.1 CoordinatorLayout的依赖交互实现 2.2 CoordinatorLayout交互依赖的源码分析 2.3 CoordinatorLayout子控件拦截事件源码分析 2.4 CoordinatorLayout嵌套滑动原理分析 引言 在上一节Android进

  • Android使用ViewStub实现布局优化方法示例

    目录 实践过程 实现方式 知识点 实践过程 Hello,大家好啊,我是小空,今天带大家了解下动态加载控件ViewStub. 在平时开发中经常会遇到复杂布局,而每一个view都是会占据内存和消耗cpu的(即使再小,累计成多,一般嵌套7级以上就有明显的卡顿了),布局优化就是我们常做的任务之一,甚至是一块心病.所以我们工作中就要留意布局优化的手段,ViewStub就是其中之一. 大家应该听过merge标签,将某个布局文件的根布局写成merge的,然后对应的布局include引用,会默认不会引入merg

  • Android:下拉刷新+加载更多+滑动删除实例讲解

    小伙伴们在逛淘宝或者是各种app上,都可以看到这样的功能,下拉刷新和加载更多以及滑动删除,刷新,指刷洗之后使之变新,比喻突破旧的而创造出新的,比如在手机上浏览新闻的时候,使用下拉刷新的功能,我们可以第一时间掌握最新消息,加载更多是什么nie,简单来说就是在网页上逛淘宝的时候,我们可以点击下一页来满足我们更多的需求,但是在手机端就不一样了,没有上下页,怎么办nie,方法总比困难多,细心的小伙伴可能会发现,在手机端中,有加载更多来满足我们的要求,其实加载更多也是分页的一种体现.小伙伴在使用手机版QQ

  • Android RefreshLayout实现下拉刷新布局

    项目中需要下拉刷新的功能,但是这个View不是ListView这类的控件,需要ViewGroup实现这个功能,一开始网上大略找了一下,没发现特别合适的,代码也是没怎么看懂,所以决定还是自己写一个. 于是翻出XlistView的源码看是一点一点看,再大致理解了XLisview源码,终于决定自己动手啦 为了省事,headView还是用了XListView的HeadView,省了很多事:) 下拉刷新,下拉刷新,肯定是先实现下拉功能,最开始我是打算通过 extends ScrollView 来实现,因为

  • Android SwipereFreshLayout下拉刷新

    Android SwipereFreshLayout下拉刷新 我们都知道现在android5.0以后就提倡使用Material Design设计了.在Material Design设计就有一个非常好的设计SwipereFreshLayout,下面我们就来看看它的使用.既然它来源于Material Design,我们第一步就应该是添加它的库. 1.我们就在build.gradle添加库: compile 'com.android.support:support-v4:22.1.1' 2.然后我们就

  • Android ListView下拉刷新上拉自动加载更多DEMO示例

    代码下载地址已经更新.因为代码很久没更新,已经很落伍了,建议大家使用RecyclerView实现. 参考项目: https://github.com/bingoogolapple/BGARefreshLayout-Android https://github.com/baoyongzhang/android-PullRefreshLayout 下拉刷新,Android中非常普遍的功能.为了方便便重写的ListView来实现下拉刷新,同时添加了上拉自动加载更多的功能.设计最初是参考开源中国的And

  • Android自定义下拉刷新上拉加载

    本文实例为大家分享了Android自定义下拉刷新上拉加载的具体实现步骤,供大家参考,具体内容如下 实现的方式是SwipeRefreshLayout + RecyclerView 的VIewType 首先看效果: 总的思路: 布局文件 <android.support.v4.widget.SwipeRefreshLayout android:layout_marginTop="?attr/actionBarSize" android:id="@+id/one_refres

  • Android自定义控件下拉刷新实例代码

    实现效果: 图片素材: --> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.xml: <resources> <string name="app_name">PullToRefreshTest</string> <string name="pull_to_refresh">下拉可以刷新</string> <string name="release_to_refre

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

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

  • Android RecyclerView下拉刷新和上拉加载更多

    今天终于有点时间,来写了一下: 为RecyclerView实现下拉刷新和上拉加载更多.今天会在前面的两篇文章的基础上: RecyclerView系列之(1):为RecyclerView添加Header和Footer RecyclerView系列之(2):为RecyclerView添加分隔线 继续讲述RecyclerView中一些常用组件的实现下拉刷新和上拉加载更多的功能. 在现在的Android手机应用中,几乎每一个APP都有下拉刷新和上拉加载更多的功能,它们的重要性不言而喻. 先不多说,先看效

  • Android SwipeRefreshLayout下拉刷新组件示例

    SwipeRefreshLayout概述 SwipeRefrshLayout是Google官方更新的一个Widget,可以实现下拉刷新的效果.该控件集成自ViewGroup在support-v4兼容包下,不过我们需要升级supportlibrary的版本到19.1以上. 用户通过手势或者点击某个按钮实现内容视图的刷新,布局里加入SwipeRefreshLayout嵌套一个子视图如ListView. RecyclerView等,触发刷新会通过OnRefreshListener的onRefresh方

  • Android  RecyclerView下拉刷新和上拉加载更多

    今天终于有点时间,来写了一下: 为RecyclerView实现下拉刷新和上拉加载更多.今天会在前面的两篇文章的基础上: RecyclerView系列之(1):为RecyclerView添加Header和Footer RecyclerView系列之(2):为RecyclerView添加分隔线 继续讲述RecyclerView中一些常用组件的实现下拉刷新和上拉加载更多的功能. 在现在的Android手机应用中,几乎每一个APP都有下拉刷新和上拉加载更多的功能,它们的重要性不言而喻. 先不多说,先看效

随机推荐