下拉刷新(Scroller 的使用)

前言

涉及知識點:

  • Scroller 使用
  • 自定義 ViewGroup
    • Measure 測量 View
  • onTouchEvent 函數(shù)

初始化視圖

public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener{

    protected Scroller mScroller;//滾動控制

    protected View mHeaderView;
    protected View mFooterView;
    protected T mContentView;

    protected int mYOffset;//本次觸摸 Y 軸坐標的偏移量
    protected int mInitScrollY = 0;//最初的滾動位置,第一次布局時滾動header的高度的距離
    protected int mLastY = 0;//最后一次觸摸 Y 軸坐標

    public static final int STATUS_IDLE = 0;//空閑狀態(tài)
    public static final int STATUS_PULL_TO_REFRESH = 1;//下拉或者上拉侣滩,但是沒有達到可以刷新的狀態(tài)
    public static final int STATUS_RELEASE_TO_REFRESH = 2;//下拉或者上拉狀態(tài)
    public static final int STATUS_REFRESHING = 3;//刷新中
    public static final int STATUS_LOADING = 4;//加載中

    protected int mCurrentStatus = STATUS_IDLE;

    private ImageView mArrowImageView;//header中的箭頭圖標
    private boolean isArrowUp;//箭頭是否向上
    private TextView mTipsTextView;//header 中的文本提示
    private TextView mTimeTextView;//header 中的時間提示
    private ProgressBar mProgressBar;//進度條
    private int mScreenHeight;//屏幕高度
    private int mHeaderHeight;// Header 高度

    protected OnRefreshListener mOnRefreshListener;//下拉刷新監(jiān)聽
    protected OnLoadListener mLoadListener;//加載更多回調(diào)

    public RefreshLayoutBase(Context context) {
        this(context, null);
    }

    public RefreshLayoutBase(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs);

        mScroller = new Scroller(context);

        mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;//獲取屏幕高度
        mHeaderHeight = mScreenHeight / 4;//header 高度為屏幕高度四分之一
        
        initLayout(context);
    }

    protected final void initLayout(Context context){
        //headerView
        setupHeaderView(context);
        //設置內(nèi)容視圖
        setupContentView(context);
        //設置布局參數(shù)
        setDefaultContentLayoutParams();
        //添加 contentView 布局
        addView(mContentView);
        //設置底部視圖
        setupFooterView(context);
    }

    /**
     * 設置布局參數(shù)
     * 給 ContentView 寬高設置為 match_parent
     */
    private void setDefaultContentLayoutParams() {
        ViewGroup.LayoutParams params =
                new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
        mContentView.setLayoutParams(params);
    }

    /**
     * 初始化 footerView
     * @param context
     */
    private void setupFooterView(Context context) {
        mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
                this, false);
        addView(mFooterView);
    }

    /**
     * 內(nèi)容視圖
     */
    protected abstract void setupContentView(Context context);

    /**
     * 初始化 header
     * @param context
     */
    private void setupHeaderView(Context context) {
        mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this, false);
        mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mHeaderHeight));
        mHeaderView.setBackgroundColor(Color.RED);
        //header 高度為屏幕 1/4,但是慢叨,他只有 100px 的有效顯示區(qū)域
        mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);//左上右下
        addView(mHeaderView);

        mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
        mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
        mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
        mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
    }

    /**if isTop return true
     * 達到頂部繼續(xù)下拉則攔截事件
     * @return
     */
    protected abstract boolean isTop();

    /**
     * if isBottom return true
     * 達到底部觸發(fā)加載更多
     * @return
     */
    protected abstract boolean isBottom();

/***************************************************
省略部分代碼壮锻,詳情見github
****************************************************/

}

首先是一大堆的變量,都加了注釋也沒什么說的蟆炊,后面用到再說吧阿趁。這里主要是 initLayout() 初始化了整個下拉刷新的布局膜蛔,從上到下 headerView、contentView脖阵、footerView皂股。其中 header 和 footer 都是從布局中加載的,固定好了的命黔。但是 contentView 是抽象的呜呐,可變的就斤,我們可以設置他是 ListView,RecyclerView蘑辑,GridView 等洋机,到時候繼承即可。

另外還有兩個函數(shù) isTop() 和 isBottom()洋魂,因為我們在下拉刷新和上拉加載更多的時候绷旗,不同的內(nèi)容視圖判斷到達底部頂部的代碼是不一樣的,所以把它也抽象了副砍。


測量寬高

MeasureSpec 的含義衔肢,組成

接下來要做的就是測量。View的測量師自定義View中最重要的一步豁翎。在貼代碼之前角骤,有幾個基本概念要搞清楚⌒陌看圖:

MeasureSpec.png

UNSPECIFIED 其實是開發(fā)人員按照自己的意愿調(diào)整大小邦尊,沒有任何限制。但是這種情況很少見刘陶。

EXACTLY 往往對應 match_parent, AT_MOST 往往對應 warp_content

getMeasureHeight() 和 getHeight() 區(qū)別

簡單地說 getMeasureHeight 可以測量到屏幕以外的布局胳赌, getHeight 測量到可視布局牢撼。

舉個例子匙隔,一個上拉加載更多的組件,contentView 是100dp熏版,footerView 是 50dp纷责,但是 footerView(加載更多視圖) 不可見。用getMeasureHeight 測量的高度就是 150dp撼短。

    /**
     * 測量 viewGroup 寬高再膳。寬度為用戶定義。高度是 header, contentView, footer 三者之和
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int childCount = getChildCount();//子視圖個數(shù)
        int finalHeight = 0;//最終的高度

        for(int i = 0; i < childCount; i++){
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);//測量每個子視圖
            finalHeight += child.getMeasuredHeight();
        }
        //設置下拉刷新組件的尺寸(也就是這個 ViewGroup )
        setMeasuredDimension(width, finalHeight);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 將 header曲横、content喂柒、footer 從上到下布局
     * 布局完成后通過 Scroller 滾動到 header 的底部
     * 滑動距離為 header 高度 + 本視圖 paddingtop,達到隱藏 header
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int top = getPaddingTop();
        for(int i=0; i<childCount; i++) {
            View child = getChildAt(i);
            child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
            top += child.getMeasuredHeight();
        }
        mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
        scrollTo(0, mInitScrollY);

    }

在onMeasure中測量了這個組件的自身大小和子視圖的大小禾嫉,并在onLayout中從上到下依次布局灾杰。

在 OnLayout 的最后通過 Scroller 將該 ViewGroup 滑動了 HeaderView 的高度使其不可見。


下拉刷新

當用戶向下按的時候熙参,判斷 ContentView 視圖滑動到了頂部艳吠。此時又通過 Scroller 將該組件向下滾動,使得 HeaderView 可見孽椰。這些功能都需要讓我們處理觸摸事件昭娩。

   /**
     * 攔截觸摸事件
     * 在 ContentView 滑動到頂部凛篙,并且下拉的時候攔截
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //獲取觸摸事件類型
        final int action = MotionEventCompat.getActionMasked(ev);
        //取消事件或者抬起事件直接返回false
        if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){
            return false;
        }

        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = (int) ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                mYOffset = (int) (ev.getRawY() - mLastY);
                //如果拉到了頂部,并且是下拉栏渺,攔截事件呛梆,轉(zhuǎn)到 onTouchEvent 處理下拉刷新
                if(isTop() && mYOffset > 0){
                    return true;
                }
                break;
            default:
                break;
        }
        return false;//false 默認不攔截
    }

onInterceptTouchEvent 是ViewGroup 中對觸摸事件進行攔截的函數(shù),返回 true時表示攔截磕诊。

例如:如果 mYOffset > 0削彬,那么代表用戶是從上往下滑動。如果此時 ContentView 已經(jīng)滑動到了頂部秀仲,那么第一個可見元素就是第一項融痛,返回 true 就是將后續(xù)的時間進行攔截。此時神僵,后續(xù)的 ACTION_MOVE 就會轉(zhuǎn)到 onTouchEvent 函數(shù)進行處理雁刷。

/**
 * 在這里處理下拉刷新或者上拉加載更多
 * @param event
 * @return
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch(event.getAction()){
        case MotionEvent.ACTION_MOVE:
            int currentY = (int) event.getRawY();
            mYOffset = currentY - mLastY;
            if(mCurrentStatus != STATUS_LOADING){
                changeScrollY(mYOffset);
            }
            rotateHeaderArrow();
            changeTips();
            mLastY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            //下拉刷新具體操作
            doRefresh();
            break;
        default:
            break;
    }
    return true;//返回 true,消費該事件
}

protected void doRefresh() {
  changeHeaderViewStaus();
  if(mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null){
    mOnRefreshListener.onRefresh();
  }
}

在 onTouchEvent 函數(shù)中保礼,判斷他的事件類型沛励。如果還是 MOVE,就計算 y 坐標的差值炮障,在此處調(diào)用 changeScrollY 函數(shù)目派,在 y 軸上滾動該控件,另外還有改變箭頭方向胁赢,文字提示等企蹭。如果是 ACTION_UP 手指抬起,說明松手了智末,就執(zhí)行下拉刷新操作谅摄。當然執(zhí)行的時候要判斷 y坐標下拉的偏移值夠不夠。

/**
 * 刷新結(jié)束時候調(diào)用系馆,視圖還原為基本狀態(tài)
 */
public void refreshComplete(){
    mScroller.startScroll(getScrollX(),getScrollY(),0,mInitScrollY - getScrollY());
    mCurrentStatus = STATUS_IDLE;
    invalidate();
    updateHeaderTimeStamp();

    //100毫秒之后處理arrow和progress送漠,免得太突兀
    this.postDelayed(new Runnable() {
        @Override
        public void run() {
            mArrowImageView.setVisibility(View.VISIBLE);
            mProgressBar.setVisibility(View.GONE);
        }
    },100);
}

本文僅僅是自己探究一下。其實個人覺得由蘑, MD 的 SwipeRefreshLayout 漂亮好用多了/手動滑稽


github:https://github.com/SHLURENJIA/PullRefresh

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闽寡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子尼酿,更是在濱河造成了極大的恐慌爷狈,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谓媒,死亡現(xiàn)場離奇詭異淆院,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門土辩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來支救,“玉大人,你說我怎么就攤上這事拷淘「髂” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵启涯,是天一觀的道長贬堵。 經(jīng)常有香客問我,道長结洼,這世上最難降的妖魔是什么黎做? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮松忍,結(jié)果婚禮上蒸殿,老公的妹妹穿的比我還像新娘。我一直安慰自己鸣峭,他們只是感情好宏所,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摊溶,像睡著了一般爬骤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上莫换,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天霞玄,我揣著相機與錄音,去河邊找鬼浓镜。 笑死溃列,一個胖子當著我的面吹牛劲厌,可吹牛的內(nèi)容都是我干的膛薛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼补鼻,長吁一口氣:“原來是場噩夢啊……” “哼哄啄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起风范,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤咨跌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后硼婿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锌半,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年寇漫,在試婚紗的時候發(fā)現(xiàn)自己被綠了刊殉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殉摔。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖记焊,靈堂內(nèi)的尸體忽然破棺而出逸月,到底是詐尸還是另有隱情,我是刑警寧澤遍膜,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布碗硬,位于F島的核電站,受9級特大地震影響瓢颅,放射性物質(zhì)發(fā)生泄漏恩尾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一挽懦、第九天 我趴在偏房一處隱蔽的房頂上張望特笋。 院中可真熱鬧,春花似錦巾兆、人聲如沸猎物。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔫磨。三九已至,卻和暖如春圃伶,著一層夾襖步出監(jiān)牢的瞬間堤如,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工窒朋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留搀罢,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓侥猩,卻偏偏與公主長得像榔至,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子欺劳,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

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