【Android】 banner+tab吸頂+viewpager切換+刷新加載之實現

功能簡圖

如圖,頂部有輪播圖,tab須要吸頂,不一樣tab對應的條目不一樣,各tab下的條目存在不一樣類型,須要支持下拉刷新與上拉加載。天資愚笨,花了一週時間終於實現,特此記錄。java

項目中刷新加載控件採用SmartRefreshLayout,此次仍然打算採用它,不知道是否衝突。android

【tab切換】毫無疑問採用TabLayout+ViewPager實現,難點是【吸頂】,由於本身沒有實現過。git

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

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

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

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

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

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

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

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

但最終找到了老外 寫的 東西解決了自定義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;
    }
}
複製代碼
相關文章
相關標籤/搜索