需求
昨天在网上找了好久都没有找到一个合适的可以下拉刷新的scrollView代码,不过github上有一个开源项目(Android-PullToRefresh)写得很不错,但好像必须是AbsListView的子类才能使用,当然自己写的这个和该项目的实现原理是一样的。想想自己学Android已经快一年了,总得努力努力自己搞搞,所以今天上午就查阅资料看博客开始写,下面就贴贴自己的思路和代码和下载地址。如果喜欢请star,如果觉得有纰漏或是有更好的点子请及时评论。
实现
咋们还是按思路一步一步往下走..
1.需要编写头部文件和布局,这个人们都知道,继承RelativeLayout,然后提供三个设置状态的方法,这里就不粘代码了,有需要的可以下载我的源码,下面开始干重要活…
2.extends ScrollView,将头部View,加到容器布局,设置头部的magrin值将头部headView隐藏
public class RefreshScrollView extends ScrollView {
private Context mContext;
// 容器布局,因为scroll只允许嵌套一个子布局
private LinearLayout mScrollContainer = null;
// 头部刷新的View
private ScrollViewHeader mRefreshHeaderView;
// 头部的高度
private int mHeaderViewHeight;
private final static float OFFSET_RADIO = 2.2f; // support iOS like pull
public CopyOfRefreshScrollView(Context context) {
this(context, null);
}
public CopyOfRefreshScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CopyOfRefreshScrollView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
initView();
}
/**
* 初始化view
*/
private void initView() {
// 添加头部布局到容器
mRefreshHeaderView = new ScrollViewHeader(mContext);
LinearLayout.LayoutParams headerViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
mScrollContainer = new LinearLayout(mContext);
mScrollContainer.addView(mRefreshHeaderView, headerViewParams);
mScrollContainer.setOrientation(LinearLayout.VERTICAL);
addView(mScrollContainer);
// 必须要测量一下头部view,要不然getMeasuredHeight()是0,
// 当然mRefreshHeaderView.getViewTreeObserver().addOnGlobalLayoutListener也可以
measureView(mRefreshHeaderView);
// 获取头部刷新headView的高度,设置margin让头部隐藏
mHeaderViewHeight = mRefreshHeaderView.getMeasuredHeight();
mRefreshHeaderView
.updateMargin(-mRefreshHeaderView.getMeasuredHeight());
}
/**
* 通知父布局,占用的宽,高
*
* @param view
*/
private void measureView(View view) {
ViewGroup.LayoutParams p = view.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
int height;
int tempHeight = p.height;
if (tempHeight > 0) {
height = MeasureSpec.makeMeasureSpec(tempHeight,
MeasureSpec.EXACTLY);
} else {
height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
view.measure(width, height);
}
3.这个时候我们可以新建一个activity试试看效果了,但一运行起来果断报异常了:java.lang.IllegalStateException:ScrollView can host only one direct child,这个没办法了总得解决一下吧。那么就新建一个方法setContainerView(View child) 吧?那如果非要在布局文件里面添加呢怎么办呢?所以只能够阅读源码了,发现scrollView在加载仅有的一个childView的时候会调用addView(View child, android.view.ViewGroup.LayoutParams params)方法,这下好了只要override这个方法就可以了,再次运行爽了。
/**
* 其他的addView 的方法也要override,暂且放一边
*/
@Override
public void addView(View child, android.view.ViewGroup.LayoutParams params) {
// 2.重载addView(View child, android.view.ViewGroup.LayoutParams params)方法
// 解决 java.lang.IllegalStateException
// 因为scrollView只许添加一个子布局,如果在xml中添加子布局,那么肯定会throw
// java.lang.IllegalStateException:ScrollView can host only one direct child
this.removeAllViews();
mScrollContainer.addView(child, params);
super.addView(mScrollContainer, mScrollContainer.getLayoutParams());
}
4.处理最重要的方法onTouchEvent的触摸事件
// 4.处理触摸事件,override onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
if(deltaY < 0 && mRefreshing){
// 如果往上滑并且是刷新的状态就不除阻力,要不然当头部出来的时候向上滑动怪怪的
updateHeader(deltaY);
}else if (getScrollY() == 0
&& (deltaY > 0 || mRefreshHeaderView.getTopMargin() > -mHeaderViewHeight)) {
// 更新headerView的高度,同时更改状态
updateHeader(deltaY/OFFSET_RADIO);
return true;
}
break;
default:
//这里没有使用action_up的原因是,可能会受到viewpager的影响接收到action_cacel事件
if (getScrollY() == 0) {
if (mRefreshHeaderView.getTopMargin() > 0 && mEnableRefresh && !mRefreshing)
{
mRefreshing = true;
mRefreshHeaderView.setState(ScrollViewHeader.STATE_REFRESHING);
// 刷新加载中,给调用者监听
if(mListener!=null){
mListener.onRefresh();
}
}
//重置RefreshHeaderView的高度
resetHeaderView();
}
break;
}
return super.onTouchEvent(ev);
}
/**
* 更新headerview的高度,同时更改状态
* @param deltY
*/
public void updateHeader(float deltY) {
int currentMargin = (int) (mRefreshHeaderView.getTopMargin() + deltY);
mRefreshHeaderView.updateMargin(currentMargin);
if(mEnableRefresh && !mRefreshing) {
if (currentMargin > mHeaderViewHeight/5) {
// 头部全部出来了,就显示松开刷新
mRefreshHeaderView.setState(ScrollViewHeader.STATE_READY);
} else {
// 否则显示下拉加载更多
mRefreshHeaderView.setState(ScrollViewHeader.STATE_NORMAL);
}
}
}
/**
* 重置RefreshHeaderView的高度
*/
public void resetHeaderView() {
int margin = mRefreshHeaderView.getTopMargin();
if(margin == -mHeaderViewHeight) {
return ;
}
if(margin < 0 && mRefreshing) {
// 当前已经在刷新,又重新进行拖动,但未拖满,不进行操作
return ;
}
int finalMargin = 0;
if(margin <= 0 && !mRefreshing) {
finalMargin = mHeaderViewHeight;
}
// 松开刷新,或者下拉刷新,又松手,没有触发刷新
// mAssistScroller辅助滚动 ,得要Override computeScroll()
// 如果头部的状态已经是隐藏的就没必要再去滚动了
if(this.getScrollY()<mHeaderViewHeight){
mAssistScroller.startScroll(0, -margin, 0, finalMargin + margin, SCROLL_DURATION);
invalidate();
}
}
@Override
public void computeScroll() {
if(mAssistScroller.computeScrollOffset()) {
mRefreshHeaderView.updateMargin(-mAssistScroller.getCurrY());
//继续重绘
postInvalidate();
}
super.computeScroll();
}
5.到第四步为止就基本大功告成了,最后就差几个提供给调用者的公共方法和刷新的监听。
/**
* 设置刷新的监听
* @param listener
*/
public void setOnRefreshScrollViewListener(OnRefreshScrollViewListener listener){
this.mListener = listener;
}
public interface OnRefreshScrollViewListener {
public void onRefresh();
}
/**
* 加载完成
*/
public void onLoadComplete(){
if(mRefreshing) {
mRefreshing = false;
resetHeaderView();
}
}
/**
* 设置scroll是否可以刷新
*
* @param enableRefresh
*/
public void setEnableRefresh(boolean enableRefresh) {
this.mEnableRefresh = enableRefresh;
}