常听各种大神说,不能只限于会使用别人的框架,一定要会自己造轮子。做Android开发以来,自己也写了一些自定义的View,以前太懒没写过博客,最近觉得还是记下来吧,于是准备写这么一个系列,虽然完全不知道要写到什么时候。
说到下拉刷新上拉加载的原理嘛,其实很简单,自定义一个ViewGroup,加入3个子View,一个Header,一个Target,一个Footer,其中Header和Footer隐藏起来。之后监听Target的滑动状态,一旦Target向下拉拉不动就显示Header,上拉拉不动就显示Footer,完成版效果图如下:
public abstract class QLoadView extends ViewGroup{
public static final int STATE_REFRESH = 0;
public static final int STATE_NORMAL = 1;
public static final int STATE_PULLING = 2;
public static final int STATE_COMPLETE = 3;
public abstract void setNormal();
public abstract void setPulling();
public abstract void setRefreshing();
public abstract void setComplete();
这里在父类中定义4个状态及其4个改变状态的方法,代表如下:
状态 | 说明 |
---|---|
Normal |
|
Pulling |
|
Refreshing |
|
Complete |
|
一般来说,普通的的Header、Footer应该是逃不出这4种状态的。
LoadView搞定后,开始正儿八经的写RefreshLayout,这里我选择直接继承FrameLayout,定义几个成员变量并在onFinishInflate
时为其赋值。
private QLoadView mHeaderView;
private QLoadView mFooterView;
private View mTarget;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() > 1) {
throw new IllegalStateException("QRefreshLayout can
only have one child");
}
mTarget = getChildAt(0);
if (mHeaderView == null) {
setHeaderView(new HeaderView(getContext()));
}
if (mFooterView == null) {
setFooterView(new FooterView(getContext()));
}
public void setHeaderView(QLoadView view) {
if (view == mHeaderView) return;
if (mHeaderView != null) removeView(mHeaderView);
mHeaderView = view;
LayoutParams headerParams = new
LayoutParams(LayoutParams.MATCH_PARENT, 0);
addView(mHeaderView, headerParams);
}
public void setFooterView(QLoadView view) {
if (view == mFooterView) return;
if (mFooterView != null) removeView(mFooterView);
mFooterView = view;
LayoutParams footerParams = new
LayoutParams(LayoutParams.MATCH_PARENT, 0);
footerParams.gravity = Gravity.BOTTOM;
addView(mFooterView, footerParams);
}
先将Header和Footer的高度都设为0,这样就只有Target会显示,然后监听手指的滑动距离,动态的调整Header或者Footer的高度另其显示,这里我选择将Touch监听放到dispatchTouchEvent
中去做,这样可以很方便的在Touch事件传递给Target之前控制Header和Footer。
在dispatchTouchEvent
中,最重要的逻辑就是处理好ACTION_MOVE
,首先通过move的距离判断出手指是在向上滑还是向下滑并通过canTargetScrollUp()
和canTargetScrollDown()
来判断Target有没有滑到头,最终计算出要触发哪种模式(上拉刷新还是下拉加载)
case MotionEvent.ACTION_MOVE: {
float currY = event.getY();
float dy = currY - mTouchY;
mTouchY = currY;
if (dy > 0 && !canTargetScrollUp() && !mAction) {
mMode = MODE_REFRESH;
} else if (dy < 0 && mLoadMoreEnable &&
!canTargetScrollDown() && !mAction) {
mMode = MODE_LOADMORE;
}
handleScroll(dy);
break;
}
canTargetScrollUp()中使用ViewCompat.canScrollVertically(View v, int direction)
方法来判断Target是否还能继续滚动。
/** * Check if this view can be scrolled vertically in a certain direction. * @param v The View against which to invoke the method. * @param direction Negative to check scrolling up, positive to check * scrolling down. * @return true if this view can be scrolled in the specified direction, * false otherwise. */
观察这个方法的说明可以发现,第二个参数传负值可以检测view上方是否可以滚动,传正值可以检测view下方是否可以滚动。这里比较有意思,这个上方滚动其实正好相当于下拉,下方滚动相当于上拉,请看图:
跑题了....接着回来说,判断完是上拉下拉之后就进入到主逻辑,这里以下拉为例,根据手指移动的y坐标距离来设置Header的高度,同时设置Target的y坐标偏移量,这样就是一种随着手指不断的往下拉,Header就被拉出来的感觉了。
private void handleScroll(int dy){
LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
params.height += dy;
int dragIndex = Math.exp(-(params.height / 400f));
if (mDragIndex < 0) mDragIndex = 0;
params.height += dy * mDragIndex;
if (params.height > mRefreshHeight) {
syncLoadViewState(mHeaderView, QLoadView.STATE_PULLING);
}
mHeaderView.setLayoutParams(params);
mTarget.setTranslationY(params.height);
}
这里先通过计算得来的Header新高度计算出一个拖拽系数,然后用手指移动的距离乘以这个系数后再真正的赋给Header的Height,为啥要这么干呢?因为我们要制造一种下拉的阻尼感,越拉越难拉。我们知道,一个负数的N次幂等于这个数正数N次幂的倒数,用负指数的原理计算出这么一个系数,这样随着我们手指移动的距离越长,Header的高度增加的就越来越慢,给人一种越来越拖不动的感觉。
当Header的高度拖到我们规定的mRefreshHeight
时,就将Header的内容切换到Pulling状态,还记得定义的QLoadView父类么,这个时候派上用场了。
当拉动到规定的mRefreshHeight
时,抬起手指触发刷新,这个时候只需要将Header的内容切换到Refreshing状态即可,也可以设置一个Header在Refreshing状态的高度,使用ValueAnimator
将Header调整到此高度。如果抬起手指时没有达到mRefreshHeight
,把Header的内容切换到Normal状态并使用ValueAnimator
将Header高度调整到0再次隐藏起来。
int state = -1;
int height = view.getHeight();
if (height > mRefreshHeight) {
height = mFinalHeight;
state = QLoadView.STATE_REFRESH;
} else if (height < mRefreshHeight) {
height = 0;
state = QLoadView.STATE_NORMAL;
}
startPullAnime(view, height, null);
syncLoadViewState(view, state);
下拉刷新的主要逻辑到这就写完了,后续只需要给外部提供一个监听,像这样:
public interface RefreshHandler {
void onRefresh(QRefreshLayout refresh);
void onLoadMore(QRefreshLayout refresh);
}
并提供一个刷新完成的方法给外部调用
public void refreshComplete() {
mRefreshing = false;
mHeaderView.setComplete();
startPullAnime(mHeaderView, 0, new AnimeListener(mHeaderView));
}
刷新完成后将Header内容切换到Complete状态,并使用ValueAnimator
将Header高度调整到0隐藏,当动画结束后,再次将Header内容切换到Normal状态。
上拉加载功能和其原理一样,就不再过多叙述。
因为我们在RefreshLayout调用的都是QLoadView这个抽象父类的方法,所以我们可以随意的继承此类创建自己喜欢的Header、Footer。
同时还可以进行简单的扩展,比如设置一个Google下拉风格,下拉的时候只调整Header的高度,并不处理Target的y坐标,分分钟变成SwipeRefreshLayout。只需要加一句
if (mStyle == STYLE_CLASSIC)
mTarget.setTranslationY(height);
就这样,我们就实现了一个上拉下拉都有的RefreshLayout。
demo源码:https://github.com/qstumn/QRefreshLayout
感谢:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
网友评论