← Back to list

自定义下拉刷新布局

Published on: | Views: 75

下拉刷新布局在很多地方用到,官方提供了SwipeRefreshLayout,也有很多第三方库可以使用,但是有时候我们需要做一些自定义刷新效果,下面来实现一个简单的下拉刷新布局,来初步了解下拉刷新布局原理和学习自定义ViewGroup。

基本原理

在子View的上方加上一个View作为刷新状态View,初始为隐藏状态,下拉时显示出来,松开后显示刷新状态,刷新完毕恢复为隐藏状态。 需要进行的工作包括:在目标view上方增加显示刷新状态view,显示不同的下拉状态,处理滑动冲突。 这里写图片描述

增加状态View和实现布局

采用垂直布局,类似LinearLayout。因为我们是继承ViewGroup,所以我们要自己处理onMeasure和onLayout。我们作一些约定来简化这个布局的工作:

  • 只包含一个子View或者一个ViewGroup
  • 不处理padding和margin

增加状态View到下拉刷新布局

写一个很简单的布局文件,仅用文字来描述刷新状态:

<!--pullable_layout_header_view.xml-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#8080">

    <TextView
        android:id="@+id/tv_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="center|bottom"
        android:text="继续下拉"/>
</FrameLayout>

然后在View的onCreate函数中实例化并添加到本布局:

mHeaderView = LayoutInflater.from(context).inflate(R.layout.pullable_layout_header_view, null,          false);
LayoutParams lp = generateDefaultLayoutParams();
lp.height = VIEW_HEIGHT;
lp.width = LayoutParams.MATCH_PARENT;
mHeaderView.setLayoutParams(lp);
addView(mHeaderView, 0);
mStatusView = (TextView) mHeaderView.findViewById(R.id.tv_status);

处理onMeasure

依次测量所有的子view,然后将所有子view的高作为本布局的高,子view的最大宽度作为本布局的宽度,设置一下测量值就可以了。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int totalLength = 0;
    int maxWidth = Integer.MIN_VALUE;
    int childMeasureState = 0;
    //将所有子View的高度加起来
    for (int i = 0; i < getChildCount(); i++) {
        View childView = getChildAt(i);
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        childMeasureState = combineMeasuredStates(childMeasureState, childView.getMeasuredState());
        totalLength += childView.getMeasuredHeight();
        maxWidth = Math.max(maxWidth, childView.getMeasuredWidth());
    }
    //和最小尺寸比较,选大的那个
    totalLength = Math.max(totalLength, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    //设置测量大小
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childMeasureState),
            resolveSizeAndState(totalLength, heightMeasureSpec, childMeasureState));
}

处理onLayout

从上到下依次摆放好所有的子view

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = l;
    int childTop = t;
    for (int i = 0; i < getChildCount(); i++) {
        View childView = getChildAt(i);
        childView.layout(
                childLeft,
                childTop,
                childLeft + childView.getMeasuredWidth(),
                childTop + childView.getMeasuredHeight());
        childTop += childView.getMeasuredHeight();
    }
    setStatus(STATUS_HIDE);
}

设计下拉刷新状态

下拉的状态有很多,定义一些常量来表示:

private final int STATUS_HIDE = 0;//隐藏状态
private final int STATUS_PULL_DOWN = 1;//下拉状态
private final int STATUS_SLOW_REFRESH = 2;//下拉到一半,松开后进行刷新状态
private final int STATUS_REFRESH = 3;//刷新状态

根据不同的状态,修改界面显示

private void setStatus(int newStatus) {
    Log.i("", "set status:" + newStatus);
    if (mStatus != newStatus) {
        mStatus = newStatus;
        switch (mStatus) {
            case STATUS_HIDE:
                scrollTo(0, VIEW_HEIGHT);
                break;
            case STATUS_PULL_DOWN:
                mStatusView.setText("继续下拉");
                break;
            case STATUS_SLOW_REFRESH:
                mStatusView.setText("松开刷新");
                break;
            case STATUS_REFRESH:
                mStatusView.setText("正在刷新...");
                //往上滚动一点,仅显示刷新文字的高度
                scrollTo(0, mHeaderView.getHeight() - mStatusView.getHeight());
                break;
        }
    }
}

响应下拉操作

事件拦截

先不考虑滑动冲突。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    //刷新状态下不需要拦截事件
    if (mStatus == STATUS_REFRESH) {
        return false;
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            //只处理下拉事件
            if (ev.getY() > mLastY) {
                return true;
            }
            break;
    }
    mLastY = (int) ev.getY();
    return false;
}

事件处理

@Override
public boolean onTouchEvent(MotionEvent event) {
    //刷新状态下不需要处理事件
    if (mStatus == STATUS_REFRESH) {
        return false;
    }
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            int dy = (int) (event.getY() - mLastY);
            //不处理上拉事件
            if (dy < 0) {
                return false;
            }
            //当HeaderView全部显示后,不允许再往下滚动
            int targetScrollY = getScrollY() - dy;
            if (targetScrollY >= 0) {
                scrollBy(0, -dy);
            }
            if (getScrollY() < REFRESH_HEIGHT) {
                setStatus(STATUS_SLOW_REFRESH);//HeaderView显示一定高度时进入松开刷新状态
            } else if (getScrollY() < VIEW_HEIGHT) {
                setStatus(STATUS_PULL_DOWN);//HeaderView开始显示时,进入继续下拉状态
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mStatus == STATUS_SLOW_REFRESH) {
                setStatus(STATUS_REFRESH);//松开刷新状态下松开时,进入刷新状态
            } else {
                setStatus(STATUS_HIDE); //否则恢复隐藏状态
            }
            break;
    }
    mLastY = (int) event.getY();
    return true;
}

处理滑动冲突

  • 不处理水平滑动
  • 目标view需要滑动时不进行下拉刷新
//参见SwipeRefreshLayoutView
protected boolean canChildScrollUp() {
    View targetView = getChildAt(1);
    if (Build.VERSION.SDK_INT < 14) {
        if (targetView instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) targetView;
            return absListView.getChildCount() > 0
                    && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
        } else {
            return ViewCompat.canScrollVertically(targetView, -1) || targetView.getScrollY() > 0;
        }
    } else {
        return ViewCompat.canScrollVertically(targetView, -1);
    }
}

修改事件拦截和事件处理,修改后代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    //刷新状态下不需要拦截事件
    if (canChildScrollUp() || mStatus == STATUS_REFRESH) {
        return false;
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            int dx = (int) (ev.getX() - mLastX);
            int dy = (int) (ev.getY() - mLastY);
            //不拦截水平滑动和上拉事件
            if (Math.abs(dy) > Math.abs(dx) && dy > 0) {
                return true;
            }
            break;
    }
    mLastY = (int) ev.getY();
    mLastX = (int) ev.getX();
    return false;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    //刷新状态下不需要处理事件
    if (canChildScrollUp() || mStatus == STATUS_REFRESH) {
        return false;
    }
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            int dx = (int) (event.getX() - mLastX);
            int dy = (int) (event.getY() - mLastY);
            //不处理水平滑动和上拉事件
            if (Math.abs(dy) <= Math.abs(dx) || dy < 0) {
                return false;
            }
            //当HeaderView全部显示后,不允许再往下滚动
            int targetScrollY = getScrollY() - dy;
            if (targetScrollY >= 0) {
                scrollBy(0, -dy);
            }
            if (getScrollY() < REFRESH_HEIGHT) {
                setStatus(STATUS_SLOW_REFRESH);//HeaderView显示一定高度时进入松开刷新状态
            } else if (getScrollY() < VIEW_HEIGHT) {
                setStatus(STATUS_PULL_DOWN);//HeaderView开始显示时,进入继续下拉状态
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mStatus == STATUS_SLOW_REFRESH) {
                setStatus(STATUS_REFRESH);//松开刷新状态下松开时,进入刷新状态
            } else {
                setStatus(STATUS_HIDE); //否则恢复隐藏状态
            }
            break;
    }
    mLastY = (int) event.getY();
    return true;
}

提供刷新接口

public interface OnPullDownRefreshListener {
    void onPullDownRefresh();
}
public void setOnPullDownRefreshListener(OnPullDownRefreshListener listener) {
    mOnPullDownRefreshListener = listener;
}

public void setRefreshFinished() {
    setStatus(STATUS_HIDE);
}

private void setStatus(int newStatus) {
   if (mStatus != newStatus) {
       mStatus = newStatus;
       switch (mStatus) {
           //...
           case STATUS_REFRESH:
               mStatusView.setText("正在刷新...");
               //往上滚动一点,仅显示状态View的高度
               scrollTo(0, mHeaderView.getHeight() - mStatusView.getHeight());
               if (mOnPullDownRefreshListener != null) {
                   //通知进行刷新
                   mOnPullDownRefreshListener.onPullDownRefresh();
               }
               break;
       }
   }
}

参考资料

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

完整代码

点这里下载完整代码