Android自定义实现PullToRefreshRecycleView刷新加载控件原理和使用

写在前头:PullToRefresh这个下拉刷新上拉加载更多的控件相信大家并不陌生,Github上搜索也有很多相关的控件,并且star数量也不低,但是呢如果你想拥有很高的自由度的话,那么还是敌不过自己实现一个或者你能把开源的控件源码看明白然后基于上面自己修改成自己需要的样式,那么我就是干了这件事,在网上看到一篇博客,然后下载源码看了看,然后就想将它进行改造一番,那篇文章的地址我是真的找不到了,不是我自己揽功啊,【尴尬】那我就来介绍一下这个被我修改增强后的控件能干些什么啦。

  1. 它能干什么?
    • 当然是支持刷新加载啦
    • 能让你自己设置头尾布局哦
    • 能让你轻松实现头尾布局的动画哦
    • 使用很简单哦
  2. 文章讲些什么
    • 带你完整的分析这个控件的实现原理
    • 教你怎么自己扩展更多的功能
    • 让你养成自己去看源码的习惯(哈哈哈哈~)

好了,我们该说点正经的了,让我想想从哪儿开始~~~

先上一张效果图吧,其实也没啥好看的,就是个简单的列表
这里写图片描述

那来看看布局吧,更简单:

  • activity_ticket.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.zy.recyclerview.view.PullRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" />

</RelativeLayout>

好了,重点来了,PullRecyclerView这个就是我们今天的主角,当然这个只是被封装好的使用组建,具体的控件的逻辑都在它的父类中,先来看看PullRecyclerView的代码:

  • PullRecyclerView.java
/**
 * RecyclerView中的所有方法都可以在此类中设置,暴露出去以供调用
 */
public class PullRecyclerView extends PullBaseView<RecyclerView> {


    public PullRecyclerView(Context context) {
        this(context, null);
    }

    public PullRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    protected RecyclerView createRecyclerView(Context context, AttributeSet attrs) {
        /**
        * 这里返回一个RecyclerView,添加到LinearLayout中
        * 那么,如果你想使用ListView的话呢,那么你在这里返回就行了
        * 当然需要修改父类中的泛型相关的地方(包括列表所使用到的Adapter)
        */
        return new RecyclerView(context, attrs);
    }

    public void setAdapter(RecyclerView.Adapter adapter) {
        mRecyclerView.setAdapter(adapter);
    }

    public void setLayoutManager(RecyclerView.LayoutManager manager) {
        mRecyclerView.setLayoutManager(manager);
    }

}

接下来我们顺着来看看它的父类PullBaseView中的具体逻辑,我会在相关的代码的地方添加详细的注释的。

  • PullBaseView.java
public abstract class PullBaseView<T extends RecyclerView> extends LinearLayout {
    //PullBaseView是继承LinearLayout,所以我们的头尾布局和RecycleView都是通过addView方法添加的
    protected T mRecyclerView;
    private boolean isCanScrollAtRereshing = false;//刷新时是否可滑动
    private boolean isCanPullDown = true;//是否可下拉
    private boolean isCanPullUp = true;//是否可上拉
    // pull state
    private static final int PULL_UP_STATE = 0;
    private static final int PULL_DOWN_STATE = 1;
    // refresh states
    private static final int PULL_TO_REFRESH = 2;
    private static final int RELEASE_TO_REFRESH = 3;
    private static final int REFRESHING = 4;

    //记住上次落点的坐标
    private int mLastMotionY;
    //headerview-头布局
    private BaseHeaderOrFooterView mHeaderView;
    //footerview-尾布局
    private BaseHeaderOrFooterView mFooterView;

    //头状态
    private int mHeaderState;
    //尾状态
    private int mFooterState;
    //下拉状态
    private int mPullState;

    //刷新接口-提供下拉刷新+上拉加载的回调方法
    private OnRefreshListener refreshListener;

    public PullBaseView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

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

    /**
     * init-初始化方法,为我们的RecyclerView做一些必要的初始化工作
     */
    private void init(Context context, AttributeSet attrs) {
        //通过回调方法获得一个RecyclerView对象
        mRecyclerView = createRecyclerView(context, attrs);
        //设置RecyclerView全屏显示
        mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
        //这里仅仅添加了一个RecyclerView只是做占位使用,在我们具体设置头布局的时候
        //会清空LinearLayout中所有的View,重新添加头布局,然后添加RecyclerView
        addView(mRecyclerView);
    }

    /**
     * 当view渲染完成后回调此方法,原先在此方法中初始化了尾布局,现在暂时废弃不用
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            //刷新时禁止滑动
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (!isCanScrollAtRereshing) {
                    if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
                        return true;
                    }
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
    * 判断是否应该到了父View,即PullToRefreshView滑动
    */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        int y = (int) e.getRawY();
        int x = (int) e.getRawX();
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 首先拦截down事件,记录y坐标
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // deltaY > 0 是向下运动,< 0是向上运动
                int deltaY = y - mLastMotionY;
                if (isRefreshViewScroll(deltaY)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return false;
    }

    /*
     * 如果在onInterceptTouchEvent()方法中没有拦截(即onInterceptTouchEvent()方法中 return
     * false)PullBaseView 的子View来处理;否则由下面的方法来处理(即由PullToRefreshView自己来处理)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaY = y - mLastMotionY;
                if (isCanPullDown && mPullState == PULL_DOWN_STATE) {
                    //头布局准备刷新
                    headerPrepareToRefresh(deltaY);
                } else if (isCanPullUp && mPullState == PULL_UP_STATE) {
                    //尾布局准备加载
                    footerPrepareToRefresh(deltaY);
                }
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //当我们的手指离开屏幕的时候,应该判断做什么处理
                int topMargin = getHeaderTopMargin();
                if (isCanPullDown && mPullState == PULL_DOWN_STATE) {
                    if (topMargin >= 0) {
                        // 开始刷新
                        headerRefreshing();
                    } else {
                        // 还没有执行刷新,重新隐藏
                        setHeaderTopMargin(-mHeaderView.getViewHeight());
                    }
                } else if (isCanPullUp && mPullState == PULL_UP_STATE) {
                    if (Math.abs(topMargin) >= mHeaderView.getViewHeight() + mFooterView.getViewHeight()) {
                        // 开始执行footer 刷新
                        footerRefreshing();
                    } else {
                        // 还没有执行刷新,重新隐藏
                        setHeaderTopMargin(-mHeaderView.getViewHeight());
                    }
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 是否应该到了父View,即PullToRefreshView滑动
     *
     * @param deltaY , deltaY > 0 是向下运动,< 0是向上运动
     * @return
     */
    private boolean isRefreshViewScroll(int deltaY) {
        if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
            return false;
        }
        if (deltaY >= -20 && deltaY <= 20)
            return false;

        if (mRecyclerView != null) {
            // 子view(ListView or GridView)滑动到最顶端
            if (deltaY > 0) {
                View child = mRecyclerView.getChildAt(0);
                if (child == null) {
                    // 如果mRecyclerView中没有数据,不拦截
                    return false;
                }
                if (isScrollTop() && child.getTop() == 0) {
                    //如果滑动到了顶端,要拦截事件交由自己处理
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }
                int top = child.getTop();
                int padding = mRecyclerView.getPaddingTop();
                if (isScrollTop() && Math.abs(top - padding) <= 8) {// 这里之前用3可以判断,但现在不行,还没找到原因
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }
            } else if (deltaY < 0) {
                View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
                if (lastChild == null) {
                    // 如果mRecyclerView中没有数据,不拦截
                    return false;
                }
                // 最后一个子view的Bottom小于父View的高度说明mRecyclerView的数据没有填满父view,
                // 等于父View的高度说明mRecyclerView已经滑动到最后
                if (lastChild.getBottom() <= getHeight() && isScrollBottom()) {
                    mPullState = PULL_UP_STATE;
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断mRecyclerView是否滑动到顶部
     *
     * @return
     */
    boolean isScrollTop() {
        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        if (linearLayoutManager.findFirstVisibleItemPosition() == 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 判断mRecyclerView是否滑动到底部
     *
     * @return
     */
    boolean isScrollBottom() {
        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        if (linearLayoutManager.findLastVisibleItemPosition() == (mRecyclerView.getAdapter().getItemCount() - 1)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * header 准备刷新,手指移动过程,还没有释放
     *
     * @param deltaY ,手指滑动的距离
     */
    private void headerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);
        // 当headerview的topMargin>=0时,说明已经完全显示出来了,修改header view的提示状态
        if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) {
            //调用我们自定义头布局的释放刷新操作,具体代码在自定义headerview中实现
            mHeaderView.releaseToRefreshOrLoad();
            mHeaderState = RELEASE_TO_REFRESH;
        } else if (newTopMargin < 0 && newTopMargin > -mHeaderView.getViewHeight()) {// 拖动时没有释放
            //调用我们自定义头布局的下拉刷新操作,具体代码在自定义headerview中实现
            mHeaderView.pullToRefreshOrLoad();
            mHeaderState = PULL_TO_REFRESH;
        }
    }

    /**
     * footer准备刷新,手指移动过程,还没有释放 移动footerview高度同样和移动header view
     * 高度是一样,都是通过修改headerview的topmargin的值来达到
     *
     * @param deltaY ,手指滑动的距离
     */
    private void footerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);
        // 如果header view topMargin 的绝对值大于或等于header + footer 的高度
        // 说明footer view 完全显示出来了,修改footer view 的提示状态
        if (Math.abs(newTopMargin) >= (mHeaderView.getViewHeight() + mFooterView.getViewHeight()) && mFooterState != RELEASE_TO_REFRESH) {
            //调用我们自定义尾布局的释放加载操作,具体代码在自定义footerview中实现
            mFooterView.releaseToRefreshOrLoad();
            mFooterState = RELEASE_TO_REFRESH;
        } else if (Math.abs(newTopMargin) < (mHeaderView.getViewHeight() + mFooterView.getViewHeight())) {
            //调用我们自定义尾布局的上拉加载操作,具体代码在自定义footerview中实现
            mFooterView.pullToRefreshOrLoad();
            mFooterState = PULL_TO_REFRESH;
        }
    }

    /**
     * 修改Headerview topmargin的值
     *
     * @param deltaY
     * @description
     */
    private int changingHeaderViewTopMargin(int deltaY) {
        LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
        float newTopMargin = params.topMargin + deltaY * 0.3f;

        //此处要做的事情是通过布局返回一个百分比大小,供有的头布局动画使用
        if ((mHeaderView.getViewHeight() + params.topMargin) <= mHeaderView.getViewHeight()) {
            //如果我们头布局的高度是正值,params.topMargin是负值
            //当头布局从完全隐藏到刚好显示的过程是0~mHeaderView.getViewHeight()的过程
            //所以用它做分子,分母就是我们的头布局高度
            //计算并通过方法返回比例
            DecimalFormat format = new DecimalFormat("0.00");
            float differ = mHeaderView.getViewHeight()+ params.topMargin;
            float total = mHeaderView.getViewHeight();
            float rate = differ/total;
            mHeaderView.getPercentage(Float.parseFloat(format.format(rate)));
        } else {
            //走到这儿说明我们的头布局已经被继续下拉,超过了本身的大小
            //所以返回1恒定值表示100%,不在继续增加
            mHeaderView.getPercentage(1.00f);
        }

        // 这里对上拉做一下限制,因为当前上拉后然后不释放手指直接下拉,会把下拉刷新给触发了,感谢网友yufengzungzhe的指出
        // 表示如果是在上拉后一段距离,然后直接下拉
        if (deltaY > 0 && mPullState == PULL_UP_STATE && Math.abs(params.topMargin) <= mHeaderView.getViewHeight()) {
            //如果每次偏移量>0,说明是下拉刷新,并且params.topMargin绝对值小于等于头布局高度
            //说明头布局还未完全显示,直接返回
            return params.topMargin;
        }
        // 同样地,对下拉做一下限制,避免出现跟上拉操作时一样的bug
        if (deltaY < 0 && mPullState == PULL_DOWN_STATE && Math.abs(params.topMargin) >= mHeaderView.getViewHeight()) {
            return params.topMargin;
        }
        params.topMargin = (int) newTopMargin;
        mHeaderView.getView().setLayoutParams(params);
        invalidate();
        return params.topMargin;
    }

    /**
     * header refreshing
     */
    public void headerRefreshing() {
        mHeaderState = REFRESHING;
        //设置头布局完全显示
        setHeaderTopMargin(0);
        //通过自定义头布局回调方法,实行我们自定义的逻辑
        mHeaderView.isRefreshingOrLoading();
        if (refreshListener != null) {
            //接口回调,用于处理网络
            refreshListener.onPullToRefresh(this);
        }
    }

    /**
     * footer refreshing
     */
    private void footerRefreshing() {
        mFooterState = REFRESHING;
        //将我们的头布局margin设为头布局+尾布局高度和,这样尾布局将完全显示
        int top = mHeaderView.getViewHeight() + mFooterView.getViewHeight();
        setHeaderTopMargin(-top);
        //通过自定义尾布局回调方法,实行我们自定义的逻辑
        mFooterView.isRefreshingOrLoading();
        if (refreshListener != null) {
            //接口回调,用于网络处理
            refreshListener.onPullToLoadMore(this);
        }
    }

    /**
     * 设置header view 的topMargin的值
     *
     * @param topMargin ,为0时,说明header view 刚好完全显示出来; 为-mHeaderViewHeight时,说明完全隐藏了
     * @description
     */
    private void setHeaderTopMargin(int topMargin) {
        LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
        params.topMargin = topMargin;
        mHeaderView.getView().setLayoutParams(params);
        invalidate();
    }

    /**
     * header view 完成更新后恢复初始状态
     */
    public void onHeaderRefreshComplete() {
        setHeaderTopMargin(-mHeaderView.getViewHeight());
        //通过自定义头布局更新我们刷新完成后的头布局各控件的状态和显示
        mHeaderView.refreshOrLoadComplete();
        mHeaderState = PULL_TO_REFRESH;
    }

/**
     * footer view 完成更新后恢复初始状态
     */
    public void onFooterRefreshComplete() {
        setHeaderTopMargin(-mHeaderView.getViewHeight());
        //通过自定义尾布局更新我们刷新完成后的尾布局各控件的状态和显示
        mFooterView.refreshOrLoadComplete();
        mFooterState = PULL_TO_REFRESH;
        if (mRecyclerView != null) {
            //加载完后列表停留在最后一项
            mRecyclerView.scrollToPosition(mRecyclerView.getAdapter().getItemCount() - 1);
        }
    }

    /**
     * 获取当前header view 的topMargin
     *
     * @description
     */
    private int getHeaderTopMargin() {
        LayoutParams params = (LayoutParams) mHeaderView.getView().getLayoutParams();
        return params.topMargin;
    }


    /**
     * set headerRefreshListener
     * 设置我们的接口
     * @description
     */
    public void setOnRefreshListener(OnRefreshListener refreshListener) {
        this.refreshListener = refreshListener;
    }
/**
  * Interface definition for a callback to be invoked when list/grid footer
  * view should be refreshed.
  */
 public interface OnRefreshListener {
     //下拉刷新的回调方法
     void onPullToRefresh(PullBaseView view);
     //上拉加载的回调方法
     void onPullToLoadMore(PullBaseView view);
 }

 /**
  * 设置是否可以在刷新时滑动
  *
  * @param canScrollAtRereshing
  */
 public void setCanScrollAtRereshing(boolean canScrollAtRereshing) {
     isCanScrollAtRereshing = canScrollAtRereshing;
 }

 /**
  * 设置是否可上拉
  *
  * @param canPullUp
  */
 public void setCanPullUp(boolean canPullUp) {
     isCanPullUp = canPullUp;
 }

 /**
  * 设置是否可下拉
  *
  * @param canPullDown
  */
 public void setCanPullDown(boolean canPullDown) {
     isCanPullDown = canPullDown;
 }

 protected abstract T createRecyclerView(Context context, AttributeSet attrs);

 /**
  * 用于设置刷新列表头部显示样式
  *
  * @param headerView
  * @return
  */
 public PullBaseView setHeaderView(BaseHeaderOrFooterView headerView) {
     this.mHeaderView = headerView;
     //清除所有的view,重新添加布局
     removeAllViews();
     //添加我们自定义的头布局
     addView(mHeaderView.getView(), mHeaderView.getParams());
     addView(mRecyclerView);
     return this;
 }

 /**
  * 用于设置刷新列表底部显示样式
  *
  * @param footerView
  * @return
  */
 public PullBaseView setFooterView(BaseHeaderOrFooterView footerView) {
     this.mFooterView = footerView;
     //添加我们自定义的尾布局
     addView(mFooterView.getView(), mFooterView.getParams());
     return this;
 }
}

好啦,到这里我们的真正的处理逻辑的代码已经完全讲完了,注释标注的应该非常的详尽了,那么我们接下来来看看如何自定义头尾布局呢,这里我封装来一个基类BaseHeaderOrFooterView,先来看这个类的代码并加以注释说明:

  • BaseHeaderOrFooterView.java
/**
 * 所有HeaderView或者是FooterView的基类
 * 因为需要强制我们的子类实现某些方法,所以这里用的是抽象类
 */
public abstract class BaseHeaderOrFooterView {

    private Context mContext;
    private View mView;
    protected int mViewHeight;
    protected LinearLayout.LayoutParams params;
    public static final int HEADER = 0;
    public static final int FOOTER = 1;
    private int type;

    public BaseHeaderOrFooterView(Context context, @NonNull ViewGroup root, int type) {
        this(context, root, type,false);
    }

    public BaseHeaderOrFooterView(Context context, @NonNull ViewGroup root,int type, boolean attachToRoot) {
        mContext = context;
        this.type = type;
        createView(context, root, attachToRoot);
    }

    private void createView(Context context, ViewGroup root, boolean attachToRoot) {
        mView = LayoutInflater.from(context).inflate(onBindLayoutId(), root, attachToRoot);
        measureView(mView);
        mViewHeight = mView.getMeasuredHeight();
        params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, mViewHeight);
        // 设置topMargin的值为负的header View高度,即将其隐藏在最上方
        if (type == HEADER)
            params.topMargin = -(mViewHeight);
        onViewCreated(mView);
    }

    public final String getFormatDateString(String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(new Date());
    }

    protected abstract void onViewCreated(View view);

    protected abstract int onBindLayoutId();

    protected LinearLayout.LayoutParams getParams() {
        return params;
    }

    protected int getViewHeight() {
        return mViewHeight;
    }

    /**
     * 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
     * 释放刷新/加载更多
     */
    public abstract void releaseToRefreshOrLoad();

    /**
     * 下拉刷新/上拉加载
     */
    public abstract void pullToRefreshOrLoad();

    /**
     * 执行正在刷新/加载的操作
     *
     * @return
     */
    public abstract void isRefreshingOrLoading();

    /**
     * 刷新/加载完成的操作
     *
     * @return
     */
    public abstract void refreshOrLoadComplete();

    /**
     * 这里提供一个方法能够获取下拉头显示的百分比,供动画效果使用
     */
    public abstract void getPercentage(float rate);

    public View getView() {
        return mView;
    }

    protected Context getContext() {
        return mContext;
    }

    public void measureView(View child) {
        ViewGroup.LayoutParams p = child.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    }
}

好的,我们的头尾布局的基类已经解释完了,那么我们可以自定义两个头尾布局继承自BaseHeaderOrFooterView,并实现某些方法来验证我们的控件的效果,先来个TicketHeaderView:

  • TicketHeaderView.java
public class TicketHeaderView extends BaseHeaderOrFooterView {

    private ProgressBar progressBar;
    private ImageView image;
    private TextView text;

    public TicketHeaderView(Context context, @NonNull ViewGroup root) {
        super(context, root, HEADER);
    }

    @Override
    protected void onViewCreated(View view) {
        progressBar = (ProgressBar) view.findViewById(R.id.progress);
        image = (ImageView) view.findViewById(R.id.image);
        text = (TextView) view.findViewById(R.id.text);
    }

    @Override
    protected int onBindLayoutId() {
        return R.layout.ticket_head;
    }

    /**
     * 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
     * 释放刷新的提示
     */
    @Override
    public void releaseToRefreshOrLoad() {
        text.setText("释放刷新数据");
        progressBar.setVisibility(View.GONE);
    }

    @Override
    public void pullToRefreshOrLoad() {
        text.setText("下拉刷新数据");
        progressBar.setVisibility(View.GONE);
    }

    /**
     * 正在刷新
     */
    @Override
    public void isRefreshingOrLoading() {
        text.setText("正在刷新...");
        image.setVisibility(View.GONE);
        progressBar.setVisibility(View.VISIBLE);
    }

    /**
    * 刷新完成的回调
    */
    @Override
    public void refreshOrLoadComplete() {
        text.setText("下拉刷新数据");
        progressBar.setVisibility(View.GONE);
        image.setVisibility(View.VISIBLE);
    }

    @Override
    public void getPercentage(float rate) {
        //这里设置的是根据下拉头显示的百分比进行一个头部图片动态缩放的效果
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) image.getLayoutParams();
        params.width = (int) (80 * rate);
        params.height = (int) (80 * rate);
        image.setLayoutParams(params);
    }
}
  • ticket_head.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:gravity="center"
    android:paddingBottom="15dip"
    android:paddingTop="15dip">

    <RelativeLayout
        android:id="@+id/relativelaytout"
        android:layout_width="30dp"
        android:layout_height="30dp">

        <ProgressBar
            android:id="@+id/progress"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:visibility="gone" />


        <ImageView
            android:id="@+id/image"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_centerInParent="true"
            android:src="@drawable/ha" />
    </RelativeLayout>

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/relativelaytout"
        android:text="下拉刷新数据"
        android:textSize="15dp" />

</RelativeLayout>
  • TicketFooterView.java
public class TicketFooterView extends BaseHeaderOrFooterView {

    private TextView mFooterTextView;
    private ProgressBar mFooterProgressBar;

    public TicketFooterView(Context context, @NonNull ViewGroup root) {
        super(context, root, FOOTER);
    }

    @Override
    protected void onViewCreated(View view) {
        mFooterTextView = (TextView) view.findViewById(R.id.pull_to_load_text);
        mFooterProgressBar = (ProgressBar) view.findViewById(R.id.pull_to_load_progress);
    }

    @Override
    protected int onBindLayoutId() {
        return R.layout.refresh_footer;
    }

    @Override
    public void releaseToRefreshOrLoad() {
        mFooterTextView.setText("松开加载更多");
    }

    @Override
    public void pullToRefreshOrLoad() {
        mFooterTextView.setText("上拉加载更多");
    }

    @Override
    public void isRefreshingOrLoading() {
        mFooterTextView.setVisibility(View.GONE);
        mFooterProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    public void refreshOrLoadComplete() {
        mFooterTextView.setText("上拉加载更多");
        mFooterTextView.setVisibility(View.VISIBLE);
        mFooterProgressBar.setVisibility(View.GONE);
    }

    @Override
    public void getPercentage(float rate) {
        //用不上的话可以不实现具体逻辑
    }
}
  • refresh_footer.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pull_to_refresh_header"
    android:layout_width="fill_parent"
    android:layout_height="60dp">

    <ProgressBar
        android:id="@+id/pull_to_load_progress"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_centerInParent="true"
        android:visibility="gone" />

    <TextView
        android:id="@+id/pull_to_load_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="上拉加载更多"
        android:textColor="#BBBCBD"
        android:textSize="16sp" />

</RelativeLayout>

接下来我们来看看数据的显示,这里提供了一个BaseAdapter类,当中封装了一些常用的方法,如果有需要大家可以根据自己的场景进行扩展。

  • BaseAdapter.java
package com.zy.recyclerview.view;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;


import java.util.List;

/**
 * BaseAdapter
 */
public class BaseAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {

    public Context context;//上下文
    public List<Object> listDatas;//数据源
    public LayoutInflater mInflater;
    public OnViewClickListener onViewClickListener;//item子view点击事件
    public OnItemClickListener onItemClickListener;//item点击事件
    public OnItemLongClickListener onItemLongClickListener;//item长按事件

    public BaseAdapter(Context context, List<Object> listDatas) {
        init(context, listDatas);
    }

    /**
     * 如果item的子View有点击事件,可使用该构造方法
     *
     * @param context
     * @param listDatas
     * @param onViewClickListener
     */
    public BaseAdapter(Context context, List<Object> listDatas, OnViewClickListener onViewClickListener) {
        init(context, listDatas);
        this.onViewClickListener = onViewClickListener;
    }

    /**
     * 初始化
     *
     * @param context
     * @param listDatas
     */
    void init(Context context, List<Object> listDatas) {
        this.context = context;
        this.listDatas = listDatas;
        this.mInflater = LayoutInflater.from(context);
    }

    @Override
    public T onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(T holder, final int position) {
        holder.itemView.setOnClickListener(new View.OnClickListener() {//item点击事件
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null) {
                    onItemClickListener.onItemClick(position);
                }
            }
        });
        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {//item长按事件
            @Override
            public boolean onLongClick(View v) {
                if (onItemLongClickListener != null) {
                    onItemLongClickListener.onItemLongClick(position);
                }
                return true;
            }
        });
    }

    @Override
    public int getItemCount() {
        return listDatas.size();
    }

    /**
     * item中子view的点击事件(回调)
     */
    public interface OnViewClickListener {
        /**
         * @param position item position
         * @param viewtype 点击的view的类型,调用时根据不同的view传入不同的值加以区分
         */
        void onViewClick(int position, int viewtype);
    }

    /**
     * item点击事件
     */
    public interface OnItemClickListener {
        void onItemClick(int position);
    }

    /**
     * item长按事件
     */
    public interface OnItemLongClickListener {
        void onItemLongClick(int position);
    }

    /**
     * 设置item点击事件
     *
     * @param onItemClickListener
     */
    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    /**
     * 设置item长按事件
     *
     * @param onItemLongClickListener
     */
    public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
        this.onItemLongClickListener = onItemLongClickListener;
    }

}

下面我们来看看我们自己的实现的TicketAdapter

  • TicketAdapter.java
package com.zy.recyclerview.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import com.zy.recyclerview.R;
import com.zy.recyclerview.bean.SampleBean;
import com.zy.recyclerview.view.BaseAdapter;

import java.util.List;

public class TicketAdapter extends BaseAdapter<TicketAdapter.MyViewHolder> {

    public TicketAdapter(Context context, List listDatas, OnViewClickListener onViewClickListener) {
        super(context, listDatas, onViewClickListener);
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MyViewHolder(mInflater.inflate(R.layout.list_item, parent, false));
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        SampleBean item = (SampleBean) listDatas.get(position);
        holder.mTitleTextView.setText(
                TextUtils.isEmpty(item.getTitle()) ? "暂无价格"
                        : item.getTitle());
        holder.mInfoTextView.setVisibility(View.GONE);

        if (TextUtils.isEmpty(item.getPrice_str())) {
            holder.mPriceTextView.setText("暂无价格");
            holder.mPriceUnit.setVisibility(View.GONE);
        } else {
            holder.mPriceTextView.setText(item.getPrice_str());
            holder.mPriceUnit.setVisibility(View.VISIBLE);
        }

        holder.mSoldTimeTextView.setText(item.getSign_date());

        if (position == listDatas.size() - 1) {
            holder.mDivider.setVisibility(View.GONE);
        } else {
            holder.mDivider.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public int getItemCount() {
        return listDatas.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        public ImageView mImageView;
        public TextView mTitleTextView;
        public TextView mInfoTextView;
        public TextView mPriceTextView;
        public TextView mSoldTimeTextView;
        public TextView mPriceUnit;
        public View mDivider;

        public MyViewHolder(View view) {
            super(view);
            mImageView = (ImageView) view.findViewById(R.id.iv_img);
            mTitleTextView = (TextView) view.findViewById(R.id.tv_title);
            mInfoTextView = (TextView) view.findViewById(R.id.tv_info);
            mPriceTextView = (TextView) view.findViewById(R.id.tv_ticket_price);
            mSoldTimeTextView = (TextView) view.findViewById(R.id.tv_sold_time);
            mPriceUnit = (TextView) view.findViewById(R.id.tv_price_unit);
            mDivider = view.findViewById(R.id.divider);
        }
    }
}
  • list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/list_item_selector"
    android:orientation="vertical"
    android:paddingLeft="24dp"
    android:paddingRight="24dp">

    <RelativeLayout
        android:id="@+id/rl_image"
        android:layout_width="105dp"
        android:layout_height="80dp"
        android:layout_marginBottom="20dp"
        android:layout_marginTop="20dp">

        <ImageView
            android:id="@+id/iv_img"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop"
            android:src="@drawable/sample" />
    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="87dp"
        android:layout_marginLeft="13dp"
        android:layout_marginTop="17dp"
        android:layout_toRightOf="@id/rl_image">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="#101D37"
            android:textSize="16dp" />

        <TextView
            android:id="@+id/tv_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_title"
            android:layout_marginTop="5dp"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="#101D37"
            android:textSize="12dp" />

        <TextView
            android:id="@+id/tv_sold_time"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_info"
            android:layout_marginTop="5dp"
            android:ellipsize="end"
            android:singleLine="true"
            android:textColor="#101D37"
            android:textSize="12dp" />

        <LinearLayout
            android:id="@+id/ll_ticket_price"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:gravity="bottom"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tv_ticket_price"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="bottom"
                android:textColor="#ff5848"
                android:textSize="15dp" />

            <TextView
                android:id="@+id/tv_price_unit"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="1dp"
                android:gravity="bottom"
                android:text="元"
                android:textColor="#ff5848"
                android:textSize="13dp" />

        </LinearLayout>
    </RelativeLayout>

    <View
        android:id="@+id/divider"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_alignParentBottom="true"
        android:background="#E4E6F0" />

</RelativeLayout>

这就是一个最普通的adapter使用了,看看就懂。

最后来看看TicketActivity如何实现的吧

  • TicketActivity.java
package com.zy.recyclerview.ui;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.LinearLayoutManager;
import android.view.Window;
import android.widget.RelativeLayout;
import android.widget.Toast;

import com.zy.recyclerview.R;
import com.zy.recyclerview.adapter.TicketAdapter;
import com.zy.recyclerview.bean.SampleBean;
import com.zy.recyclerview.view.BaseAdapter;
import com.zy.recyclerview.view.TicketHeaderView;
import com.zy.recyclerview.view.TicketFooterView;
import com.zy.recyclerview.view.PullBaseView;
import com.zy.recyclerview.view.PullRecyclerView;

import java.util.ArrayList;
import java.util.List;

public class TicketActivity extends Activity implements PullBaseView.OnRefreshListener, BaseAdapter.OnItemClickListener {

    private PullRecyclerView recyclerView;
    List<SampleBean> mList = new ArrayList<>();
    TicketAdapter ticketAdapter;
    RelativeLayout container;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_ticket);
        container = (RelativeLayout) findViewById(R.id.ll_container);
        initData();
        initRecycleView();
    }

    private void initRecycleView() {
        recyclerView = (PullRecyclerView) findViewById(R.id.recyclerView);
        recyclerView.setHeaderView(new TicketHeaderView(this, container))
                .setFooterView(new TicketFooterView(this, container));
        recyclerView.setOnRefreshListener(this);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        ticketAdapter = new TicketAdapter(this, mList, null);
        ticketAdapter.setOnItemClickListener(this);
        recyclerView.setAdapter(ticketAdapter);
    }

    private void initData() {
        for (int i = 0; i < 10; i++) {
            SampleBean bean = new SampleBean();
            bean.setPrice_str(200 + i + "");
            bean.setPrice_unit("元");
            bean.setSign_date("2018.05.31");
            bean.setTitle("国家大剧院-风雪夜归人-" + (i + 1) + "场");
            mList.add(bean);
        }
    }

    @Override
    public void onPullToRefresh(PullBaseView view) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                SampleBean bean = new SampleBean();
                bean.setPrice_str("666");
                bean.setPrice_unit("元");
                bean.setSign_date("2018.05.31");
                bean.setTitle("国家大剧院-风雪夜归人-VIP场");
                mList.add(0, bean);
                ticketAdapter.notifyDataSetChanged();
                recyclerView.onHeaderRefreshComplete();
            }
        }, 1500);
    }

    @Override
    public void onPullToLoadMore(PullBaseView view) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                SampleBean bean = new SampleBean();
                bean.setPrice_str("888");
                bean.setPrice_unit("元");
                bean.setSign_date("2018.05.31");
                bean.setTitle("国家大剧院-风雪夜归人-VVIP场");
                mList.add(bean);
                ticketAdapter.notifyDataSetChanged();
                recyclerView.onFooterRefreshComplete();
            }
        }, 1500);
    }

    @Override
    public void onItemClick(int position) {
        Toast.makeText(this, "position : " + position, Toast.LENGTH_SHORT).show();
    }
}

好了,到这里整个控件的原理+使用已经全部讲完了,来回顾一下这些内容:

1.PullToRefreshRecycleView的源码自己好好揣摩揣摩,在本子上画一下,然后根据代码的注释自己分析一下是不是这么个道理
2.如果想自定义或者扩展更多的东西,那么第一步能必须做到,其实原理看懂了理解了并不难,不是吗?
3.头尾布局的问题,如果你想让别人能够使用自定义布局,将应当像我这样把东西整理一下实现一个基类,并对外暴露足够的方法,头尾布局中只应该关心自己布局中的控件,以及刷新或者加载前后中的状态的控件的变化
4.接下来你能在这个基础上做些什么呢?其实我觉得能做的东西还是很多的,毕竟这只是一个初版,你也可以发挥自己的想象力在这个的基础上进行大胆的改造,比如除了刷新头布局以外,如果要求你在RecycleView列表上方加上一个banner控件,是不是很简单?毕竟我们的PullBaseView是一个LinearLayout,可以自由的addView进去,所以这就是你可以做的事情

好啦,说完了啦,有什么问题,欢迎留言,一起探讨~【微笑】

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Android Studio 中实现自定义控件的过程如下: 1. 创建自定义控件类 在 Android Studio 中,创建一个继承自 View 或其子类的类,作为自定义控件的类。例如,创建一个名为 MyView自定义控件类: ```java public class MyView extends View { public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制控件的内容 } } ``` 2. 在布局文件中使用自定义控件 在布局文件中使用自定义控件,可以通过全限定类名的方式引用自定义控件类。例如,在布局文件中使用刚刚创建的 MyView 控件: ```xml <com.example.MyView android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 3. 实现自定义控件的逻辑 在自定义控件类中,实现控件的逻辑。这包括控件的属性、事件和绘制等。例如,实现 MyView 控件的点击事件: ```java public class MyView extends View { private OnClickListener mClickListener; public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setOnClickListener(OnClickListener listener) { mClickListener = listener; } @Override public boolean onTouchEvent(MotionEvent event) { if (mClickListener != null && event.getAction() == MotionEvent.ACTION_UP) { mClickListener.onClick(this); return true; } return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制控件的内容 } } ``` 在这个例子中,我们实现了一个 onClickListener 方法,重写了 onTouchEvent 方法,当用户在控件上抬起手指时触发 onClick 事件。 以上就是在 Android Studio 中实现自定义控件的基本步骤。通过这些步骤,我们可以创建自定义控件,并实现其属性、事件和绘制等逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖子爱你520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值