您的位置:首页 > 移动开发 > 微信开发

打造Android微信朋友圈下拉刷新控件

2016-07-26 15:42 357 查看
微信朋友圈我们都经常用,朋友圈的下拉刷新比较有意思,我们今天将要模仿打造微信朋友圈的下拉刷新控件,当然微信的这种刷新设计可能不是最好的,实际项目中你可以用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) {

4000

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部分上传了。

下拉刷新.gif

下拉刷新.gif

文/ALIOUS(简书作者)

原文链接:http://www.jianshu.com/p/1ca0caf5fd8b

著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: