自定义下拉刷新布局
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;
}
}
}