Android自定義下拉刷新、上拉加載炫酷動畫效果


目    錄(本篇字數:2120)

介紹

實現思路和代碼

布局文件

下拉刷新實現

上拉加載實現

飛機轉頭和動畫代碼

博文續篇

為之前的自定義View添加DrawerLayout(側拉抽屜),為自定義View系列畫上完美句號。


  • 介紹

    繼續我上篇文章的內容:一步步實現ListView的Item側滑刪除菜單效果,仿QQ的聊天頁面側滑刪除,這篇我將給ListView加上上拉刷新、下拉加載的動畫效果。

    其實,這篇內容和上篇內容用到的原理、邏輯、思路及實現等基本都類似。所謂一通百通啊,真的是這樣,你只要掌握自定義View的一些套路,其實也不是很難嘛。

    主要解決問題(ListView 與下拉刷新、上拉加載的滑動沖突)

    先來看看我實現的效果,首先是上拉刷新的效果:

  • 效果圖

  • 實現思路和代碼

    那么看這樣實現,如果你沒做過的話,是不是覺得這個很復雜呢?其實並不然。首先,依然是我們的布局,布局分上、中、下三部分。上為上拉刷新內容、中為ListView、下為下拉加載內容。只要你清楚了這樣的布局,那么實現起來輕輕松松啊,有沒有?

  • 布局文件

    看一下我們的布局文件:

    <listview.example.x.slidelistview.RefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:background="@android:color/holo_red_dark"
            android:gravity="center_horizontal">

            <ProgressBar
                android:id="@+id/refresh_progress"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_gravity="bottom|right"
                tools:ignore="RtlHardcoded" />

            <TextView
                android:id="@+id/tv_refresh_state"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom|center_horizontal"
                android:layout_marginBottom="32dp"
                android:layout_marginTop="8dp"
                android:textColor="@android:color/white" />

            <ImageView
                android:id="@+id/iv_refreshing"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_gravity="center_horizontal|bottom"
                android:src="@drawable/ic_flight_black_24dp" />
        </FrameLayout>

        <ListView
            android:id="@+id/lv_contact"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:background="@android:color/holo_orange_dark"
            android:gravity="center">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:orientation="vertical">

                <ProgressBar
                    android:id="@+id/load_progress"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />

                <TextView
                    android:id="@+id/tv_load_state"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="正在加載"
                    android:textColor="@android:color/white" />
            </LinearLayout>

        </RelativeLayout>
    </listview.example.x.slidelistview.RefreshLayout>

    布局里的內容元素我就不做多的說明了,也沒什么好說明的。我們看最外層這個控件,是我自定義的繼承FrameLayout的一個RefreshLayout類。為什么用FrameLayout?我在上篇文章已經做了說明了,不清楚的依然可以在上面推薦鏈接點進去查看。首先,我們將這三個家伙進行布局,當然是從上到下的那種。來看看代碼:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mHeaderView.layout(0, -mHeaderHeight, mHeaderWidth, 0);
        mContentView.layout(0, 0, mContentWidth, mContentHeight);
        mFooterView.layout(0, mContentHeight, mFooterWidth, mContentHeight + mFooterHeight);
    }
  • 下拉刷新實現

    這就完成了我從上至下的布局。既然,我們把它布局到了屏幕上方,顯然是看不見的。現在只能通過手指將它滑動下來顯示,那么我們在touch事件做滑動處理,來看看代碼。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (firstDownTag == 0) {
                        /**
                         * 如果是第一次的話,因為事件傳遞原因
                         * onInterceptTouchEvent()執行了 ACTION_DOWN事件
                         * 標記了startY的值(這個值也許非常大,是根據手指按下的y坐標來定的)
                         * 關鍵是onTouchEvent的ACTION_DOWN無法得到執行,所以 scrollTo(0, disY);將直接移動到startY的位置
                         * 效果就是導致第一次向下拉,瞬間移動了非常多
                         */
                        firstDownTag++;
                    } else {
                        final float dy = y - startY;
                        int disY = (int) (getScrollY() - dy);
                        if (-disY <= 0) {
                            disY = 0;
                        }

                        if (-disY < mHeaderHeight) {
                            scrollTo(0, disY);
                            mRefreshProgress.setVisibility(INVISIBLE);
                            if (-disY < mRefreshHeight) {
                                tvRefreshText.setText("准備起飛");
                                startRefreshIcon();
                            } else {
                                tvRefreshText.setText("加速中");
                                stopRefreshIcon();
                            }
                        }
                    }
                }
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                if (-getScrollY() > mRefreshHeight) {
                    startRefreshing();
                } else {
                    stopRefreshing();
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isIntercept = false;
                upX = x;
                upY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (upY - y < 0) {
                        isIntercept = true;
                    } else if (y - upY < 0) {
                        isIntercept = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                upY = 0;
                upX = 0;
                break;
        }
        return isIntercept;
    }

    這里有一個大坑我們得爬,就是在RefreshLayout不攔截事件的時候,它默認會分發事件給ListView,導致ListView把touch事件給消費了,所以不攔截的情況下,盡管你怎么往下拉,它始終是拉不出來的。哈哈,那么解決方法就是我們攔截它。但是攔截總是有條件的,這個條件有兩點:

1、ListView的子項在第一個,也就是到達最頂部。

2、如果在ListView到達頂部前提下,手指還繼續往下滑動,那么就是下拉刷新的動作了,在此時攔截它。

   上面代碼就是做了這兩件事情,還有就是滑動動畫。當然,這得在我們RefreshLayout中實現對ListView的滑動監聽的接口,判斷是否處於頂部和底部:​​​​​​。還有一個就是我們的飛機動畫了,這比較簡單了。

  • 上拉加載實現

    既然說完了下拉刷新,下面我們來看看上拉加載動畫吧。

  • 上拉加載效果

    其實,上拉加載只是和我們的下拉刷新方向相反的。既然我們已經實現了下拉刷新,那么上拉加載還不是手到擒來嘛。因為我們前面已經處理過了事件沖突,所以可以一路向前,通暢無阻。

    我們看一下關鍵代碼,最主要的還是我們的touch事件的代碼,添加上拉加載的邏輯代碼,其他都非常簡單了:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (firstDownTag == 0) {
                        /**
                         * 如果是第一次的話,因為事件傳遞原因
                         * onInterceptTouchEvent()執行了 ACTION_DOWN事件
                         * 標記了startY的值(這個值也許非常大,是根據手指按下的y坐標來定的)
                         * 關鍵是onTouchEvent的ACTION_DOWN無法得到執行,所以 scrollTo(0, disY);將直接移動到startY的位置
                         * 效果就是導致第一次向下拉,瞬間移動了非常多
                         */
                        firstDownTag++;
                    } else {
                        final float dy = y - startY;
                        int disY = (int) (getScrollY() - dy);
                        if (-disY <= 0) {
                            disY = 0;
                        }

                        if (-disY < mHeaderHeight) {
                            scrollTo(0, disY);
                            mRefreshProgress.setVisibility(INVISIBLE);
                            if (-disY < mRefreshHeight) {
                                tvRefreshText.setText("准備起飛");
                                startRefreshIcon();
                            } else {
                                tvRefreshText.setText("加速中");
                                stopRefreshIcon();
                            }
                        }
                    }
                } else if (isBottom) {/** 在ListView底部,繼續上拉 **/
                    final float dy = y - startY;
                    int disY = (int) (getScrollY() - dy);
                    if (disY < 0) {
                        disY = 0;
                        ivLoadingIcon.setVisibility(VISIBLE);
                        mLoadingProgress.setVisibility(INVISIBLE);
                    } else if (disY >= mLoadingHeight) {
                        disY = mLoadingHeight + 5;
                    }
                    scrollTo(getScrollX(), disY);

//                    if (dy < 0) {
//                        startLoadingIcon();
//                    } else {
//                        stopLoadingIcon();
//                    }
                }
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                if (isTop) {
                    if (-getScrollY() > mRefreshHeight) {
                        startRefreshing();
                    } else {
                        stopRefreshing();
                    }
                } else if (isBottom) {
                    if (getScrollY() > mLoadingHeight) {
                        startLoading();
                    } else {
                        stopLoading();
                    }
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isIntercept = false;
                upX = downX = x;
                upY = downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    /** 下拉刷新攔截 **/
                    if (upY - y < 0) {
                        isIntercept = true;
                    } else if (y - upY < 0) {
                        isIntercept = false;
                    }
                } else if (isBottom) {
                    /** 上拉加載攔截 **/
                    if (y - downY < 0) {
                        isIntercept = true;
                    } else if (y - downY > 0) {
                        isIntercept = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                downX = upY = 0;
                downX = upX = 0;
                break;
        }
        return isIntercept;
    }
  • 飛機轉頭和動畫代碼

  private void stopRefreshing() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
        /**
         * ListView子項移動到第一個
         */
        mListView.setSelection(0);
        invalidate();
    }

    private void startRefreshing() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -mRefreshHeight - getScrollY());
        tvRefreshText.setText("起飛咯~");
        mRefreshProgress.setVisibility(VISIBLE);
        startIconAnimation();
        invalidate();
        /**
         * 模擬刷新完成,延遲關閉
         */
        handler.postDelayed(() -> stopRefreshing(), 2000);
    }

    private void startLoading() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, mFooterHeight - getScrollY());
        ivLoadingIcon.setVisibility(INVISIBLE);
        mLoadingProgress.setVisibility(VISIBLE);
        invalidate();
        handler.postDelayed(() -> stopLoading(), 1500);
    }

    private void stopLoading() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY(),1500);
        ivLoadingIcon.setVisibility(VISIBLE);
        mLoadingProgress.setVisibility(INVISIBLE);
        ivLoadingIcon.setPivotX(ivLoadingIcon.getWidth() / 2);
        ivLoadingIcon.setPivotY(ivLoadingIcon.getHeight() / 2);
        ivLoadingIcon.setRotation(180);
        invalidate();
    }

    private void startIconAnimation() {
        TranslateAnimation animation = new TranslateAnimation(0, 0,
                getScaleY(), -mRefreshHeight);
        animation.setFillAfter(false);
        animation.setDuration(2000);
        ivRefreshIcon.startAnimation(animation);
    }

    private void startRefreshIcon() {
        ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
        ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
        ivRefreshIcon.setRotation(180);
    }

    private void stopRefreshIcon() {
        ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
        ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
        ivRefreshIcon.setRotation(360);
    }

    那么,我們整個下拉刷新、上拉加載的最終效果:

博文續篇

為之前的自定義View添加DrawerLayout(側拉抽屜),為自定義View系列畫上完美句號。

©原文鏈接:https://blog.csdn.net/smile_Running/article/details/81950872

@作者博客:_Xu2WeI

@更多博文:查看作者的更多博文


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM