自个儿写Android的下拉刷新/上拉加载控件

前段时间自己写了一个能够“通用”的,支持下拉刷新和上拉加载的自定义控件。可能现如今这已经不新鲜了,但有兴趣的朋友还是可以一起来看看的。

  • 与通常的View配合使用(比如ImageView)

ImageView下拉刷新

  • 与ListView配合使用

ListView下拉刷新、上拉加载

  • 与RecyclerView配合使用

RecyclerView下拉刷新、上拉加载

  • 与SrcollView配合使用

SrcollView下拉刷新

  • 局部刷新(但想必这种需要实际应该还是不多的….)

作为局部View刷新

好啦,效果大概就是这样。如果您看后觉得有一点兴趣。那么,以下是相关的信息:


好了,闲话就到这里了。现在正式切入正题,于此逐步简单的记录和总结一下实现这个自定义View的思路以及实现过程。

首先,我们分析一下:假设我们现在的需求是需要让ListView支持下拉刷新和上拉加载,那么其实我们选择去扩展系统自身的ListView是最好的。
但我们这里的初衷是创造一个通用的Pullable的控件,也就是说它可以配合Android中各种View使用。所以,显然我们需要的是一个ViewGroup。
那么,既然有了思路就可以开动了:第一步我们先去创建我们自己的View,并让其继承自ViewGroup。例如就像下面这样:

public class PullableLayout extends ViewGroup{

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

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

接下来,我们静静的思考一下所谓的下拉刷新,上拉加载的本质何如。就会发现,其实归根结底原理仍旧是“视图的滚动”而已。
那么,我们来分析下我们为什么会这么说呢?假设现在先在脑海中简单构画一下如下所示的这样一个ViewGroup的结构图:

假设上图中蓝色的部分就是屏幕区域,也就是我们想要呈现内容的区域(比如我们在这里放一个ListView)。而我们的ViewGroup所需要做的工作就是:
为Content部分加上一个Header(头视图)与Footer(尾视图),并且显然Header的位置应该位于Content之上,同理Footer则位于其之下。

那么,在这个基础上,如果我们让整个Viewgroup支持滚动,那么就得以实现一种效果了,即:初始情况下,屏幕上将正常呈现我们的Content视图。
与此同时:当我们上下滑动屏幕,那么当滑动到Content视图的顶部时,就会出现Header视图;当滑动到Content的底部时,则会出现Footer视图。

当然,这种纸上谈兵式的原理性的东西,永远都让人感到无聊。所以,现在我们实际的来“兑换”一下我们目前为止谈到的这种效果。看以下布局文件:

左边的布局非常简单和熟悉,就是显示一个宽高填满父窗口的ImageView。而在右边我们则是把父布局替换成了我们自定义的PullableLayout。

好的,现在我们就一起来看看,我们应该怎么样逐步完善PullableLayout让它实现我们说到的效果。
首先,既然我们说到需要一个Header与Footer。那么,我们就先来定义好这两个东东的布局。比如说,我们定义一个如下的Header布局:

这个布局还是非常简单明了的。同样的,Footer布局的定义其实与Header是非常类似的,所以就不再贴一次代码了。
准备好Header与Footer布局后,我们应该考虑的工作,就是怎么把它们按照我们的需要给“放进”我们自己的PullableLayout当中了,其实这并不难。

private View mHeader,mFooter;

    public PullableLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHeader = LayoutInflater.from(context).inflate(R.layout.header_pullable_layout,null);
        mFooter = LayoutInflater.from(context).inflate(R.layout.footer_pullable_layout,null);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 看这里哦,亲
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams
                (RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT);
        mHeader.setLayoutParams(params);
        mFooter.setLayoutParams(params);
        addView(mHeader);
        addView(mFooter);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 测量
        for (int i = 0; i < getChildCount(); i++){
            View child = getChildAt(i);
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
        }
    }

    private int mLayoutContentHeight;
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mLayoutContentHeight = 0;
        // 置位
        for (int i = 0; i < getChildCount(); i++){
            View child = getChildAt(i);
            if (child == mHeader) { // 头视图隐藏在顶端
                child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
            } else if (child == mFooter) { // 尾视图隐藏在layout所有内容视图之后
                child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());
            } else { // 内容视图根据定义(插入)顺序,按由上到下的顺序在垂直方向进行排列
                child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());
                mLayoutContentHeight += child.getMeasuredHeight();
            }
        }
    }

以上的代码也并不复杂,核心的工作就是填充Header与Footer视图,并且按需要进行测量和置位的工作。如果作为新手来说,值得注意的可能就是:

  • Header与Footer的addView()工作:如果放在Constructor中,那么因为此时布局文件中的内容都还未进行装载和填充,就可能会在后续的代码中因为某些代码逻辑出现意料之外的异常错误;而如果放在onMeasure,则会因为onMeasure的内部机制造成重复add。所以放在onFinishInflate算是一个比较合适的选择。

  • 个人在这里定义了一个变量mLayoutContentHeight用来记录内容视图部分的实际总高度。需要注意的是,要在onLayout开头的地方将其置零,否则同样会因为重复累加得到错误的结果。

现在,当我们运行程序,就会在屏幕上呈现一个宽高占满屏幕的图片。目前看起来是与把ImageView放在其它常用的Layout中的效果是没有区别的。

所以,显然我们接下来要做的工作就是让视图能够跟随着我们的手指滚动起来。那么,还有什么好想的呢?自然就是覆写onTouchEvent了。

  private int mLastMoveY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastMoveY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int dy = mLastMoveY - y;
                scrollBy(0, dy);
                break;
        }

        mLastMoveY = y;
        return true;
    }

我们看到现在似乎已经有点意思了,但其实显然是远远不够的。现在说穿了就只是一个支持滚动的视图而已,看上去非常呆板,更别提下拉刷新此类了。

那么,我们想一下应该怎么改进呢?有了,我们可以给每次的拉动设置一些相关信息,比如“最大滚动距离,有效距离”等等。这是什么意思呢?
打个比方:当拉动的距离超过了最大距离,我们就不允许视图继续滚动了;而当此次拉动的距离超过有效距离我们就认为这是一次有效的行为。
那么现在我们先做点小改进,当拉动的距离超过有效距离,我们就将文字信息改为“松开刷新”,以提示用户你现在松开手指就会执行刷新的行为了。

            case MotionEvent.ACTION_MOVE:
                int dy = mLastMoveY - y;
                // dy < 0代表是针对下拉刷新的操作
                if(dy < 0) {
                    if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight() / 2) {
                        scrollBy(0, dy);
                        if(Math.abs(getScrollY()) >= effectiveScrollY){
                            tvPullHeader.setText("松开刷新");
                        }
                    }
                }
                break;

这里我们所做的改动实际就是:当进行下拉操作的时候,如果下拉距离已经达到header的一半高度,就不允许继续下拉了。
同时来说,如果当我们的拉动行为超过了有效距离effectiveScrollY,就提示用户可以“松开刷新”了。同样的,看看效果如何:

显然,我们又向前迈进了小小的一步。但最终的效果依旧有些呆板。因为虽然提示了可以“松开刷新”,但现在即使我们松开,也不会有任何效果。
松开手指却没有对应效果,显然是因为我们还没有在Action_Up的时候做对应的操作,那么现在就来进一步的修改吧:

            case MotionEvent.ACTION_UP:
                if(Math.abs(getScrollY()) >= effectiveScrollY){
                    mLayoutScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + effectiveScrollY));
                    tvPullHeader.setVisibility(View.GONE);
                    pbPullHeader.setVisibility(View.VISIBLE);
                }else{
                    mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
                }
                break;

因为仅仅是为了说明原理,所以这一步的改动代码也非常的简单。简单来说就是:如果松开手指时,滑动的距离并未超过有效距离,我们就认为这并不是一次成功有效的刷新行为,那么让view的位置变动恢复就行了。而如果手指离开时,已经滑动超过了有效驱离,则将view滑动到刚好能够让Header显示出有效距离的部分的位置,来提示用户正处于刷新的状态下。对应下面的效果图就更容易理解我们所做的工作是什么了:

让人高兴的是,到了这里看上去效果就很不错了。但虽然效果是有了,看上去像是在刷新,实际却没有执行任何实际用于刷新的操作。
所以说,显然我们还需要提供一个回调接口,让client端在使用的时候能够顺利在合适的时机执行需要的操作(刷新/加载)。

 public interface onRefreshListener{
        void onRefresh();
    }

    private onRefreshListener mRefreshListener;

    public void setRefreshListener(onRefreshListener listener){
        mRefreshListener = listener;
    }

    public void refreshDone(){
        mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
        pbPullHeader.setVisibility(View.GONE);
        tvPullHeader.setText("继续向下拉");
        tvPullHeader.setVisibility(View.VISIBLE);
    }

case MotionEvent.ACTION_UP:
if(Math.abs(getScrollY()) >= effectiveScrollY){
   // 省略之前的代码......

   // 执行回调
   mRefreshListener.onRefresh();
}else{
   mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
}
break;

public class MainActivity extends AppCompatActivity {
    private PullableLayout plMain;
    private ImageView iv;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            iv.setBackgroundResource(R.drawable.ace);
            plMain.refreshDone();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iv = (ImageView) findViewById(R.id.iv);
        plMain = (PullableLayout) findViewById(R.id.pl_main);
        plMain.setRefreshListener(new PullableLayout.onRefreshListener() {
            @Override
            public void onRefresh() {
                 new Thread(new Runnable() {
                     @Override
                     public void run() {
                         try {
                             Thread.sleep(3000);
                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }

                         mHandler.sendEmptyMessage(0);
                     }
                 }).start();
            }
        });
    }
}

OK,大功告成,现在我们在来看一看效果如何:

可以看到,到这里我们就已经完全实现了“下拉刷新”这一功能了。当然这里只是为了演示原理的demo,所以很多代码都没有那么的追求严谨。
当然,这里要总结的重点其实也只是个人的思路和实现原理而已。所以同理,只要理解了这种思路,“上拉加载”也同样就能够实现了,故不再赘述。

那么,是不是到了这里,我们就可以结束了呢?当然不是,因为之前我们说过需要让我们的PullableLayout是通用的。而以目前来说:
我们绝大多数普通的常用控件,是能够通用的。但是呢?对另一类以ListView,GridView,RecyclerView,ScrollView为代表的控件就不灵了。
显然,这类控件与普通的View相比,最大的特点就是:它们自身就是支持滚动的。所以无法避免的,就会与我们的控件出现“滑动冲突”。

那么,关于“滑动冲突”的解决方案,可以参考《Android开发艺术探索》,作者针对各种常见的滑动冲突都给出了非常实用的干货方案。
OK,这里我们假设以ListView与我们自定义的Layout配合使用为例。那么出现的滑动冲突就是,双方都需要处理上下滑动的行为。
《Android开发艺术探索》中已经说过,这种冲突往往都可以从业务逻辑上找到突破口。那么,我们来思考一下这个所谓的“突破口”:
显然,如果我们的ListView需要下拉刷新或者上拉加载,那么刷新行为的发生时机就是在ListView的内容已经到达最现有的最顶部时,再继续下拉。
同理,加载的行为发生的时机就是内容已经到达最现有的最底部时,继续上拉。所以,如此一分析,这个突破口就已经出现了:
以下拉行为为例,我们就应该在ListView未到达顶部的情况下,将滑动事件交给ListView处理。而如果已经到达顶部,就将事件拦截,自己处理

现在我们的思路已经明确了,接着要做的,自然就是将思路转化到代码上面了。其实,所谓的“滑动冲突”的处理,最终实际就是回归到在ViewGroup的onInterceptTouchEvent方法上根据业务逻辑处理事件的拦截。对应我们这里的需求来说,以ListView的下拉操作为例,就可以这样做:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        boolean intercept = false;
        // 记录此次触摸事件的y坐标
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercept = false;
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                if (y > mLastMoveY) { // 下滑操作
                    View child = getChildAt(0);
                    if (child instanceof AdapterView) {
                        AdapterView adapterChild = (AdapterView) child;
                        // 判断AbsListView是否已经到达内容最顶部(如果已经到达最顶部,就拦截事件,自己处理滑动)
                        if (adapterChild.getFirstVisiblePosition() == 0
                                || adapterChild.getChildAt(0).getTop() == 0) {
                            intercept = true;
                        }
                    }
                }

                break;
            }
            // Up事件
            case MotionEvent.ACTION_UP: {
                intercept = false;
                break;
            }
        }

        mLastMoveY = y;
        return intercept;
    }

好了,差不多就是这样了。再次说明这里主要旨在总结和分享一下个人对于此类需求的实现思路。当然大家可能会有更加优秀的实现方式,请多多指教!
另外,也可能有朋友注意到在最初的演示图中,使用了两个比较有趣的Loading动画。一个是下拉时的小幽灵,一个时上拉时的吃豆子的形象。
同样再次申明:这两种效果都来自Github上一位作者开源的库:https://github.com/ldoublem/LoadingView,里面有很多有意思的Loading效果。
个人而言,对那个小幽灵的形象比较有兴趣,所以也简单研究了下作者的源码。如果您也有兴趣,那也可以看一看我之前写的:用Canvas和属性动画造一只萌蠢的“小鬼”

  • 13
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
一个C语言编译器是一项非常复杂的任务,需要对编译原理、语言设计、数据结构与算法等多个领域有深入的理解和掌握。以下是一个简单的自制C语言编译器的步骤: 1. 设计语言规范:定义C语言的语法、语义、关键字、运算符、数据类型等规范。 2. 词法分析:编词法分析器,将源代码转换为令牌(token)序列,识别出关键字、标识符、运算符、常量等。 3. 语法分析:使用语法分析器将令牌序列转换为语法树(syntax tree),检查语法是否正确,建立抽象语法树(abstract syntax tree)。 4. 语义分析:对抽象语法树进行语义分析,检查类型、作用域、符号引用等语义信息。 5. 中间代码生成:将抽象语法树转换为中间代码(intermediate code),如三地址代码、四元式等。 6. 代码优化:对生成的中间代码进行优化,如常数合并、循环展开、函数内联、死代码删除等。 7. 目标代码生成:将优化后的中间代码转换为目标代码(target code),如汇编语言或机器码等。 8. 目标代码优化:对生成的目标代码进行优化,如指令选择、寄存器分配、代码调度等。 9. 目标代码链接:将生成的目标代码与库文件、其他目标代码链接成可执行程序。 以上是一个简单的自制C语言编译器的步骤,但实际上编一个完整的C语言编译器需要考虑更多的问题和细节。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值