【Android】 banner+tab吸頂+viewpager切換+重新整理載入之實現

鍵筆刀發表於2019-03-13

功能簡圖

如圖,頂部有輪播圖,tab需要吸頂,不同tab對應的條目不同,各tab下的條目存在不同型別,需要支援下拉重新整理與上拉載入。天資愚笨,花了一週時間終於實現,特此記錄。

專案中重新整理載入控制元件採用SmartRefreshLayout,這次仍然打算採用它,不知道是否衝突。

【tab切換】毫無疑問採用TabLayout+ViewPager實現,難點是【吸頂】,因為自己沒有實現過。

google關鍵詞:【android 吸頂+切換】,淘到了這篇簡文,算是有一點眉目,感激作者的分享。

run了Demo並整合了SmartRefreshLayout,謝天謝地,沒有衝突。

Demo中的一個乾貨是OuterRecyclerViewInnerRecyclerView兩個類。主要解決了RecyclerView巢狀後,縱向滑動的衝突。前者負責是否進行事件攔截,後者負責是否消費事件及將結果通知給前者。這兩個類的部分命名錶意性不強,註釋不足,且引入tab後點選事件不靈敏了,我進行了改進,程式碼見文末。

Demo中的另一個乾貨是讓我知道了阿里vLayout的存在,承認平時太懶了,作為程式設計師不關注技術時事及大廠動態的我有點失敗。還好,【吸頂】採用vLayout實現了。但也走了一些彎路,參見了示例的我最後發現要在例項化StickyLayoutHelper之後為其設定顏色helper.setBgColor(0xffffffff)才符合產品效果圖。

右看
ViewPager的每一個Item都是一個Fragment(with InnerRecyclerView),發現InnerRecyclerView無法滑動導致列表只能展示一部分。以為是SmartRefreshLayout的干擾,以為是OuterRecyclerView和InnerRecyclerView的實現有問題,但最終發現是ViewPager的高度出現了問題。

於是就根據不同的資料來源(由於Tab不同)中item的數量及Item的佈局高度計算出ViewPager的高度,並在合適的時候(ViewPager的pageChange監聽中)改變ViewPager的Height,這需要維護的東西太多了,太low了,而且ViewPager的高度最終是計算出來的最大值。

百度關鍵字:【ViewPager Fragment 高度】會發現,各種讓自定義ViewPager並重寫onMeasure方法。有遍歷child找到其中最大高度的、有使用getChildView(0)使用其高度的、有使用getCurrentItem()高度的,看的眼花繚亂。

OuterRecyclerView的Item中layout_height="wrap_content"的ViewPager顯示根本不出來(空白),layout_height="match_parent"的ViewPager也只是顯示一部分(即InnerRecyclerView的列表無法滑動)

但最終找到了老外 寫的 東西解決了自定義ViewPager的問題。同時,對ViewPager的自定義做了擴充套件:ViewPager支援最小高度,否則不滿一屏特別醜,程式碼在下面。

彩蛋

  1. 由於ViewPager中呼叫了requestLayout方法,因此tab切換的時候就無法保留之前的狀態(每次都會顯示InnerRecyclerView的頂部)
  2. requestLayout和requestDisallowInterceptTouchEvent兩個方法很重要
  3. ViewPager2出來了
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * 該類由 <b>鍵筆刀</b> 於 2019年2月21日 星期四 9時10分13秒 建立;<br>
 * 作用是:<b>存在RecyclerView巢狀時,外層Recyclerview</b>;<br>
 * 用於【吸頂】+【切換】功能的實現
 * 該RecyclerView對外提供了方法用於控制【是否進行事件攔截】
 * <p>
 * 參見:https://github.com/FrizzleLiu/NestDemo
 */
public class OuterNestingRecyclerView extends RecyclerView {

    /**
     * 標記是否需要進行事件攔截,預設攔截
     */
    private boolean isNeedIntercept = true;
    private float downX;    //按下時 的X座標
    private float downY;    //按下時 的Y座標

    /**
     * 內層巢狀的ViewPager
     */
    private ViewPager vp;

    public OuterNestingRecyclerView(Context context) {
        super(context);
    }

    public OuterNestingRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public OuterNestingRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        float x = e.getX();
        float y = e.getY();
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //將按下時的座標儲存
                downX = x;
                downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //獲取到距離差
                float dx = x - downX;
                float dy = y - downY;
                //通過距離差判斷方向
                int orientation = getOrientation(dx, dy);
                switch (orientation) {
                    //右滑動交給ViewPager處理(只有當可以右滑的時候)
                    case 'r':
                        //如果呼叫方沒有設定Viewpager,則不攔截手指向右移動事件
                        if (vp == null) {
                            setNeedIntercept(false);
                        } else {
                            //如果設定了ViewPager,則只有當左邊有ViewPager的條目時,才不攔截手指向右移動事件
                            if (vp.getCurrentItem() > 0) {
                                setNeedIntercept(false);
                            }
                        }
                        break;
                    //左滑動交給ViewPager處理(只有當可以左滑的時候)
                    case 'l':
                        //如果呼叫方沒有設定Viewpager,則不攔截手指向左移動事件
                        if (vp == null) {
                            setNeedIntercept(false);
                        } else {
                            //如果設定了Viewpager,則之後當右邊有ViewPager的條目時,才不攔截手指向左移動事件
                            if (vp.getCurrentItem() < vp.getAdapter().getCount() - 1) {
                                setNeedIntercept(false);
                            }
                        }
                        break;
//                    點選事件,則不攔截,如果不做此判斷,則tablayout的點選事件就不靈敏了
                    case 'c':
                        return false;
                }
                return isNeedIntercept;
        }
        return super.onInterceptTouchEvent(e);
    }

    public void setNeedIntercept(boolean needIntercept) {
        isNeedIntercept = needIntercept;
    }

    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx) < 3 && Math.abs(dy) < 3) {
            return 'c';//click的意思
        }
        if (Math.abs(dx) > Math.abs(dy)) {
            //X軸移動
            return dx > 0 ? 'r' : 'l';//右,左
        } else {
            //Y軸移動
            return dy > 0 ? 'b' : 't';//下//上
        }
    }

    public void setViewPager(ViewPager vp) {
        this.vp = vp;
    }
}
複製程式碼
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * 該類由 <b>鍵筆刀</b> 於 2019年2月21日 星期四 9時16分09秒 建立;<br>
 * 作用是:<b>當存在RecyclerView之間的巢狀時,內層Recyclerview</b>;<br>
 * 用於【吸頂】+【切換】功能的實現
 * 該RecyclerView主要負責何時進行事件消費,何時禁止父容器攔截事件
 * <p>
 * 參見:https://github.com/FrizzleLiu/NestDemo
 */
public class InnerNestingRecyclerView extends RecyclerView {

    private float downX;    //按下時 的X座標
    private float downY;    //按下時 的Y座標

    /**
     * 吸頂時,內層RecyclerView左上角的y軸座標
     */
    private int stickY;

    //初始化個預設值,使用的時候就無需判null了
    private InnerConsumeEventListener innerConsumeEventListener = innerConsumeEventOrNot -> {
        //no-op
    };

    public InnerNestingRecyclerView(Context context) {
        super(context);
    }

    public InnerNestingRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public InnerNestingRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }


    @Override
    public boolean onTouchEvent(MotionEvent e) {
        float x = e.getX();
        float y = e.getY();
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //將按下時的座標儲存
                downX = x;
                downY = y;
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                //獲取到距離差
                float dx = x - downX;
                float dy = y - downY;
                //通過距離差判斷方向
                int orientation = getOrientation(dx, dy);
                int[] location = {0, 0};
                getLocationOnScreen(location);
                switch (orientation) {
                    case 'b':
                        // 手指從上往下滑動時,即資料向bottom方向滑動,canScrollVertically()用於判
                        // 斷RecyclerView是否可以縱向滑動,檢查向上滾動為負,檢查向下滾動為正。
                        // 內層RecyclerView下拉到最頂部時候不再處理事件
                        if (!canScrollVertically(-1)) {
                            getParent().requestDisallowInterceptTouchEvent(false);
                            innerConsumeEventListener.notice(false);
                        } else {
                            //內層RecyclerView可以向上滾動的話,父容器禁止攔截時間,內部RecyclerView消費事件
                            getParent().requestDisallowInterceptTouchEvent(true);
                            innerConsumeEventListener.notice(true);
                        }
                        break;
                    case 't':
                        // 當手指從下往上滑動時,即資料向top方向滑動,location[1]代表內層RecyclerView的左
                        // 上角與螢幕左上點的y軸方向上的距離,
                        // 如果內層RecyclerView的左上角(亦即頂部)沒有向上滑動到指定位置,即沒有吸
                        // 頂,則事件由父容器處理
                        if (location[1] > stickY) {
                            getParent().requestDisallowInterceptTouchEvent(false);
                            innerConsumeEventListener.notice(false);
                            return true;
                        } else {
                            //如果已經吸頂了,手指往上滑動時,內層RecyclerView進行事件消費,
                            //父容器禁止攔截事件
                            getParent().requestDisallowInterceptTouchEvent(true);
                            innerConsumeEventListener.notice(true);
                        }
                        break;
                    //左右滑動交給ViewPager處理,不禁止父類進行攔截,即允許父類進行事件攔截
                    case 'r':
                    case 'l':
                        getParent().requestDisallowInterceptTouchEvent(false);
                        break;
                }
                break;
        }
        return super.onTouchEvent(e);
    }


    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx) > Math.abs(dy)) {
            //X軸移動
            return dx > 0 ? 'r' : 'l';//右,左
        } else {
            //Y軸移動
            return dy > 0 ? 'b' : 't';//下//上
        }
    }

    public void setStickY(int stickY) {
        this.stickY = stickY;
    }

    /**
     * 內層RecyclerView是否需要消費事件的監聽
     */
    public interface InnerConsumeEventListener {
        /**
         * 用於通知呼叫方,內層RecyclerView是否消費了事件
         *
         * @param innerConsumeEventOrNot true:內層消費了事件  false:內層  無需/沒有  消費事件
         */
        void notice(boolean innerConsumeEventOrNot);
    }

    /**
     * 設定監聽器,監聽內層RecyclerView是否消費了時間
     */
    public void setInnerConsumeEventListener(InnerConsumeEventListener innerConsumeEventListener) {
        this.innerConsumeEventListener = innerConsumeEventListener;
    }
}

複製程式碼
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;

/**
 * https://mobikul.com/viewpager/
 * https://medium.com/winkl-insights/how-to-have-a-height-wrapping-viewpager-when-images-have-variable-heights-on-android-60b18e55e72e
 * https://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content
 */
public class WrapContentHeightViewPager extends ViewPager {

    public WrapContentHeightViewPager(Context context) {
        super(context);
        initPageChangeListener();
    }

    public WrapContentHeightViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPageChangeListener();
    }

    private void initPageChangeListener() {
        addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageSelected(int position) {
                requestLayout();
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        View child = getChildAt(getCurrentItem());
        if (child != null) {
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (minHeight > h) {
                h = minHeight;
            }
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private int minHeight;

    public void setMinHeight(int minHeight) {
        this.minHeight = minHeight;
    }
}
複製程式碼

相關文章