RecyclerView更全解析之 - 打造通用的下拉刷新上拉加载

红橙Darren
• 阅读 1552

1.概述


这期我们在上一期的RecyclerView更全解析之 - 为它优雅的添加头部和底部的基础上再去增加功能,我相信我们在真正的实践开发过程中肯定少不了下拉刷新和上拉加载。
  我们需要思考一个问题上拉刷新下拉加载风格各式各样,淘宝和京东的列表刷新样式就肯定不一样,我们怎么样做到版本迭代的时候可以快速的更改样式。有时还需要显示正在加载数据或者无数据,比如筛选的时候有可能会出现没有数据的情况会显示无数据页面,怎么快速做到?当然如果你对系统架构比较了解那就非常简单了,又或者是你对面向对象的六大基本原则比较熟悉也行。

相关文章:
  
  RecyclerView更全解析之 - 基本使用和分割线解析
  
  RecyclerView更全解析之 - 打造通用的万能Adapter
  
  RecyclerView更全解析之 - 为它优雅的添加头部和底部
  
  RecyclerView更全解析之 - 打造通用的下拉刷新上拉加载
  
  RecyclerView更全解析之 - 仿支付宝侧滑删除和拖动排序         

RecyclerView更全解析之 - 打造通用的下拉刷新上拉加载

这里写图片描述

2.基本思路


我们在写项目或是搭建架构的时候需要考虑最多的是扩展,而不是先把所有的功能写在一起或是全部写好,或者说代码过度设计本来很简单的东西你非得跟人解释这怎么怎么的,很忌讳。
  肯定是希望目前写好的东西,以后如果出现什么问题或者添加新的功能都不需要去修改我们已经写好的代码,而是在原来的基础上利用面向对象的思想去扩展无论你是继承也好还是实现也好都行,就不会出现需求改变的时候我们的代码就改成了别人口中说的改成了......
  
  本着这个原则我们大致的思想就是:

  • 先处理下拉刷新,同时考虑刷新列表的不同风格样式,确保这个项目还是下一个项目都能用
  • 再处理上拉加载更多,只需去继承写好的下拉刷新控件即可
  • 可以适当的增加一些基本功能,如正在加载列表样式或者说是无页面数据样式
  • 封装通用默认的样式,封装好整个项目的通用样式,如果下次需要修改扩展即可
  • 最后思考一下我们这样去写合不合理,给自己的同事用用自己和他们都做一下测评和修改

3.基本实现


3.1 下拉刷新
  先处理下拉刷新,同时考虑刷新列表的不同风格样式,确保这个项目还是下一个项目都能用。这里我们肯定是继承上一期的可以直接添加头部和底部的WrapRecyclerView,为了确保实现不同的样式,需要一个额外的辅助类:

/**
 * Created by Darren on 2017/1/3.
 * Email: 240336124@qq.com
 * Description: 下拉刷新的辅助类为了匹配所有效果
 */

public abstract class RefreshViewCreator {

    /**
     * 获取下拉刷新的View
     *
     * @param context 上下文
     * @param parent  RecyclerView
     */
    public abstract View getRefreshView(Context context, ViewGroup parent);

    /**
     * 正在下拉
     * @param currentDragHeight   当前拖动的高度
     * @param refreshViewHeight  总的刷新高度
     * @param currentRefreshStatus 当前状态
     */
    public abstract void onPull(int currentDragHeight, int refreshViewHeight, int currentRefreshStatus);

    /**
     * 正在刷新中
     */
    public abstract void onRefreshing();

    /**
     * 停止刷新
     */
    public abstract void onStopRefresh();
} 

/**

  • Created by Darren on 2017/1/3.

  • Email: 240336124@qq.com

  • Description: 下拉刷新的RecyclerView

  • / public class RefreshRecyclerView extends WrapRecyclerView { // 下拉刷新的辅助类 private RefreshViewCreator mRefreshCreator; // 下拉刷新头部的高度 private int mRefreshViewHeight = 0; // 下拉刷新的头部View private View mRefreshView; // 手指按下的Y位置 private int mFingerDownY; // 手指拖拽的阻力指数 private float mDragIndex = 0.35f; // 当前是否正在拖动 private boolean mCurrentDrag = false; // 当前的状态 private int mCurrentRefreshStatus; // 默认状态 public int REFRESH_STATUS_NORMAL = 0x0011; // 下拉刷新状态 public int REFRESH_STATUS_PULL_DOWN_REFRESH = 0x0022; // 松开刷新状态 public int REFRESH_STATUS_LOOSEN_REFRESHING = 0x0033; // 正在刷新状态 public int REFRESH_STATUS_REFRESHING = 0x0033;

    public RefreshRecyclerView(Context context) {

      super(context);

    }

    public RefreshRecyclerView(Context context, @Nullable AttributeSet attrs) {

      super(context, attrs);

    }

    public RefreshRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {

      super(context, attrs, defStyle);

    }

    // 先处理下拉刷新,同时考虑刷新列表的不同风格样式,确保这个项目还是下一个项目都能用 // 所以我们不能直接添加View,需要利用辅助类 public void addRefreshViewCreator(RefreshViewCreator refreshCreator) {

      this.mRefreshCreator = refreshCreator;
      addRefreshView();

    }

    @Override public void setAdapter(Adapter adapter) {

      super.setAdapter(adapter);
      addRefreshView();

    }

    @Override public boolean dispatchTouchEvent(MotionEvent ev) {

      switch (ev.getAction()) {
          case MotionEvent.ACTION_DOWN:
              // 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,
              // 那么就不会进入onTouchEvent里面,所以只能在这里获取
              mFingerDownY = (int) ev.getRawY();
              break;
    
          case MotionEvent.ACTION_UP:
              if (mCurrentDrag) {
                  restoreRefreshView();
              }
              break;
      }
      return super.dispatchTouchEvent(ev);

    }

    /**

    • 重置当前刷新状态状态

    • / private void restoreRefreshView() { int currentTopMargin = ((MarginLayoutParams) mRefreshView.getLayoutParams()).topMargin; int finalTopMargin = -mRefreshViewHeight + 1; if (mCurrentRefreshStatus == REFRESH_STATUS_LOOSEN_REFRESHING) {

        finalTopMargin = 0;
        mCurrentRefreshStatus = REFRESH_STATUS_REFRESHING;
        if (mRefreshCreator != null) {
            mRefreshCreator.onRefreshing();
        }
        if (mListener != null) {
            mListener.onRefresh();
        }

      }

      int distance = currentTopMargin - finalTopMargin;

      // 回弹到指定位置 ValueAnimator animator = ObjectAnimator.ofFloat(currentTopMargin, finalTopMargin).setDuration(distance); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float currentTopMargin = (float) animation.getAnimatedValue();
            setRefreshViewMarginTop((int) currentTopMargin);
        }

      }); animator.start(); mCurrentDrag = false; }

@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
            // 如果是在最顶部才处理,否则不需要处理
            if (canScrollUp() || mCurrentRefreshStatus == REFRESH_STATUS_REFRESHING) {
                // 如果没有到达最顶端,也就是说还可以向上滚动就什么都不处理
                return super.onTouchEvent(e);
            }

            // 解决下拉刷新自动滚动问题
            if (mCurrentDrag) {
                scrollToPosition(0);
            }

            // 获取手指触摸拖拽的距离
            int distanceY = (int) ((e.getRawY() - mFingerDownY) * mDragIndex);
            // 如果是已经到达头部,并且不断的向下拉,那么不断的改变refreshView的marginTop的值
            if (distanceY > 0) {
                int marginTop = distanceY - mRefreshViewHeight;
                setRefreshViewMarginTop(marginTop);
                updateRefreshStatus(marginTop);
                mCurrentDrag = true;
                return false;
            }
            break;
    }

    return super.onTouchEvent(e);
}

/**
 * 更新刷新的状态
 */
private void updateRefreshStatus(int marginTop) {
    if (marginTop <= -mRefreshViewHeight) {
        mCurrentRefreshStatus = REFRESH_STATUS_NORMAL;
    } else if (marginTop < 0) {
        mCurrentRefreshStatus = REFRESH_STATUS_PULL_DOWN_REFRESH;
    } else {
        mCurrentRefreshStatus = REFRESH_STATUS_LOOSEN_REFRESHING;
    }

    if (mRefreshCreator != null) {
        mRefreshCreator.onPull(marginTop, mRefreshViewHeight, mCurrentRefreshStatus);
    }
}

/**
 * 添加头部的刷新View
 */
private void addRefreshView() {
    RecyclerView.Adapter adapter = getAdapter();
    if (adapter != null && mRefreshCreator != null) {
        // 添加头部的刷新View
        View refreshView = mRefreshCreator.getRefreshView(getContext(), this);
        if (refreshView != null) {
            addHeaderView(refreshView);
            this.mRefreshView = refreshView;
        }
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    if (changed) {
        if (mRefreshView != null && mRefreshViewHeight <= 0) {
            // 获取头部刷新View的高度
            mRefreshViewHeight = mRefreshView.getMeasuredHeight();
            if (mRefreshViewHeight > 0) {
                // 隐藏头部刷新的View  marginTop  多留出1px防止无法判断是不是滚动到头部问题
                setRefreshViewMarginTop(-mRefreshViewHeight + 1);
            }
        }
    }
}

/**
 * 设置刷新View的marginTop
 */
public void setRefreshViewMarginTop(int marginTop) {
    MarginLayoutParams params = (MarginLayoutParams) mRefreshView.getLayoutParams();
    if (marginTop < -mRefreshViewHeight + 1) {
        marginTop = -mRefreshViewHeight + 1;
    }
    params.topMargin = marginTop;
    mRefreshView.setLayoutParams(params);
}


/**
 * @return Whether it is possible for the child view of this layout to
 * scroll up. Override this if the child view is a custom view.
 * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
 */
public boolean canScrollUp() {
    if (android.os.Build.VERSION.SDK_INT < 14) {
        return ViewCompat.canScrollVertically(this, -1) || this.getScrollY() > 0;
    } else {
        return ViewCompat.canScrollVertically(this, -1);
    }
}

/**
 * 停止刷新
 */
public void onStopRefresh() {
    mCurrentRefreshStatus = REFRESH_STATUS_NORMAL;
    restoreRefreshView();
    if (mRefreshCreator != null) {
        mRefreshCreator.onStopRefresh();
    }
}

// 处理刷新回调监听
private OnRefreshListener mListener;

public void setOnRefreshListener(OnRefreshListener listener) {
    this.mListener = listener;
}

public interface OnRefreshListener {
    void onRefresh();
}

}


我们来写一个默认的下拉刷新效果测试一下,这个gif录制软件的效果不是特别给力

/**

  • Created by Darren on 2017/1/3.
  • Email: 240336124@qq.com
  • Description: 默认样式的头部刷新
  • 如淘宝、京东、不同的样式可以自己去实现
  • /

public class DefaultRefreshCreator extends RefreshViewCreator { // 加载数据的ImageView private View mRefreshIv;

@Override
public View getRefreshView(Context context, ViewGroup parent) {
    View refreshView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header_view, parent, false);
    mRefreshIv = refreshView.findViewById(R.id.refresh_iv);
    return refreshView;
}

@Override
public void onPull(int currentDragHeight, int refreshViewHeight, int currentRefreshStatus) {
    float rotate = ((float) currentDragHeight) / refreshViewHeight;
    // 不断下拉的过程中不断的旋转图片
    mRefreshIv.setRotation(rotate * 360);
}

@Override
public void onRefreshing() {
    // 刷新的时候不断旋转
    RotateAnimation animation = new RotateAnimation(0, 720,
            Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    animation.setRepeatCount(-1);
    animation.setDuration(1000);
    mRefreshIv.startAnimation(animation);
}

@Override
public void onStopRefresh() {
    // 停止加载的时候清除动画
    mRefreshIv.setRotation(0);
    mRefreshIv.clearAnimation();
}

}


![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/ec7e9ec914f16117425b57c1ead44b9d.gif)

这里写图片描述

**3.2 处理上拉加载更多**  
  再处理上拉加载更多,只需去继承写好的下拉刷新控件即可。我们的确可以在原来的这个下拉刷新的控件中去写,但是有几个问题都写到一堆出了问题找谁?别人怎么看代码?如果该需求只要下拉刷新呢?说好的扩展。所以我们新写一个控件继承已经写好的下拉刷新控件每个类负责单独的事情

/**

  • Created by Darren on 2017/1/3.
  • Email: 240336124@qq.com
  • Description: 上拉加载更多的辅助类为了匹配所有效果
  • /

public abstract class LoadViewCreator {

/**
 * 获取上拉加载更多的View
 *
 * @param context 上下文
 * @param parent  RecyclerView
 */
public abstract View getLoadView(Context context, ViewGroup parent);

/**
 * 正在上拉
 *
 * @param currentDragHeight    当前拖动的高度
 * @param loadViewHeight    总的加载高度
 * @param currentLoadStatus 当前状态
 */
public abstract void onPull(int currentDragHeight, int loadViewHeight, int currentLoadStatus);

/**
 * 正在加载中
 */
public abstract void onLoading();

/**
 * 停止加载
 */
public abstract void onStopLoad();

}

/**
 * Created by Darren on 2017/1/3.
 * Email: 240336124@qq.com
 * Description: 下拉刷新上拉加载更多的RecyclerView
 */
public class LoadRefreshRecyclerView extends RefreshRecyclerView {
    // 上拉加载更多的辅助类
    private LoadViewCreator mLoadCreator;
    // 上拉加载更多头部的高度
    private int mLoadViewHeight = 0;
    // 上拉加载更多的头部View
    private View mLoadView;
    // 手指按下的Y位置
    private int mFingerDownY;
    // 当前是否正在拖动
    private boolean mCurrentDrag = false;
    // 当前的状态
    private int mCurrentLoadStatus;
    // 默认状态
    public int LOAD_STATUS_NORMAL = 0x0011;
    // 上拉加载更多状态
    public static int LOAD_STATUS_PULL_DOWN_REFRESH = 0x0022;
    // 松开加载更多状态
    public static int LOAD_STATUS_LOOSEN_LOADING = 0x0033;
    // 正在加载更多状态
    public int LOAD_STATUS_LOADING = 0x0044;

    public LoadRefreshRecyclerView(Context context) {
        super(context);
    }

    public LoadRefreshRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public LoadRefreshRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // 先处理上拉加载更多,同时考虑加载列表的不同风格样式,确保这个项目还是下一个项目都能用
    // 所以我们不能直接添加View,需要利用辅助类
    public void addLoadViewCreator(LoadViewCreator loadCreator) {
        this.mLoadCreator = loadCreator;
        addRefreshView();
    }

    @Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(adapter);
        addRefreshView();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,
                // 那么就不会进入onTouchEvent里面,所以只能在这里获取
                mFingerDownY = (int) ev.getRawY();
                break;

            case MotionEvent.ACTION_UP:
                if (mCurrentDrag) {
                    restoreLoadView();
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 重置当前加载更多状态
     */
    private void restoreLoadView() {
        int currentBottomMargin = ((MarginLayoutParams) mLoadView.getLayoutParams()).bottomMargin;
        int finalBottomMargin = 0;
        if (mCurrentLoadStatus == LOAD_STATUS_LOOSEN_LOADING) {
            mCurrentLoadStatus = LOAD_STATUS_LOADING;
            if (mLoadCreator != null) {
                mLoadCreator.onLoading();
            }
            if (mListener != null) {
                mListener.onLoad();
            }
        }

        int distance = currentBottomMargin - finalBottomMargin;

        // 回弹到指定位置
        ValueAnimator animator = ObjectAnimator.ofFloat(currentBottomMargin, finalBottomMargin).setDuration(distance);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentTopMargin = (float) animation.getAnimatedValue();
                setLoadViewMarginBottom((int) currentTopMargin);
            }
        });
        animator.start();
        mCurrentDrag = false;
    }


    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                // 如果是在最底部才处理,否则不需要处理
                if (canScrollDown() || mCurrentLoadStatus == LOAD_STATUS_LOADING) {
                    // 如果没有到达最顶端,也就是说还可以向上滚动就什么都不处理
                    return super.onTouchEvent(e);
                }

                if (mLoadCreator != null) {
                    mLoadViewHeight = mLoadView.getMeasuredHeight();
                }
                // 解决上拉加载更多自动滚动问题
                if (mCurrentDrag) {
                    scrollToPosition(getAdapter().getItemCount() - 1);
                }

                // 获取手指触摸拖拽的距离
                int distanceY = (int) ((e.getRawY() - mFingerDownY) * mDragIndex);
                // 如果是已经到达头部,并且不断的向下拉,那么不断的改变refreshView的marginTop的值
                if (distanceY < 0) {
                    setLoadViewMarginBottom(-distanceY);
                    updateLoadStatus(-distanceY);
                    mCurrentDrag = true;
                    return true;
                }
                break;
        }

        return super.onTouchEvent(e);
    }

    /**
     * 更新加载的状态
     */
    private void updateLoadStatus(int distanceY) {
        if (distanceY <= 0) {
            mCurrentLoadStatus = LOAD_STATUS_NORMAL;
        } else if (distanceY < mLoadViewHeight) {
            mCurrentLoadStatus = LOAD_STATUS_PULL_DOWN_REFRESH;
        } else {
            mCurrentLoadStatus = LOAD_STATUS_LOOSEN_LOADING;
        }

        if (mLoadCreator != null) {
            mLoadCreator.onPull(distanceY, mLoadViewHeight, mCurrentLoadStatus);
        }
    }

    /**
     * 添加底部加载更多View
     */
    private void addRefreshView() {
        Adapter adapter = getAdapter();
        if (adapter != null && mLoadCreator != null) {
            // 添加底部加载更多View
            View loadView = mLoadCreator.getLoadView(getContext(), this);
            if (loadView != null) {
                addFooterView(loadView);
                this.mLoadView = loadView;
            }
        }
    }

    /**
     * 设置加载View的marginBottom
     */
    public void setLoadViewMarginBottom(int marginBottom) {
        MarginLayoutParams params = (MarginLayoutParams) mLoadView.getLayoutParams();
        if (marginBottom < 0) {
            marginBottom = 0;
        }
        params.bottomMargin = marginBottom;
        mLoadView.setLayoutParams(params);
    }


    /**
     * @return Whether it is possible for the child view of this layout to
     * scroll up. Override this if the child view is a custom view.
     * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
     */
    public boolean canScrollDown() {
        return ViewCompat.canScrollVertically(this, 1);
    }

    /**
     * 停止加载更多
     */
    public void onStopLoad() {
        mCurrentLoadStatus = LOAD_STATUS_NORMAL;
        restoreLoadView();
        if (mLoadCreator != null) {
            mLoadCreator.onStopLoad();
        }
    }

    // 处理加载更多回调监听
    private OnLoadMoreListener mListener;

    public void setOnLoadMoreListener(OnLoadMoreListener listener) {
        this.mListener = listener;
    }

    public interface OnLoadMoreListener {
        void onLoad();
    }
} 
```

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/7afe0629bf8679f2445ce3f3ee0562c1.gif)

这里写图片描述

**3.3 增加一些基本通用功能**

最后我们在这个基础在增加一些基本的功能,如正在加载数据的页面,或者数据是空的页面,所以决定找一层最合适的方法去改,那就是我们上一期的WrapRecyclerView的基础上去改,因为那是我们Adapter密切联系的一层。

```
/**
 * Created by Darren on 2016/12/29.
 * Email: 240336124@qq.com
 * Description: 可以添加头部和底部的RecyclerView
 */
public class WrapRecyclerView extends RecyclerView {
    // 增加一些通用功能
    // 空列表数据应该显示的空View
    // 正在加载数据页面,也就是正在获取后台接口页面
    private View mEmptyView, mLoadingView;

    // 省略...上一期已有代码

    private AdapterDataObserver mDataObserver = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyDataSetChanged没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyDataSetChanged();

            dataChanged();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyDataSetChanged没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemRemoved(positionStart);
            dataChanged();
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemMoved没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemMoved(fromPosition, toPosition);
            dataChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemChanged没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemChanged(positionStart);
            dataChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemChanged没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemChanged(positionStart, payload);
            dataChanged();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 观察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemInserted没效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemInserted(positionStart);
            dataChanged();
        }
    };

    /**
     * 添加一个空列表数据页面
     */
    public void addEmptyView(View emptyView) {
        this.mEmptyView = emptyView;
    }

    /**
     * 添加一个正在加载数据的页面
     */
    public void addLoadingView(View loadingView) {
        this.mLoadingView = loadingView;
    }

    /**
     * Adapter数据改变的方法
     */
    private void dataChanged() {
        if (mAdapter.getItemCount() == 0) {
            // 没有数据
            if (mEmptyView != null) {
                mEmptyView.setVisibility(VISIBLE);
            } else {
                mEmptyView.setVisibility(GONE);
            }
        }
    }
    // 省略...上一期已有代码
} 
```

上一期的代码就已经省略了[RecyclerView更全解析之 - 为它优雅的添加头部和底部](https://link.jianshu.com/?t=http://blog.csdn.net/z240336124/article/details/5393925081),到目前应该所有的这些列表刷新和加载样式都可以实现,具体的一些要求可以自己修改修改。我这里就不在把它使用到具体的项目中了,我自己也用到了自己的项目中,之所以之前没写这一期的博客是因为在使用的过程中出现了一些Bug,所以才等到这个时候。

![](https://img-hello-world.oss-cn-beijing.aliyuncs.com/e4289e2972781ff2643f2c6c5a9d02d7.gif)

这里写图片描述

所有分享大纲:[Android进阶之旅 - 自定义View篇](https://www.jianshu.com/p/b272528165a2)

视频讲解地址:[http://pan.baidu.com/s/1kUGvvwj](https://link.jianshu.com/?t=http://pan.baidu.com/s/1kUGvvwj)

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Taro下拉刷新,上拉加载更多
1、引入插件importTaro,{Component}from'@tarojs/taro'import{View,Text,ScrollView}from'@tarojs/components'import{AtActivityIndicator}from'taroui'imp
Stella981 Stella981
2年前
Django之Django模板
1、问:html页面从数据库中读出DateTimeField字段时,显示的时间格式和数据库中存放的格式不一致,比如数据库字段内容为2012082616:00:00,但是页面显示的却是Aug.26,2012,4p.m.答:为了页面和数据库中显示一致,需要在页面格式化时间,需要添加<td{{dayrecord.p\_time|date:
Stella981 Stella981
2年前
Android如何实现一个上拉刷新下拉加载的ListView
20191220关键字:自定义上下拉ListView在APK开发中,一个具备在列表顶部下拉刷新、在列表尾部上拉加载功能的ListView的需求还是比较多的。具备这种功能的优秀开源代码同样也有很多。但今天,笔者就非要自己实现一个这样的控件不可。以下是成品效果图:!(https://oscimg.oschin
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这