微信朋友圈我們都經(jīng)常用,朋友圈的下拉刷新比較有意思,我們今天將要模仿打造微信朋友圈的下拉刷新控件,當(dāng)然微信的這種刷新設(shè)計可能不是最好的,實際項目中你可以用V4包里面的SwipeRefreshView或者Chris Banes的AndroidPullRerfresh,看產(chǎn)品經(jīng)理的設(shè)計。
思路
我們初步分析下,界面上主要有二個控件,一個彩虹狀的圓形LoadingView,一個是ListView,那么我大致可以有下面三個步驟:
第一步:需要自定義一個ViewGroup,把上面的2個控件add進來。
第二步:利用ViewDragHelper處理控件拖動。當(dāng)ListView處于頂部時,如果繼續(xù)向下拖動,就攔截觸摸事件,將觸摸事件傳遞給ViewDragHelper處理,這里比較關(guān)鍵,主要是是否攔截觸摸事件的判斷條件要處理好,否則如果ListView的點擊和滾動事件被我們攔截了,那就悲劇了。
第三步:在ViewDragHelper的拖動回調(diào)方法里面,設(shè)置listView和彩虹LoadingView的位置,調(diào)用requestLayout。
第四步:手勢松開后,開始刷新,LoadingView在固定位置做旋轉(zhuǎn)動畫。
第五步:如果設(shè)置了onRefreshListener,執(zhí)行onRefresh接口。
第六步:調(diào)用stopRefresh,完成刷新,這一步需要控件使用者手動去調(diào)用,控件本身不自動觸發(fā)。
代碼實現(xiàn)
篇幅關(guān)系,我還是貼出部分關(guān)鍵代碼,項目我慣例共享到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的當(dāng)前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;
//圓形加載指示器旋轉(zhuǎn)的角度
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
}
//控件當(dāng)前狀態(tài)
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 執(zhí)行刷新時的轉(zhuǎn)動動畫和stopRefresh時滾動到初始位置的位移動畫。
/**
* 初始化handler,當(dāng)ViewDragHelper釋放了mContentView時,
* 我們通過循環(huán)發(fā)送消息刷新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,已經(jīng)是我們的老朋友了,有不熟悉的朋友可以參考我上一篇分享--實現(xiàn)小米應(yīng)用我的小米
/**
* 初始化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;
}
}
觸摸事件的分發(fā)和攔截,核心部分。
/**
* 我們invoke 方法shouldIntercept來判斷是否需要攔截事件,
* 攔截事件是為了將事件傳遞給mDragHelper來處理,我們這里只有當(dāng)mContentView滑動到頂部
* 且mContentView沒有處于滑動狀態(tài)時才觸發(fā)攔截。
* @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的觸摸事件進行判斷,是否處于滑動狀態(tài)
*/
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"));
}
/*當(dāng)前活動的點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方法,供外部調(diào)用。
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();
}
更多的細(xì)節(jié),大家可以下源碼參考,最后還是提供最終的運行效果圖,因為附件有容量限制,我只好分成2部分上傳了。