1. 程式人生 > >通過ViewGroup實現下拉重新整理和上拉載入,2018/2/12 06

通過ViewGroup實現下拉重新整理和上拉載入,2018/2/12 06

為了重新瞭解一下自定義ViewGroup,自己實現了一個下拉重新整理view,衝突的解決Recyclerview滾動到底部和頂部的處理全部放在了父view 中,滾動實現使用的是Scroller,所以使整個控制元件還有類似ios的彈性效果,程式碼很簡單,使用也很簡單,重新整理的頭佈局和腳佈局都可以在佈局檔案中直接新增,處理。
其中內容也可以不是Recyclerview,直接寫在佈局裡就行,佈局效果類似LinearLayout 但只能是豎直方向堆疊。
初始化佈局等程式碼:

private Scroller mScroller;
    private RecyclerView mRecyclerView;
    private
RefreshListener mListener; private int mTotalHeight; //子view加在一起總高度 private HashMap<Integer,Integer> mViewMarginTop; //所有子view marginTop距離 private HashMap<Integer,Integer> mViewMarginBottom;//所有子view marginBottom距離 private float mStartY;//手指落下位置 移動之後更新 private float mStartX; private
int mMoveHeight; //移動的總高度 private int mState; // 重新整理狀態 private static int NORMAL=0; // 正常狀態 private static int REFRESH=1; // 重新整理中 private static int LOAD=2; //上拉載入中 private boolean isPull; //是否拖動中 private int mCanScrollDistance=-1; //可拖動最遠距離 private boolean mCanLoadMore;//可否上拉載入 public
RefreshLayout(Context context) { this(context,null); } public RefreshLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mViewMarginTop= new HashMap<>(); mViewMarginBottom= new HashMap<>(); mScroller = new Scroller(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { //測量每一個子 measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int layoutWidth = getMeasuredWidth(); mTotalHeight=t;//初始化高度 for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); if (view.getVisibility() != GONE) { LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); int viewHeight = view.getMeasuredHeight(); int viewWidth = view.getMeasuredWidth(); if (i != 0) { mTotalHeight += viewHeight; if (mViewMarginTop.get(i) != null) {//總高度加上子view距離上方的距離 mTotalHeight += mViewMarginTop.get(i); } } if(layoutParams.mCenter) { int marginHorizontal = (layoutWidth - viewWidth-getPaddingLeft()-getPaddingRight()) / 2; if(mTotalHeight>getMeasuredHeight()) {//如果子view總高度大於父控制元件總高度則最大值為父控制元件高度 int viewMarginBottom=0; if (mViewMarginBottom.get(i) != null) { viewMarginBottom = mViewMarginBottom.get(i); } if((mTotalHeight-viewHeight)>=getMeasuredHeight()) {//判斷如果子view在父控制元件高度之外 繪製的位置 view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), mTotalHeight); }else { view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), getMeasuredHeight() - viewMarginBottom); } }else{ view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), mTotalHeight); } }else{ if(mTotalHeight>getMeasuredHeight()) {//如果子view總高度大於父控制元件總高度則最大值為父控制元件高度 int viewMarginBottom=0; if (mViewMarginBottom.get(i) != null) { viewMarginBottom = mViewMarginBottom.get(i); } if((mTotalHeight-viewHeight)>=getMeasuredHeight()) {//判斷如果子view在父控制元件高度之外 繪製的位置 view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, mTotalHeight); }else{ view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, getMeasuredHeight() - viewMarginBottom); mTotalHeight=getMeasuredHeight() - viewMarginBottom; } }else{ view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, mTotalHeight); } } if (mViewMarginBottom.get(i) != null) {//總高度加上子view距離下方的距離 mTotalHeight += mViewMarginBottom.get(i); } } } }

獲取子view屬性,和自定義屬性,其中自定義屬性需要在values資料夾下建立attrs資原始檔,僅僅只取了幾個處理了一下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Refresh_Layout">
        <attr name="center_horizontal" format="boolean" />
    </declare-styleable>
</resources>
@Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return super.generateLayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(),attrs);
    }

    /**
     * 自定義LayoutParams 新增自定義屬性
     * 並可以獲取到子view的屬性
     */
    private  class LayoutParams extends ViewGroup.LayoutParams {
        private boolean mCenter;
        private LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            for (int i = 0; i < attrs.getAttributeCount(); i++) {
                String attributeName = attrs.getAttributeName(i);
                String attributeValue = attrs.getAttributeValue(i);
                switch (attributeName){
                    case "layout_marginTop"://單位px 儲存所有子view marginTop高度
                        if(attributeValue.length()>2) {
                            mViewMarginTop.put(getChildCount(),(int) Double.parseDouble(attributeValue.substring(0, attributeValue.length() - 2)));
                        }
                        break;
                    case "layout_marginBottom"://單位px 儲存所有子view marginBottom高度
                        if(attributeValue.length()>2) {
                            mViewMarginBottom.put(getChildCount(),(int) Double.parseDouble(attributeValue.substring(0, attributeValue.length() - 2)));
                        }
                        break;
                    case "center_horizontal"://自定義屬性水平居中
                        mCenter = Boolean.valueOf(attributeValue);
                        break;
                }
            }
        }
    }

之後是觸控事件的處理:

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (mScroller != null && !mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mStartY = event.getRawY();
                mStartX = event.getRawY();
                super.dispatchTouchEvent(event);
                return true;
            case MotionEvent.ACTION_MOVE:
                if(Math.abs(mMoveHeight)>mCanScrollDistance && mCanScrollDistance>0){//判斷是否到達設定的極限滾動距離
                    return true;
                }
                int round = Math.round(mStartY - event.getRawY());
                int roundX = Math.round(mStartX - event.getRawX());
                if(mListener!=null){
                    mListener.moveDy(round);
                }
                if(Math.abs(roundX)>Math.abs(round)){//保證橫向能夠滾動
                    mStartY -= round;
                    mStartX -= roundX;
                    return super.dispatchTouchEvent(event);
                }
                if(mRecyclerView!=null) {
                    if (!mRecyclerView.canScrollVertically(-1)) {//豎直方向recyclerView可否下拉
                        if (round < 0) {
                            isPull = true;
                            dealMove(round,event);
                            return true;
                        }
                    }
                    if (!mRecyclerView.canScrollVertically(1)) {//豎直方向recyclerView可否上拉
                        if (round > 0) {//上拉
                            isPull = true;
                            dealMove(round,event);
                            return true;
                        }
                    }
                    if (mState == REFRESH || isPull) {//重新整理狀態,拖動狀態下攔截,自己處理移動
                        dealMove(round,event);
                        return true;
                    }
                    if(mMoveHeight>0){//如果底部view還在顯示則下拉是從底部view開始
                        dealMove(round,event);
                        return true;
                    }
                    mStartY -= round;
                    mStartX -= roundX;
                }else{
                    dealMove(round,event);
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                isPull=false;
                int height = getChildAt(0).getHeight();
                if(mMoveHeight<0 || mState == REFRESH) {
                    if (-mMoveHeight > height && mState == NORMAL) { // 正常狀態下下拉超過第一個view高度,改為重新整理狀態
                        mState = REFRESH;
                        if (mListener != null) {
                            mListener.refresh();
                        }
                        smoothScrollBy( -mMoveHeight - height);
                    } else if (mMoveHeight > height / 2 && mState == REFRESH) {//重新整理狀態下移動到第一個view顯示小於一半會滾到到正常顯示狀態
                        mState = NORMAL;
                        smoothScrollBy( -mMoveHeight + height);
                    } else {
                        smoothScrollBy( -mMoveHeight);
                    }
                    mMoveHeight = 0;
                }else{
                    if(mMoveHeight>mTotalHeight-getMeasuredHeight()) {//判斷上拉載入
                        if(mCanLoadMore) {
                            mState = LOAD;
                            if(mListener!=null){
                                mListener.loadMore();
                            }
                        }
                        if(mTotalHeight>getMeasuredHeight()) {
                            //滾動到最後一個view顯示的位置
                            smoothScrollBy(-mMoveHeight + mTotalHeight - getMeasuredHeight());
                            //計算滾回到最後一個view顯示位置需要移動的距離
                            mMoveHeight = mTotalHeight - getMeasuredHeight();
                        }else{
                            smoothScrollBy( -mMoveHeight );
                            mMoveHeight = 0;
                        }
                    }
                }
                super.dispatchTouchEvent(event);
                return true;
        }
        return super.dispatchTouchEvent(event);
    }
    /**
     * 處理拖動 的高度計算和回撥
     */
    private void dealMove(int round, MotionEvent event) {
        mMoveHeight += round;
        if(mListener!=null) {
            if (Math.abs(mMoveHeight) > getChildAt(0).getHeight()) {
                mListener.pullDown();
            }else{
                mListener.pullUp();
            }
        }
        smoothScrollBy( round);
        mStartY = event.getRawY();
        mStartX = event.getRawX();
    }

    /**
     * 滑動到指定位置
     * @param dy 豎直方向偏移量
     */
    private void smoothScrollBy(int dy) {
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), 0, dy);
        postInvalidate();
    }

最後是事件的監聽回撥 和方法呼叫:

public abstract class RefreshListener {
    abstract  void refresh();
     void pullUp(){

     }
     void pullDown(){

     }
     void loadMore(){

     }

     void moveDy(int dy){

     }
}
/**
     * 如果含有recyclerView需要繫結之後才能滾動
     */
    public void bindRecyclerView(RecyclerView recyclerView){
        mRecyclerView=recyclerView;
    }

    /**
     * 重新整理完成呼叫
     */
    public void refreshComplete(){
        if(mState==REFRESH) {
            int height = getChildAt(0).getHeight();
            mState = NORMAL;
            smoothScrollBy( -mMoveHeight + height);
            mMoveHeight = 0;
        }
    }

    /**
     * 上拉載入完成呼叫
     * @param type 0為還有資料需要載入 1 為資料全部載入完成
     */
    public void loadComplete(int type){
        if(mState==LOAD) {
            mState = NORMAL;
            if(type==0) {
                smoothScrollBy(-mMoveHeight);
                mMoveHeight = 0;
            }else{
                //滾動到最後一個view顯示的位置
                smoothScrollBy( -mMoveHeight + mTotalHeight - getMeasuredHeight());
                //計算滾回到最後一個view顯示位置需要移動的距離
                mMoveHeight =mTotalHeight - getMeasuredHeight();
            }
        }
    }

    /**
     * 最大可彈性滾動的距離
     * @param scrollDistance 距離單位px
     */
    public void canScrollDistance(int scrollDistance){
        mCanScrollDistance=scrollDistance;
    }

    /**
     * 繫結重新整理狀態監聽
     */
    /**
     * 繫結重新整理狀態監聽
     */
    public void setRefreshListener(RefreshListener listener,boolean canLoadMore){
        mListener=listener;
        mCanLoadMore=canLoadMore;
    }

最後別忘了Scroller要想滾動需要呼叫的方法:

@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

使用:

    <com.zqb.refreshlayout.RefreshLayout
        android:id="@+id/refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10px"
        android:paddingRight="200px">

        <RelativeLayout
            android:id="@+id/header_content"
            android:layout_width="match_parent"
            android:layout_height="150px">

            <LinearLayout
                android:id="@+id/header_text_layout"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:gravity="center"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/header_hint_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/header_hint_refresh_loading"
                    android:textSize="13sp"/>

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="3dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/header_hint_refresh_time"
                        android:textSize="14sp"/>

                    <TextView
                        android:id="@+id/header_hint_time"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="無記錄"
                        android:textSize="12sp"/>
                </LinearLayout>
            </LinearLayout>
        </RelativeLayout>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:paddingBottom="40px"
            android:paddingTop="20px"
            android:text="sfhsdkfh "/>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        </android.support.v7.widget.RecyclerView>

        <TextView
            android:text="載入中...."
            android:id="@+id/load_text_view"
            app:center_horizontal="true"
            android:layout_width="250px"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:paddingBottom="40px"
            android:paddingTop="20px"/>
    </com.zqb.refreshlayout.RefreshLayout>
mRecyclerView =  findViewById(R.id.recycler_view);
        mRefreshLayout = findViewById(R.id.refresh_layout);
        mHeaderHintTextView = findViewById(R.id.header_hint_text);
        mLoadTextView = findViewById(R.id.load_text_view);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        mAdapter = new MyAdapter();
        mRecyclerView.setAdapter(mAdapter);
        mRefreshLayout.bindRecyclerView(mRecyclerView);
        mRefreshLayout.setRefreshListener(new RefreshListener() {
            @Override
            void refresh() {
                mHeaderHintTextView.setText(R.string.header_hint_refresh_loading);
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mRefreshLayout.refreshComplete();
                    }

                }, 2000);
            }

            @Override
            void pullUp() {
                mHeaderHintTextView.setText(R.string.header_hint_refresh_normal);
            }

            @Override
            void pullDown() {
                mHeaderHintTextView.setText(R.string.header_hint_refresh_ready);
            }

            @Override
            void loadMore() {
                super.loadMore();
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(mCount>30) {
                            mLoadTextView.setText("載入完畢");
                            mRefreshLayout.loadComplete(1);
                        }else {
                            mCount += 10;
                            mAdapter.notifyDataSetChanged();
                            mRefreshLayout.loadComplete(0);
                        }
                    }

                }, 2000);
            }
        },false);

忘了加下拉減速效果了,之後有時間補上
GitHub上原始碼地址