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

微信朋友圈我們都經(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部分上傳了。

下拉刷新.gif
下拉刷新.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容