本人Android菜鸟,在做东西时候发现有时候我们需要在项目中使用scrollview和listview的结合才能使项目看起来更加完善,但是谷歌官网是不推荐scrollview和listview一起嵌套的,因为这俩个东西嵌套后会出现很多事件冲突。但是有时候就是需要这样的搭配怎么办?所以有些牛人自定义了view来满足这样的需求。好了,废话不多说,上代码
package com.example.pulldown;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
/**
*
* @author pengguichu
*
*/
public class PullDownScrollView extends LinearLayout {
private static final String TAG = "PullDownScrollView";
private int refreshTargetTop = -60;
private int headContentHeight;
private RefreshListener refreshListener;
private RotateAnimation animation;
private RotateAnimation reverseAnimation;
private final static int RATIO = 2;
private int preY = 0;
private boolean isElastic = false;
private int startY;
private int state;
private String note_release_to_refresh = "松开更新";
private String note_pull_to_refresh = "下拉刷新";
private String note_refreshing = "正在更新...";
private IPullDownElastic mElastic;
public PullDownScrollView(Context context) {
super(context);
init();
}
public PullDownScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
animation = new RotateAnimation(0, -180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
animation.setInterpolator(new LinearInterpolator());
animation.setDuration(250);
animation.setFillAfter(true);
reverseAnimation = new RotateAnimation(-180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
reverseAnimation.setInterpolator(new LinearInterpolator());
reverseAnimation.setDuration(200);
reverseAnimation.setFillAfter(true);
}
/**
* 刷新监听
* @param listener
*/
public void setRefreshListener(RefreshListener listener) {
this.refreshListener = listener;
}
/**
* 下拉布局
* @param elastic
*/
public void setPullDownElastic(IPullDownElastic elastic) {
mElastic = elastic;
headContentHeight = mElastic.getElasticHeight();
refreshTargetTop = - headContentHeight;
LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, headContentHeight);
lp.topMargin = refreshTargetTop;
addView(mElastic.getElasticLayout(), 0, lp);
}
/**
* 设置更新提示语
* @param pullToRefresh 下拉刷新提示语
* @param releaseToRefresh 松开刷新提示语
* @param refreshing 正在刷新提示语
*/
public void setRefreshTips(String pullToRefresh, String releaseToRefresh, String refreshing) {
note_pull_to_refresh = pullToRefresh;
note_release_to_refresh = releaseToRefresh;
note_refreshing = refreshing;
}
/*
* 该方法一般和ontouchEvent 一起用 (non-Javadoc)
*
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Logger.d(TAG, "onInterceptTouchEvent");
printMotionEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
preY = (int) ev.getY();
}
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
Logger.d(TAG, "isElastic:" + isElastic + " canScroll:"+ canScroll() + " ev.getY() - preY:"+(ev.getY() - preY));
if (!isElastic && canScroll()
&& (int) ev.getY() - preY >= headContentHeight / (3*RATIO)
&& refreshListener != null && mElastic != null) {
isElastic = true;
startY = (int) ev.getY();
Logger.i(TAG, "在move时候记录下位置startY:" + startY);
return true;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Logger.d(TAG, "onTouchEvent");
printMotionEvent(event);
handleHeadElastic(event);
return super.onTouchEvent(event);
}
private void handleHeadElastic(MotionEvent event) {
if (refreshListener != null && mElastic != null) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Logger.i(TAG, "down");
break;
case MotionEvent.ACTION_UP:
Logger.i(TAG, "up");
if (state != IPullDownElastic.REFRESHING && isElastic) {
if (state == IPullDownElastic.DONE) {
// 什么都不做
setMargin(refreshTargetTop);
}
if (state == IPullDownElastic.PULL_To_REFRESH) {
state = IPullDownElastic.DONE;
setMargin(refreshTargetTop);
changeHeaderViewByState(state, false);
Logger.i(TAG, "由下拉刷新状态,到done状态");
}
if (state == IPullDownElastic.RELEASE_To_REFRESH) {
state = IPullDownElastic.REFRESHING;
setMargin(0);
changeHeaderViewByState(state, false);
onRefresh();
Logger.i(TAG, "由松开刷新状态,到done状态");
}
}
isElastic = false;
break;
case MotionEvent.ACTION_MOVE:
Logger.i(TAG, "move");
int tempY = (int) event.getY();
if (state != IPullDownElastic.REFRESHING && isElastic) {
// 可以松手去刷新了
if (state == IPullDownElastic.RELEASE_To_REFRESH) {
if (((tempY - startY) / RATIO < headContentHeight)
&& (tempY - startY) > 0) {
state = IPullDownElastic.PULL_To_REFRESH;
changeHeaderViewByState(state, true);
Logger.i(TAG, "由松开刷新状态转变到下拉刷新状态");
} else if (tempY - startY <= 0) {
state = IPullDownElastic.DONE;
changeHeaderViewByState(state, false);
Logger.i(TAG, "由松开刷新状态转变到done状态");
}
}
if (state == IPullDownElastic.DONE) {
if (tempY - startY > 0) {
state = IPullDownElastic.PULL_To_REFRESH;
changeHeaderViewByState(state, false);
}
}
if (state == IPullDownElastic.PULL_To_REFRESH) {
// 下拉到可以进入RELEASE_TO_REFRESH的状态
if ((tempY - startY) / RATIO >= headContentHeight) {
state = IPullDownElastic.RELEASE_To_REFRESH;
changeHeaderViewByState(state, false);
Logger.i(TAG, "由done或者下拉刷新状态转变到松开刷新");
} else if (tempY - startY <= 0) {
state = IPullDownElastic.DONE;
changeHeaderViewByState(state, false);
Logger.i(TAG, "由DOne或者下拉刷新状态转变到done状态");
}
}
if (tempY - startY > 0) {
setMargin((tempY - startY)/2 + refreshTargetTop);
}
}
break;
}
}
}
/**
*
*/
private void setMargin(int top) {
LinearLayout.LayoutParams lp = (LayoutParams) mElastic.getElasticLayout()
.getLayoutParams();
lp.topMargin = top;
// 修改后刷新
mElastic.getElasticLayout().setLayoutParams(lp);
mElastic.getElasticLayout().invalidate();
}
private void changeHeaderViewByState(int state, boolean isBack) {
mElastic.changeElasticState(state, isBack);
switch (state) {
case IPullDownElastic.RELEASE_To_REFRESH:
mElastic.showArrow(View.VISIBLE);
mElastic.showProgressBar(View.GONE);
mElastic.showLastUpdate(View.VISIBLE);
mElastic.setTips(note_release_to_refresh);
mElastic.clearAnimation();
mElastic.startAnimation(animation);
Logger.i(TAG, "当前状态,松开刷新");
break;
case IPullDownElastic.PULL_To_REFRESH:
mElastic.showArrow(View.VISIBLE);
mElastic.showProgressBar(View.GONE);
mElastic.showLastUpdate(View.VISIBLE);
mElastic.setTips(note_pull_to_refresh);
mElastic.clearAnimation();
// 是由RELEASE_To_REFRESH状态转变来的
if (isBack) {
mElastic.startAnimation(reverseAnimation);
}
Logger.i(TAG, "当前状态,下拉刷新");
break;
case IPullDownElastic.REFRESHING:
mElastic.showArrow(View.GONE);
mElastic.showProgressBar(View.VISIBLE);
mElastic.showLastUpdate(View.GONE);
mElastic.setTips(note_refreshing);
mElastic.clearAnimation();
Logger.i(TAG, "当前状态,正在刷新...");
break;
case IPullDownElastic.DONE:
mElastic.showProgressBar(View.GONE);
mElastic.clearAnimation();
// arrowImageView.setImageResource(R.drawable.goicon);
// tipsTextview.setText("下拉刷新");
// lastUpdatedTextView.setVisibility(View.VISIBLE);
Logger.i(TAG, "当前状态,done");
break;
}
}
private void onRefresh() {
// downTextView.setVisibility(View.GONE);
// scroller.startScroll(0, i, 0, 0 - i);
// invalidate();
if (refreshListener != null) {
refreshListener.onRefresh(this);
}
}
/**
*
*/
@Override
public void computeScroll() {
// if (scroller.computeScrollOffset()) {
// int i = this.scroller.getCurrY();
// LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
// .getLayoutParams();
// int k = Math.max(i, refreshTargetTop);
// lp.topMargin = k;
// this.refreshView.setLayoutParams(lp);
// this.refreshView.invalidate();
// invalidate();
// }
}
/**
* 结束刷新事件,UI刷新完成后必须回调此方法
* @param text 一般传入:“上次更新时间:12:23”
*/
public void finishRefresh(String text) {
if (mElastic == null) {
Logger.e(TAG, "finishRefresh mElastic:" + mElastic);
return;
}
if (state == IPullDownElastic.DONE) {
Logger.e(TAG, "==> finishRefresh state has already done");
}
state = IPullDownElastic.DONE;
if (text != null) {
mElastic.setLastUpdateText(text);
}
changeHeaderViewByState(state,false);
Logger.i(TAG, "==>执行了=====finishRefresh "+ text);
mElastic.showArrow(View.VISIBLE);
mElastic.showLastUpdate(View.VISIBLE);
setMargin(refreshTargetTop);
// scroller.startScroll(0, i, 0, refreshTargetTop);
// invalidate();
}
private boolean canScroll() {
View childView;
if (getChildCount() > 1) {
childView = this.getChildAt(1);
if (childView instanceof AbsListView) {
int top = ((AbsListView) childView).getChildAt(0).getTop();
int pad = ((AbsListView) childView).getListPaddingTop();
if ((Math.abs(top - pad)) < 3
&& ((AbsListView) childView).getFirstVisiblePosition() == 0) {
return true;
} else {
return false;
}
} else if (childView instanceof ScrollView) {
if (((ScrollView) childView).getScrollY() == 0) {
return true;
} else {
return false;
}
}
}
return canScroll(this);
}
/**
* 子类重写此方法可以兼容其它的子控件,目前只兼容AbsListView和ScrollView
* @param view
* @return
*/
public boolean canScroll(PullDownScrollView view) {
return false;
}
private void printMotionEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Logger.d(TAG, "down");
break;
case MotionEvent.ACTION_MOVE:
Logger.d(TAG, "move");
break;
case MotionEvent.ACTION_UP:
Logger.d(TAG, "up");
default:
break;
}
}
/**
* 刷新监听接口
*/
public interface RefreshListener {
public void onRefresh(PullDownScrollView view);
}
}
既然官网不支持scrollview嵌套listview,那么我们就使用其他的View来代替scrollview,所以我参照了网上的一些代码,改了一些东西,重写的LinearLayout,将他的触摸事件改成了类似于scrollview,方法给了注释,想要研究的可以看看。
项目只需将PullDownScrollView.java这个类包裹scrollview就行,然后个给scrollview添加一个属性
android:scrollbars="none"
就可以了
然后你就可以布置自己的布局,但是当你加入listview时,你会发现你的listview只会显示几行(本博主显示俩行),然后我查了一些资料,说需要重新定义listview的高度,但是博主觉得重新定义高度有点麻烦,就想着既然都自定义视图了,干脆在定义listview不就得了,代码如下
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View.MeasureSpec;
import android.widget.ListView;
/**
*
* 不能在一个拥有Scrollbar的组件中嵌入另一个拥有Scrollbar的组件,因为这不科学,会混淆滑动事件,导致只显示一到两行数据。那么就换一种思路,
* 首先让子控件的内容全部显示出来,禁用了它的滚动。如果超过了父控件的范围则显示父控件的scrollbar滚动显示内容,思路是这样,一下是代码。
* 重载onMeasure方法:
*
* @author pengguichu
*
*/
public class MyListView extends ListView {
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/***
*
* 改变高度 其中onMeasure函数决定了组件显示的高度与宽度;
* makeMeasureSpec函数中第一个函数决定布局空间的大小,第二个参数是布局模式
* MeasureSpec.AT_MOST的意思就是子控件需要多大的控件就扩展到多大的空间
* 之后在ScrollView中添加这个组件就OK了,同样的道理,ListView也适用。
*/
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
代码很简单,只是继承了listview,重写了listview的onMeasure方法,让其先计算高度,再显示内容,将他替换掉你的listview,这样你的listview的内容就能全部显示出来了。
随后就是下拉刷新了,在MainActivity中获取到PullDownScrollView,实现其方法,代码如下
mPullDownScrollView = (PullDownScrollView) findViewById(R.id.refresh_root);
设置监听
mPullDownScrollView.setRefreshListener(this);
mPullDownScrollView.setPullDownElastic(new PullDownElasticImp(this));
然后实现为实现的方法
@Override
public void onRefresh(PullDownScrollView view) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
mPullDownScrollView.finishRefresh("上次刷新时间:12:23");
}
}, 2000);
}
那么下拉刷新就搞定了。但是此时你会发现,每当你进入界面或者下拉的时候,他会自动定义到listview的顶端,这样当你scrollview和listview中间还有一些其他的布局时,这样用户体验很不好,于是我就想,进入的时候我手动把scrollview滚到顶部。这样是解决了进入时会自动滚到listview顶部,但是,当你下拉时发现,卧槽,它还是会滚动到listview的顶部,手动滚动的方法不管用了。这个问题困扰了博主很久,问了很多人,都不能给出很明确的回答,于是我查看了文档,发现listview和scrollview都会有焦点。
就是当你手触摸屏幕时会触发焦点事件,然后博主灵光一闪,禁用listview的焦点是不是listview就不会得到事件了。当博主抱着试一试的态度设置了
listView.setFocusable(false);
发现下拉刷新不会自己滚动到listview的顶部了,一切都归于平静。
如果有大神发现有什么不足之处希望指出,或者有什么更好的办法解决类似以下需求的方法,
<scrollview>
<一些其他布局,需要动态刷新数据(如动态广告或者一些服务器更新了的内容)/>
<listview></listview>
</scroolview>
附上源码下载地址点击打开链接http://download.csdn.net/detail/pengguichu/9316271