微信朋友圈我们都经常用,朋友圈的下拉刷新比较有意思,我们今天将要模仿打造微信朋友圈的下拉刷新控件,当然微信的这种刷新设计可能不是最好的,实际项目中你可以用V4包里面的SwipeRefreshView或者Chris Banes的AndroidPullRerfresh,看产品经理的设计。
思路
我们初步分析下,界面上主要有二个控件,一个彩虹状的圆形LoadingView,一个是ListView,那么我大致可以有下面三个步骤:
第一步:需要自定义一个ViewGroup,把上面的2个控件add进来。
第二步:利用ViewDragHelper处理控件拖动。当ListView处于顶部时,如果继续向下拖动,就拦截触摸事件,将触摸事件传递给ViewDragHelper处理,这里比较关键,主要是是否拦截触摸事件的判断条件要处理好,否则如果ListView的点击和滚动事件被我们拦截了,那就悲剧了。
第三步:在ViewDragHelper的拖动回调方法里面,设置listView和彩虹LoadingView的位置,调用requestLayout。
第四步:手势松开后,开始刷新,LoadingView在固定位置做旋转动画。
第五步:如果设置了onRefreshListener,执行onRefresh接口。
第六步:调用stopRefresh,完成刷新,这一步需要控件使用者手动去调用,控件本身不自动触发。
代码实现
篇幅关系,我还是贴出部分关键代码,项目我惯例共享到Github了,大家可以去直接下载 https://github.com/aliouswang/FriendRefreshView
public class FriendRefreshView extends ViewGroup{
//圆形指示器
private ImageView mRainbowView;
private ListView mContentView;
//控件宽,高
private int sWidth;
private int sHeight;
private ViewDragHelper mDragHelper;
//contentView的当前top属性
private int currentTop;
//listView首个item
private int firstItem;
private boolean bScrollDown = false;
private boolean bDraging = false;
//圆形加载指示器最大top
private int rainbowMaxTop = 80;
//圆形加载指示器刷新时的top
private int rainbowStickyTop = 80;
//圆形加载指示器初始top
private int rainbowStartTop = -120;
//圆形加载指示器的半径
private int rainbowRadius = 100;
private int rainbowTop = - 120;
//圆形加载指示器旋转的角度
private int rainbowRotateAngle = 0;
private boolean bViewHelperSettling = false;
//刷新接口listener
private OnRefreshListener mRefreshLisenter;
private AbsListView.OnScrollListener onScrollListener;
private com.sw.library.widget.friendrefreshview.OnDetectScrollListener onDetectScrollListener;
public enum State {
NORMAL,
REFRESHING,
DRAGING
}
//控件当前状态
private State mState = State.NORMAL;
public FriendRefreshView(Context context) {
this(context, null);
}
public FriendRefreshView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FriendRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHandler();
initDragHelper();
initListView();
initRainbowView();
setBackgroundColor(Color.parseColor("#000000"));
onDetectScrollListener = this;
}
....
}
这里我们还是利用handler来处理LoadingView 执行刷新时的转动动画和stopRefresh时滚动到初始位置的位移动画。
/**
* 初始化handler,当ViewDragHelper释放了mContentView时,
* 我们通过循环发送消息刷新mRainbowView的位置和角度
*/
private void initHandler() {
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
if (rainbowTop > rainbowStartTop) {
rainbowTop -= 10;
requestLayout();
mHandler.sendEmptyMessageDelayed(0, 15);
}
break;
case 1:
if (rainbowTop <= rainbowStickyTop) {
if (rainbowTop < rainbowStickyTop) {
rainbowTop += 10;
if (rainbowTop > rainbowStickyTop) {
rainbowTop = rainbowStickyTop;
}
}
mRainbowView.setRotation(rainbowRotateAngle -= 10);
}else {
mRainbowView.setRotation(rainbowRotateAngle += 10);
}
requestLayout();
mHandler.sendEmptyMessageDelayed(1, 15);
break;
}
}
};
}
初始化ViewDragHelper,已经是我们的老朋友了,有不熟悉的朋友可以参考我上一篇分享--实现小米应用我的小米
/**
* 初始化mDragHelper,我们处理拖动的核心类
*/
private void initDragHelper() {
mDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View view, int i) {
return view == mContentView && !bViewHelperSettling;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if (changedView == mContentView) {
int lastContentTop = currentTop;
if (top >= 0) {
currentTop = top;
}else {
top = 0;
}
int lastTop = rainbowTop;
int rTop = top + rainbowStartTop;
if (rTop >= rainbowMaxTop) {
if (!isRefreshing()) {
rainbowRotateAngle += (currentTop - lastContentTop) * 2;
rTop = rainbowMaxTop;
rainbowTop = rTop;
mRainbowView.setRotation(rainbowRotateAngle);
}else {
rTop = rainbowMaxTop;
rainbowTop = rTop;
}
}else {
if (isRefreshing()) {
rainbowTop = rainbowStickyTop;
}else {
rainbowTop = rTop;
rainbowRotateAngle += (rainbowTop - lastTop) * 3;
mRainbowView.setRotation(rainbowRotateAngle);
}
}
requestLayout();
}
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
mDragHelper.settleCapturedViewAt(0, 0);
ViewCompat.postInvalidateOnAnimation(FriendRefreshView.this);
//如果手势释放时,拖动的距离大于rainbowStickyTop,开始刷新
if (currentTop >= rainbowStickyTop) {
startRefresh();
}
}
});
}
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
bViewHelperSettling = true;
}else {
bViewHelperSettling = false;
}
}
触摸事件的分发和拦截,核心部分。
/**
* 我们invoke 方法shouldIntercept来判断是否需要拦截事件,
* 拦截事件是为了将事件传递给mDragHelper来处理,我们这里只有当mContentView滑动到顶部
* 且mContentView没有处于滑动状态时才触发拦截。
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
mDragHelper.shouldInterceptTouchEvent(ev);
return shouldIntercept();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelper.processTouchEvent(event);
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
mLastMotionY = 0;
bDraging = false;
bScrollDown = false;
rainbowRotateAngle = 0;
break;
case MotionEvent.ACTION_MOVE:
int index = MotionEventCompat.getActionIndex(event);
int pointerId = MotionEventCompat.getPointerId(event, index);
if (shouldIntercept()) {
mDragHelper.captureChildView(mContentView, pointerId);
}
break;
}
return true;
}
/**
* 判断是否需要拦截触摸事件
* @return
*/
private boolean shouldIntercept() {
if (bDraging) return true;
int childCount = mContentView.getChildCount();
if (childCount > 0) {
View firstChild = mContentView.getChildAt(0);
if (firstChild.getTop() >= 0
&& firstItem == 0 && currentTop == 0
&& bScrollDown) {
return true;
}else return false;
}else {
return true;
}
}
/**
* 判断mContentView是否处于顶部
* @return
*/
private boolean checkIsTop() {
int childCount = mContentView.getChildCount();
if (childCount > 0) {
View firstChild = mContentView.getChildAt(0);
if (firstChild.getTop() >= 0
&& firstItem == 0 && currentTop == 0) {
return true;
}else return false;
}else {
return false;
}
}
measure和layout,我们的老朋友了,不多解释。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
sWidth = MeasureSpec.getSize(widthMeasureSpec);
sHeight = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
LayoutParams contentParams = (LayoutParams) mContentView.getLayoutParams();
contentParams.left = 0;
contentParams.top = 0;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
LayoutParams contentParams = (LayoutParams) mContentView.getLayoutParams();
mContentView.layout(contentParams.left, currentTop,
contentParams.left + sWidth, currentTop + sHeight);
mRainbowView.layout(rainbowRadius, rainbowTop,
rainbowRadius * 2 , rainbowTop + rainbowRadius);
}
自定义ListView,处理触摸事件
private float mLastMotionX;
private float mLastMotionY;
/**
* 对ListView的触摸事件进行判断,是否处于滑动状态
*/
private class FriendRefreshListView extends ListView {
public FriendRefreshListView(Context context) {
this(context, null);
}
public FriendRefreshListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FriendRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setBackgroundColor(Color.parseColor("#ffffff"));
}
/*当前活动的点Id,有效的点的Id*/
protected int mActivePointerId = INVALID_POINTER;
/*无效的点*/
private static final int INVALID_POINTER = -1;
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
if (mActivePointerId == INVALID_POINTER)
break;
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int indexMove = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, indexMove);
if (mActivePointerId == INVALID_POINTER) {
}else {
final float y = ev.getY();
float dy = y - mLastMotionY;
if (checkIsTop() && dy >= 1.0f) {
bScrollDown = true;
bDraging = true;
}else {
bScrollDown = false;
bDraging = false;
}
mLastMotionX = y;
}
break;
case MotionEvent.ACTION_UP:
mLastMotionY = 0;
break;
}
return super.onTouchEvent(ev);
}
}
public void setAdapter(BaseAdapter adapter) {
if (mContentView != null) {
mContentView.setAdapter(adapter);
}
}
暴露onRefreshListener接口,和startRefresh和stopRefresh方法,供外部调用。
Handler mHandler;
public void startRefresh() {
if (!isRefreshing()) {
mHandler.removeMessages(0);
mHandler.removeMessages(1);
mHandler.sendEmptyMessage(1);
mState = State.REFRESHING;
invokeListner();
}
}
private void invokeListner() {
if (mRefreshLisenter != null) {
mRefreshLisenter.onRefresh();
}
}
public void stopRefresh() {
mHandler.removeMessages(1);
mHandler.sendEmptyMessage(0);
mState = State.NORMAL;
}
public void setOnRefreshListener(OnRefreshListener listener) {
this.mRefreshLisenter = listener;
}
public interface OnRefreshListener {
public void onRefresh();
}
更多的细节,大家可以下源码参考,最后还是提供最终的运行效果图,因为附件有容量限制,我只好分成2部分上传了。