Android自定義下拉重新整理控制元件

weixin_33751566發表於2017-06-29
4906791-0e918ffb5cf40c2e.jpg
abstract-blue-bright-1312488.jpg

自定義控制元件大家都會用,網上也一大堆,有些的確很炫酷,但是很難遇到在自己的專案中想要的效果。所以會一些基本的自定義控制元件還是必要的。我也是剛開始學習自定義控制元件,有興趣的可以看看,交流,指出我的不足之處

下拉重新整理控制元件是一個經常被自定義的控制元件,可以實現很炫的效果,一般情況,Google官方的SwipeRefreshLayout就很好用了,現在我要實現的效果如下:

4906791-381118c1dcc36384.gif
G_1109174541.gif

gif可能有點慢,在實際效果很流暢的,顯示效果也可能更好。
剛開始做自定義控制元件一開始就想自己從零實現真的難,所以先看看別人是怎麼做的,然後自己照著模仿,改動,然後自己實現。我先看了郭霖的自定義ListView下拉重新整理控制元件自己改的,然後瞭解了其中過程,基本上這類的下拉重新整理效果自己去實現就沒什麼問題了。

實現思路:

這裡我們將採取的方案是使用組合View的方式,先自定義一個佈局繼承自LinearLayout,然後在這個佈局中加入下拉頭和ListView這兩個子元素,並讓這兩個子元素縱向排列。初始化的時候,讓下拉頭向上偏移出螢幕,這樣我們看到的就只有ListView了。然後對ListView的touch事件進行監聽,如果當前ListView已經滾動到頂部並且手指還在向下拉的話,那就將下拉頭顯示出來,鬆手後進行重新整理操作,並將下拉頭隱藏。原理示意圖如下:

4906791-33b9d18be427498b

先新建一個佈局作為下拉的頭部:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pull_to_refresh_head"
    android:layout_width="match_parent"
    android:layout_height="360dp">

    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="360dp"
        android:elevation="2dp"
        android:gravity="center">

        <ImageView
            android:id="@+id/iv_triangle"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_marginTop="130dp"
            android:src="@mipmap/ic_triangle" />

    </LinearLayout>


    <ImageView
        android:layout_width="match_parent"
        android:layout_height="360dp"
        android:scaleType="centerCrop"
        android:src="@mipmap/bg_refresh" />


</RelativeLayout>

裡面很簡單就是一個ImageView顯示重新整理的那個旋轉的三角形,另一個就是下拉頭的背景。

然後新建一個RefreshableView繼承自LinearLayout,程式碼如下所示:

package myview;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.yorhp.refreshview.R;

import static android.R.attr.fromDegrees;
import static android.R.attr.pivotX;
import static android.R.attr.pivotY;
import static android.R.attr.toDegrees;
import static android.icu.lang.UCharacter.GraphemeClusterBreak.L;

public class RefreshableViewList extends LinearLayout implements View.OnTouchListener {

    /**
     * 下拉狀態
     */
    public static final int STATUS_PULL_TO_REFRESH = 0;

    /**
     * 釋放立即重新整理狀態
     */
    public static final int STATUS_RELEASE_TO_REFRESH = 1;

    /**
     * 正在重新整理狀態
     */
    public static final int STATUS_REFRESHING = 2;

    /**
     * 重新整理完成或未重新整理狀態
     */
    public static final int STATUS_REFRESH_FINISHED = 3;

    /**
     * 下拉頭部回滾的速度
     */
    public static final int SCROLL_SPEED = -30;


    /**
     * 下拉的長度
     */

    private int pullLength;


    /**
     * 下拉重新整理的回撥介面
     */
    private PullToRefreshListener mListener;


    /**
     * 下拉頭的View
     */
    private View header;

    /**
     * 需要去下拉重新整理的ListView
     */
    private ListView listView;

    /**
     * 重新整理時顯示的進度條
     */
   // private ProgressBar progressBar;


    //三角形
    private ImageView iv_triangle;

    /**
     * 指示下拉和釋放的箭頭
     */
   // private ImageView arrow;

    /**
     * 指示下拉和釋放的文字描述
     */
    //private TextView description;


    /**
     * 下拉頭的佈局引數
     */
    private MarginLayoutParams headerLayoutParams;


    /**
     * 為了防止不同介面的下拉重新整理在上次更新時間上互相有衝突,使用id來做區分
     */
    private int mId = -1;

    /**
     * 下拉頭的高度
     */
    private int hideHeaderHeight;

    /**
     * 當前處理什麼狀態,可選值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
     * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
     */
    private int currentStatus = STATUS_REFRESH_FINISHED;
    ;

    /**
     * 記錄上一次的狀態是什麼,避免進行重複操作
     */
    private int lastStatus = currentStatus;

    /**
     * 手指按下時的螢幕縱座標
     */
    private float yDown;

    /**
     * 在被判定為滾動之前使用者手指可以移動的最大值。
     */
    private int touchSlop;

    /**
     * 是否已載入過一次layout,這裡onLayout中的初始化只需載入一次
     */
    private boolean loadOnce;

    /**
     * 當前是否可以下拉,只有ListView滾動到頭的時候才允許下拉
     */
    private boolean ableToPull;

    /**
     * 下拉重新整理控制元件的建構函式,會在執行時動態新增一個下拉頭的佈局。
     *
     * @param context
     * @param attrs
     */
    public RefreshableViewList(Context context, AttributeSet attrs) {
        super(context, attrs);
        header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);
        iv_triangle = (ImageView) header.findViewById(R.id.iv_triangle);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        setOrientation(VERTICAL);
        addView(header, 0);
    }

    /**
     * 進行一些關鍵性的初始化操作,比如:將下拉頭向上偏移進行隱藏,給ListView註冊touch事件。
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed && !loadOnce) {
            hideHeaderHeight = -header.getHeight();
            pullLength = hideHeaderHeight / 4 * 3;
            headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
            headerLayoutParams.topMargin = hideHeaderHeight;
            listView = (ListView) getChildAt(1);
            listView.setOnTouchListener(this);
            loadOnce = true;
        }
    }


    int preDistance = 0;

    /**
     * 當ListView被觸控時呼叫,其中處理了各種下拉重新整理的具體邏輯。
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        setIsAbleToPull(event);
        if (ableToPull) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    yDown = event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float yMove = event.getRawY();
                    int distance = (int) (yMove - yDown);
                    // 如果手指是下滑狀態,並且下拉頭是完全隱藏的,就遮蔽下拉事件
                    if (distance <= pullLength && headerLayoutParams.topMargin <= hideHeaderHeight) {
                        return false;
                    }
                    if (distance < touchSlop) {
                        return false;
                    }
                    if (currentStatus != STATUS_REFRESHING) {
                        if (headerLayoutParams.topMargin > pullLength) {
                            currentStatus = STATUS_RELEASE_TO_REFRESH;
                        } else {
                            currentStatus = STATUS_PULL_TO_REFRESH;
                        }
                        // 通過偏移下拉頭的topMargin值,來實現下拉效果,修改分母可以有不同的拉力效果
                        headerLayoutParams.topMargin = (int) ((distance / 2.8) + hideHeaderHeight);
                        header.setLayoutParams(headerLayoutParams);
                        //新增動畫(這裡是手指控制滑動的動畫)、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
                        rotateTriangle((distance - preDistance)/2);
                        preDistance=distance;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                default:
                    if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
                        // 鬆手時如果是釋放立即重新整理狀態,就去呼叫正在重新整理的任務
                        new RefreshingTask().execute();
                    } else if (currentStatus == STATUS_PULL_TO_REFRESH) {
                        // 鬆手時如果是下拉狀態,就去呼叫隱藏下拉頭的任務
                        new HideHeaderTask().execute();
                    }
                    break;
            }
            // 時刻記得更新下拉頭中的資訊  
            if (currentStatus == STATUS_PULL_TO_REFRESH
                    || currentStatus == STATUS_RELEASE_TO_REFRESH) {
                updateHeaderView();
                // 當前正處於下拉或釋放狀態,要讓ListView失去焦點,否則被點選的那一項會一直處於選中狀態  
                listView.setPressed(false);
                listView.setFocusable(false);
                listView.setFocusableInTouchMode(false);
                lastStatus = currentStatus;
                // 當前正處於下拉或釋放狀態,通過返回true遮蔽掉ListView的滾動事件  
                return true;
            }
        }
        return false;
    }

    /**
     * 給下拉重新整理控制元件註冊一個監聽器。
     *
     * @param listener 監聽器的實現。
     * @param id       為了防止不同介面的下拉重新整理在上次更新時間上互相有衝突, 請不同介面在註冊下拉重新整理監聽器時一定要傳入不同的id。
     */
    public void setOnRefreshListener(PullToRefreshListener listener, int id) {
        mListener = listener;
        mId = id;
    }

    /**
     * 當所有的重新整理邏輯完成後,記錄呼叫一下,否則你的ListView將一直處於正在重新整理狀態。
     */
    public void finishRefreshing() {
        currentStatus = STATUS_REFRESH_FINISHED;
        new HideHeaderTask().execute();
    }

    /**
     * 根據當前ListView的滾動狀態來設定 {@link #ableToPull}
     * 的值,每次都需要在onTouch中第一個執行,這樣可以判斷出當前應該是滾動ListView,還是應該進行下拉。
     *
     * @param event
     */
    private void setIsAbleToPull(MotionEvent event) {
        View firstChild = listView.getChildAt(0);
        if (firstChild != null) {
            int firstVisiblePos = listView.getFirstVisiblePosition();
            if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
                if (!ableToPull) {
                    yDown = event.getRawY();
                }
                // 如果首個元素的上邊緣,距離父佈局值為0,就說明ListView滾動到了最頂部,此時應該允許下拉重新整理  
                ableToPull = true;
            } else {
                if (headerLayoutParams.topMargin != hideHeaderHeight) {
                    headerLayoutParams.topMargin = hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull = false;
            }
        } else {
            // 如果ListView中沒有元素,也應該允許下拉重新整理  
            ableToPull = true;
        }
    }

    /**
     * 更新下拉頭中的資訊。
     */
    private void updateHeaderView() {
        if (lastStatus != currentStatus) {
            if (currentStatus == STATUS_PULL_TO_REFRESH) {  //下拉狀態

            } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {  //釋放狀態

            } else if (currentStatus == STATUS_REFRESHING) {  //重新整理中

                iv_triangle.clearAnimation();
                TriangelRotate();
            }
        }
    }

    float preDegres = 0;

    //手指下拉的時候的動畫
    private void rotateTriangle(float angle) {
        float pivotX = iv_triangle.getWidth() /2;
        float pivotY = (float) (iv_triangle.getHeight() /1.6);
        float fromDegrees = preDegres;
        float toDegrees = angle;

        RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
        animation.setDuration(10);
        animation.setFillAfter(true);
        iv_triangle.startAnimation(animation);
        preDegres = preDegres + angle;
    }

    //重新整理的時候一直轉的動畫
    private void TriangelRotate(){
        float pivotX = iv_triangle.getWidth() /2;
        float pivotY = (float) (iv_triangle.getHeight() /1.6);
        RotateAnimation animation = new RotateAnimation(0f, 120f, pivotX, pivotY);
        animation.setDuration(50);
        animation.setRepeatMode(Animation.RESTART);
        animation.setRepeatCount(Animation.INFINITE);
        preDegres = 0;
        LinearInterpolator linearInterpolator=new LinearInterpolator();
        animation.setInterpolator(linearInterpolator);
        iv_triangle.startAnimation(animation);
    }




    /**
     * 正在重新整理的任務,在此任務中會去回撥註冊進來的下拉重新整理監聽器。
     * 下拉超過了,要返回到重新整理的位置
     *
     * @author guolin
     */
    class RefreshingTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            while (true) {
                topMargin = topMargin + SCROLL_SPEED;
                if (topMargin <= pullLength) {
                    topMargin = pullLength;
                    break;
                }
                publishProgress(topMargin);
                sleep(10);
            }
            currentStatus = STATUS_REFRESHING;
            publishProgress(pullLength);
            if (mListener != null) {
                mListener.onRefresh();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... topMargin) {
            updateHeaderView();
            headerLayoutParams.topMargin = topMargin[0];
            header.setLayoutParams(headerLayoutParams);
            //新增動畫(這裡是手指鬆開後返回到重新整理位置的動畫)、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
        }

    }

    /**
     * 隱藏下拉頭的任務,當未進行下拉重新整理或下拉重新整理完成後,此任務將會使下拉頭重新隱藏。
     *
     * @author guolin
     */
    class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {

        @Override
        protected Integer doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            while (true) {
                topMargin = topMargin + SCROLL_SPEED;
                if (topMargin <= hideHeaderHeight) {
                    topMargin = hideHeaderHeight;
                    break;
                }
                publishProgress(topMargin);
                sleep(10);
            }
            return topMargin;
        }

        @Override
        protected void onProgressUpdate(Integer... topMargin) {
            headerLayoutParams.topMargin = topMargin[0];
            header.setLayoutParams(headerLayoutParams);
            //新增動畫(這裡是手指鬆開後返回到初始位置的動畫)、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

        }

        @Override
        protected void onPostExecute(Integer topMargin) {
            headerLayoutParams.topMargin = topMargin;
            header.setLayoutParams(headerLayoutParams);
            currentStatus = STATUS_REFRESH_FINISHED;
            //完成重新整理、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
            iv_triangle.clearAnimation();
        }
    }

    /**
     * 使當前執行緒睡眠指定的毫秒數。
     *
     * @param time 指定當前執行緒睡眠多久,以毫秒為單位
     */
    private void sleep(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 下拉重新整理的監聽器,使用下拉重新整理的地方應該註冊此監聽器來獲取重新整理回撥。
     *
     * @author guolin
     */
    public interface PullToRefreshListener {

        /**
         * 重新整理時會去回撥此方法,在方法內編寫具體的重新整理邏輯。注意此方法是在子執行緒中呼叫的, 你可以不必另開執行緒來進行耗時操作。
         */
        void onRefresh();

    }

}  

程式碼說明

這個類是整個下拉重新整理功能中最重要的一個類,註釋已經寫得比較詳細了,我再簡單解釋一下。首先在RefreshableView的建構函式中動態新增了剛剛定義的pull_to_refresh這個佈局作為下拉頭,然後在onLayout方法中將下拉頭向上偏移出了螢幕,再給ListView註冊了touch事件。之後每當手指在ListView上滑動時,onTouch方法就會執行。在onTouch方法中的第一行就呼叫了setIsAbleToPull方法來判斷ListView是否滾動到了最頂部,只有滾動到了最頂部才會執行後面的程式碼,否則就視為正常的ListView滾動,不做任何處理。當ListView滾動到了最頂部時,如果手指還在向下拖動,就會改變下拉頭的偏移值,讓下拉頭顯示出來,下拉的距離設定為手指移動距離的1/2.8,這樣才會有拉力的感覺。如果下拉的距離足夠大,在鬆手的時候就會執行重新整理操作,如果距離不夠大,就僅僅重新隱藏下拉頭。

具體的重新整理操作會在RefreshingTask中進行,其中在doInBackground方法中回撥了PullToRefreshListener介面的onRefresh方法,這也是大家在使用RefreshableView時必須要去實現的一個介面,因為具體重新整理的邏輯就應該寫在onRefresh方法中,後面會演示使用的方法。

你可能一下子看起來覺得很多程式碼,但是如果你把自己實現的那些可以修改的程式碼刪除後看起來就簡單很多了。然後你再把自己的邏輯新增進去,你的程式碼可能別人看起來也很難的。最重要的是自己要親手做,簡單地看可能怎麼也看不會。

下拉重新整理那自然要適配RecyclerView和NestedScrollView,實現起來也很簡單,就是在判定列表是否滑動到頂部的時候程式碼改動一下。

//ListView判定方法
  private void setIsAbleToPull(MotionEvent event) {
        View firstChild = listView.getChildAt(0);
        if (firstChild != null) {
            int firstVisiblePos = listView.getFirstVisiblePosition();
            if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
                if (!ableToPull) {
                    yDown = event.getRawY();
                }
                // 如果首個元素的上邊緣,距離父佈局值為0,就說明ListView滾動到了最頂部,此時應該允許下拉重新整理  
                ableToPull = true;
            } else {
                if (headerLayoutParams.topMargin != hideHeaderHeight) {
                    headerLayoutParams.topMargin = hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull = false;
            }
        } else {
            // 如果ListView中沒有元素,也應該允許下拉重新整理  
            ableToPull = true;
        }
    }
    
//RecyclerView判定方法
 private void setIsAbleToPull(MotionEvent event) {
        View firstChild = listView.getChildAt(0);
        if (firstChild != null) {
            LinearLayoutManager lm = (LinearLayoutManager) listView.getLayoutManager();
            int firstVisiblePos = lm.findFirstVisibleItemPosition();
            if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
                if (!ableToPull) {
                    yDown = event.getRawY();
                }
                // 如果首個元素的上邊緣,距離父佈局值為0,就說明ListView滾動到了最頂部,此時應該允許下拉重新整理  
                ableToPull = true;
            } else {
                if (headerLayoutParams.topMargin != hideHeaderHeight) {
                    headerLayoutParams.topMargin = hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull = false;
            }
        } else {
            // 如果ListView中沒有元素,也應該允許下拉重新整理  
            ableToPull = true;
        }
    }
    
    
//NestedScrollView

private void setIsAbleToPull(MotionEvent event) {
        View firstChild = listView.getChildAt(0);
        if (firstChild != null) {
            int scrollY = listView.getScrollY();
            if (scrollY == 0 && firstChild.getTop() == 0) {
                if (!ableToPull) {
                    yDown = event.getRawY();
                }
                // 如果首個元素的上邊緣,距離父佈局值為0,就說明ListView滾動到了最頂部,此時應該允許下拉重新整理  
                ableToPull = true;
            } else {
                if (headerLayoutParams.topMargin != hideHeaderHeight) {
                    headerLayoutParams.topMargin = hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull = false;
            }
        } else {
            // 如果ListView中沒有元素,也應該允許下拉重新整理  
            ableToPull = true;
        }
    }

新增動畫和改變狀態什麼的重要的地方我註釋出來了,自己去搞搞還是很有意思的。

參考文章:Android下拉重新整理完全解析,教你如何一分鐘實現下拉重新整理功能

專案地址:Tyhj的可自定義下拉重新整理控制元件

相關文章