写一个自己的下拉刷新组件

开发动机

现在的APP里面十个有八个肯定会有下拉刷新组件,但是有的时候这些第三方Jar并不能满足我们的需求定制。我所在的项目就遇到了这种情况,需要在刷新成功后加一个停留动画,并且需要区分成功和失败,因为我们项目组是分模块的开发,同事采用了Hardcode的方式满足了自身的需求,但是其他模块都通用不了。所以,我决定自己写一个下拉刷新来替代原有的变成一个通用的BaseView。
下面,开始我们的表演~

开发思路

在写代码之前,我们简单想一想,下拉刷新需要怎么实现?
~请原谅我不能画图来表示,画图真的很花时间~

  • 第一步 整体框架

    我首先想到的滑动,在一个ViewGroup内部滑动的话可以通过scoller来管理scroll值。所以思路就是一个自定义的ViewGroup,考虑到自定义ViewGroup需要自己去onMeasureonLayout,有点麻烦呀。怎么办?先继承LinearLayout吧,先从简单的开始。顶部的View命名为HeaderView,底部的View命名为FooterView,中间夹着一个实体View,通过scoller控制HeaderView 和FooterView的显示和隐藏

  • 第二步 HeaderView和FooterView 的实现
    整体框架出来之后,就需要考虑HeaderView怎么去实现。(FooterView的实现和HeaderView一模一样,所以后面就只写HeaderView。我写代码有一个习惯,就是先用接口去写想要的结果,然后再去实现这个接口。)因为HeaderView中需要显示下拉刷新、刷新中、刷新成功等提示,所以接口需要begin,progress,end最基本的三个方法。

  • 第三步 到顶计算

    有的子View也能滑动,我们在什么情况下可以下拉刷新,需要去计算子View是否到顶,还有一种复杂布局的情况下,需要多个子View一起配合,才能下拉刷新。所以需要提供一个方法,将需要计算到顶的View,传入。然后统一判断。

  • 第四步 合并实现

    好了。整体框架和顶部底部的思路都出来,还差什么?还差了一个触摸控制。触摸控制就要明白android的Touch事件分发了,这个就是细节实现问题了。所以,开始写代码吧。

知识储备

等等,我们先总结下开发下拉刷新需要什么知识点
按优先级排序为:

Scroller

其实不使用Scroller也可以,我们通过View内部自带的getScrollY()拿到滑动值,然后ACTION_UP的时候,启动一个动画,不断调用scrollTo(int x, int y)改变scroll值一样能达到目的。
那为什么要使用Scroller呢?
就像汽车的自动挡和手动挡一样。Scroller就是自动挡,已经帮我们把业务之外的工作全部做了。知道当前的起点Y,目的地Y2,就可以调用Scrollerpublic void startScroll(int startX, int startY, int dx, int dy, int duration) 实现松手滑动动画。
Scroller最重要的一点是 Scroller 只改变View的scroll值,但是不滑动,需要我们主动刷新,切记切记。如果调用了startScroll结果发现没有效果,不要惊讶,请主动复写如下代码:

     @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset() && !mScroller.isFinished()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

Touch事件分发

接收Touch时间主要有三个方法,优先级从大到小排序依次为,分发、拦截、使用。
这里简单说一下三个方法的区别
这里要注意一下:onInterceptTouchEvent在dispatchTouchEvent里会判断,所以会是这样传递。
[定义:父layout在最下层,子View在最上层]
下层dispatchTouchEvent->下层onInterceptTouchEvent ->
——该层dispatchTouchEvent-> 该层onInterceptTouchEvent ->
————- 上层dispatchTouchEvent-> 上层onInterceptTouchEvent
分发(ViewGroup独有)
默认能收到所以经过此viewGroup的Touch事件,包括子View,Touch事件从父View一级一级往上传递,此方法要慎用
当在dispatchTouchEvent中返回true后,表示事件被收起来,不会继续往上一层传递。

并且该层的onInterceptTouchEventonTouchEvent将不会再接收到任何Touch事件,上层的三个方法都不会接收到任何Touch事件。
当在dispatchTouchEvent中返回false后,该层的和上层的所有方法都将接收不到任何Touch事件。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

拦截(ViewGroup独有)
当在onInterceptTouchEvent中返回true后,表示所有经过的事件被此View**强制接收**,上层View会收到一次ACTION_CANCEL,然后上层View的三个方法将不会收到任何事件。
当在onInterceptTouchEvent中返回false后,该层的onTouchEvent不会收到任何事件

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

使用(ViewGroup和View都有)
onTouchEvent就比较简单了。
在ViewGroup中,不在拦截中拦截事件的话,不会走进onTouchEvent
onTouchEvent中返回true表示使用该事件,其他层的onTouchEvent将不会收到事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

基本的事件传递描述完毕。三个方法返回值相互配合,情况又会不一样。ACTION_DOWN、ACTION_MOVE、ACTION_UP在事件传递中还有一些细节,用文字描述的话可能有点绕,大家写一个Demo,跑一下会有更好的认识。

自定义ViewGroup
主要有三个地方需要注意:

  • onMeasure
    用于计算ViewGroup大小。使用setMeasuredDimension(mTotalWidth, mTotalHeight)设置ViewGroup的大小。

  • onLayout

    用于布局子View,设置子View的位置

  • generateLayoutParams

    设置自己的LayoutParams,通常继承MarginLayoutParams才能使用Margin属性

    @Override
    public RefreshLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new RefreshLayout.LayoutParams(getContext(), attrs);
    }
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }

代码

源码和Demo GitHub:[https://github.com/xindasunday/easyrefreshlayout]

导入方式:
第一步:增加maven地址

allprojects {
    repositories {
        jcenter()
        repositories {
            maven { url 'https://www.jitpack.io' }
        }
    }
}

第二步:
模块build.gradle中增加

dependencies {
    compile 'com.github.xindasunday:easyrefreshlayout:1.0.1'
}

gradle sync之后即可开始使用easyrefreshlayout。

和Demo见GitHub:https://github.com/xindasunday/easyrefreshlayout
一个周的时间,调试的差不多,并且在实际项目中测试解决过BUG了。

HeaderView:

public interface HeaderView {
    void begin();

    /**
     * 相对于HeaderView高度的倍数
     * **/
    void progress(float progress);

    void loading();

    //初始化和结束后调用
    void reset();

    View getView();

    //刷新成功后暂停几秒用于显示动画效果后
    void showPause(boolean success);

    boolean isPauseTime();

    long getPauseMillTime();
}

接口的意义在与可以自定义多种多样的HeaderVIew。

public interface RefreshListener {
    void refresh();

    void loadMore();
}

RefreshLayout的属性

    private HeaderView mHeaderView;
    private FootView mFootView;
    private float mLastX;
    private float mLastY;
    private int mHeadViewHeight;
    private int mFootViewHeight;
    private Scroller mScroller;
    private RefreshListener mRefreshListener;
    //是否可以拉动超出HeaderView的高度
    private boolean isFullPull = true;
    //是否可以拉动超出FooterView的高度
    private boolean isFullPush = true;
    //把HeaderView拉出的最大高度,isFullPull = true时 生效
    private int maxPullHeight;
    //把FooterView拉出的最大高度,isFullPush = true时 生效
    private int maxPushHeight;
    //滑动差值,值越小,滑动速度越慢
    private float mMoveRate = 0.3f;
    //headerView或者footerView 在可全屏滑动下,松手后滑动进入刷新/加载状态的时间
    private int mOutRangeScrollTime = 800;
    //headerView或者footerView 在刷新/加载结束后,隐藏的滑动时间
    public int hideHeadFootViewTime = 800;//ms
    //是否处于刷新状态
    private boolean isRefresh;
    //是否处于加载状态
    private boolean isLoadMore;
    //是否允许刷新
    private boolean isCanRefresh = true;
    //是否允许加载
    private boolean isCanLoadMore = true;
    //用于计算子View是否到底/到顶,轻松解决嵌套/组合 可滑动控件
    private Set<View> mChildCalcList;
    //覆盖mBaseView的错误提示view;
    private View mErrorView;
    private View mBaseView;
效果(gif动图,加载较慢,请耐心等一下):

这里写图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值