博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
系列文章:
自定义 View(一)仿 QQ 列表 Item 侧拉删除功能
自定义 View(二)自己动手实现下拉刷新、上拉加载功能
自定义 View(三)仿 DrawerLayout 实现侧拉功能
继续我上篇文章的内容:Android进阶(一)为ListView的每项Item添加侧拉删除菜单按钮功能,这篇我将给ListView加上上拉刷新、下拉加载的动画效果。
其实,这篇内容和上篇内容用到的原理、逻辑、思路及实现等基本都类似。所谓一通百通啊,真的是这样,你只要掌握自定义View的一些套路,其实也不是很难嘛。
主要解决问题(ListView 与下拉刷新、上拉加载的滑动冲突)
先来看看我实现的效果,首先是上拉刷新的效果:
那么看这样实现,如果你没做过的话,是不是觉得这个很复杂呢?其实并不然。首先,依然是我们的布局,布局分上、中、下三部分。上为上拉刷新内容、中为ListView、下为下拉加载内容。只要你清楚了这样的布局,那么实现起来轻轻松松啊,有没有?
布局文件:
看一下我们的布局文件:
布局里的内容元素我就不做多的说明了,也没什么好说明的。我们看最外层这个控件,是我自定义的继承FrameLayout的一个RefreshLayout类。为什么用FrameLayout?我在上篇文章已经做了说明了,不清楚的依然可以在上面推荐链接点进去查看。首先,我们将这三个家伙进行布局,当然是从上到下的那种。来看看代码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHeaderView.layout(0, -mHeaderHeight, mHeaderWidth, 0);
mContentView.layout(0, 0, mContentWidth, mContentHeight);
mFooterView.layout(0, mContentHeight, mFooterWidth, mContentHeight + mFooterHeight);
}
这就完成了我从上至下的布局。既然,我们把它布局到了屏幕上方,显然是看不见的。现在只能通过手指将它滑动下来显示,那么我们在touch事件做滑动处理,来看看代码。
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
startY = y;
break;
case MotionEvent.ACTION_MOVE:
if (isTop) {
if (firstDownTag == 0) {
/**
* 如果是第一次的话,因为事件传递原因
* onInterceptTouchEvent()执行了 ACTION_DOWN事件
* 标记了startY的值(这个值也许非常大,是根据手指按下的y坐标来定的)
* 关键是onTouchEvent的ACTION_DOWN无法得到执行,所以 scrollTo(0, disY);将直接移动到startY的位置
* 效果就是导致第一次向下拉,瞬间移动了非常多
*/
firstDownTag++;
} else {
final float dy = y - startY;
int disY = (int) (getScrollY() - dy);
if (-disY <= 0) {
disY = 0;
}
if (-disY < mHeaderHeight) {
scrollTo(0, disY);
mRefreshProgress.setVisibility(INVISIBLE);
if (-disY < mRefreshHeight) {
tvRefreshText.setText("准备起飞");
startRefreshIcon();
} else {
tvRefreshText.setText("加速中");
stopRefreshIcon();
}
}
}
}
startX = x;
startY = y;
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
if (-getScrollY() > mRefreshHeight) {
startRefreshing();
} else {
stopRefreshing();
}
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
upX = x;
upY = y;
break;
case MotionEvent.ACTION_MOVE:
if (isTop) {
if (upY - y < 0) {
isIntercept = true;
} else if (y - upY < 0) {
isIntercept = false;
}
}
break;
case MotionEvent.ACTION_UP:
upY = 0;
upX = 0;
break;
}
return isIntercept;
}
这里有一个大坑我们得爬,就是在RefreshLayout不拦截事件的时候,它默认会分发事件给ListView,导致ListView把touch事件给消费了,所以不拦截的情况下,尽管你怎么往下拉,它始终是拉不出来的。哈哈,那么解决方法就是我们拦截它。但是拦截总是有条件的,这个条件有两点:
1、ListView的子项在第一个,也就是到达最顶部。
2、如果在ListView到达顶部前提下,手指还继续往下滑动,那么就是下拉刷新的动作了,在此时拦截它。
上面代码就是做了这两件事情,还有就是滑动动画。当然,这得在我们RefreshLayout中实现对ListView的滑动监听的接口,判断是否处于顶部和底部:。还有一个就是我们的飞机动画了,这比较简单了。
既然说完了下拉刷新,下面我们来看看上拉加载动画吧。
效果
其实,上拉加载只是和我们的下拉刷新方向相反的。既然我们已经实现了下拉刷新,那么上拉加载还不是手到擒来嘛。因为我们前面已经处理过了事件冲突,所以可以一路向前,通畅无阻。
我们看一下关键代码,最主要的还是我们的touch事件的代码,添加上拉加载的逻辑代码,其他都非常简单了:
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
startY = y;
break;
case MotionEvent.ACTION_MOVE:
if (isTop) {
if (firstDownTag == 0) {
/**
* 如果是第一次的话,因为事件传递原因
* onInterceptTouchEvent()执行了 ACTION_DOWN事件
* 标记了startY的值(这个值也许非常大,是根据手指按下的y坐标来定的)
* 关键是onTouchEvent的ACTION_DOWN无法得到执行,所以 scrollTo(0, disY);将直接移动到startY的位置
* 效果就是导致第一次向下拉,瞬间移动了非常多
*/
firstDownTag++;
} else {
final float dy = y - startY;
int disY = (int) (getScrollY() - dy);
if (-disY <= 0) {
disY = 0;
}
if (-disY < mHeaderHeight) {
scrollTo(0, disY);
mRefreshProgress.setVisibility(INVISIBLE);
if (-disY < mRefreshHeight) {
tvRefreshText.setText("准备起飞");
startRefreshIcon();
} else {
tvRefreshText.setText("加速中");
stopRefreshIcon();
}
}
}
} else if (isBottom) {/** 在ListView底部,继续上拉 **/
final float dy = y - startY;
int disY = (int) (getScrollY() - dy);
if (disY < 0) {
disY = 0;
ivLoadingIcon.setVisibility(VISIBLE);
mLoadingProgress.setVisibility(INVISIBLE);
} else if (disY >= mLoadingHeight) {
disY = mLoadingHeight + 5;
}
scrollTo(getScrollX(), disY);
// if (dy < 0) {
// startLoadingIcon();
// } else {
// stopLoadingIcon();
// }
}
startX = x;
startY = y;
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
if (isTop) {
if (-getScrollY() > mRefreshHeight) {
startRefreshing();
} else {
stopRefreshing();
}
} else if (isBottom) {
if (getScrollY() > mLoadingHeight) {
startLoading();
} else {
stopLoading();
}
}
break;
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
upX = downX = x;
upY = downY = y;
break;
case MotionEvent.ACTION_MOVE:
if (isTop) {
/** 下拉刷新拦截 **/
if (upY - y < 0) {
isIntercept = true;
} else if (y - upY < 0) {
isIntercept = false;
}
} else if (isBottom) {
/** 上拉加载拦截 **/
if (y - downY < 0) {
isIntercept = true;
} else if (y - downY > 0) {
isIntercept = false;
}
}
break;
case MotionEvent.ACTION_UP:
downX = upY = 0;
downX = upX = 0;
break;
}
return isIntercept;
}
private void stopRefreshing() {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
/**
* ListView子项移动到第一个
*/
mListView.setSelection(0);
invalidate();
}
private void startRefreshing() {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -mRefreshHeight - getScrollY());
tvRefreshText.setText("起飞咯~");
mRefreshProgress.setVisibility(VISIBLE);
startIconAnimation();
invalidate();
/**
* 模拟刷新完成,延迟关闭
*/
handler.postDelayed(() -> stopRefreshing(), 2000);
}
private void startLoading() {
mScroller.startScroll(getScrollX(), getScrollY(), 0, mFooterHeight - getScrollY());
ivLoadingIcon.setVisibility(INVISIBLE);
mLoadingProgress.setVisibility(VISIBLE);
invalidate();
handler.postDelayed(() -> stopLoading(), 1500);
}
private void stopLoading() {
mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY(),1500);
ivLoadingIcon.setVisibility(VISIBLE);
mLoadingProgress.setVisibility(INVISIBLE);
ivLoadingIcon.setPivotX(ivLoadingIcon.getWidth() / 2);
ivLoadingIcon.setPivotY(ivLoadingIcon.getHeight() / 2);
ivLoadingIcon.setRotation(180);
invalidate();
}
private void startIconAnimation() {
TranslateAnimation animation = new TranslateAnimation(0, 0,
getScaleY(), -mRefreshHeight);
animation.setFillAfter(false);
animation.setDuration(2000);
ivRefreshIcon.startAnimation(animation);
}
private void startRefreshIcon() {
ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
ivRefreshIcon.setRotation(180);
}
private void stopRefreshIcon() {
ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
ivRefreshIcon.setRotation(360);
}
那么,我们整个下拉刷新、上拉加载的最终效果: