android之滑動懸浮tab&無限循環的viewPager

效果圖如下:



雖然listview現在已經過時,而且這種效果也滿地都是,但是因爲自己項目的原因還是自己寫一個,而且也想整合都涉及的優化知識點,所以還是值得寫一寫,當作練練手,也算是一種提升吧

一:知識點

     1、屬性動畫的實現view的移動,讓其懸浮在頂部
     2、HorizontalScrollview計算寬度實現選中tab居中
     3、Fragment避免預加載
     4、viewPager實現真正的無限循環只需要5個fragment(思路及原理網上是有的),而不是通過設置viewPager的無限大來實現
     5、Fragment中的listview和滑動時的事件衝突解決(外部攔截即父類攔截)

其中知識點3、4不進行講解
知識點3可以移步我的另一篇博客:
知識點4:可以查看這個作者的博客

二、原理

原理的話一步步拆分就不是那麼的難了,一下逐一分析

   1、懸浮tab

         (1)懸浮的tab是一個horizontalScrollview,重寫FrameLayout爲SlideRootFrameLayout作爲activity的佈局中父布              局,tab自然是它的一個子view,所以我們可以在這裏搞事情,重寫這個主要是滑動事件用到

       (2)計算tab到SlideRootFrameLayout的距離top,然後通過重寫滑動事件,可知其滑動的距離,當手指順着屏                   幕向上滑動時,tab跟其一起滑動,其實是控制SlideRootFrameLayout滑動,

              《1》若是滑動大於等於top則不再進行滑動

              《2》若是小於top,則向上滑動還是遵循《1》,向下滑動則就是要恢復到原來的位置,由於滑動的時候可                          知道其滑動的偏移量,所以向下滑動時,滑動距離超過這個偏移量則將偏移量置0就回到原來位置

        注意:這裏所說的向上向下滑動,都是手指順着屏幕操作,即手指向上滑動或手指向下滑動


代碼如下:

重寫父佈局SlideRootFramelayout的onTouchEvent如下

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener != null) {
            switch (ev.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mInitialPoint = new PointF(ev.getX(), ev.getY());
                    MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                    event.setLocation(ev.getX(), ev.getY());
                    mTouchInterceptionListener.onDownMotionEvent(event);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float diffX = ev.getX() - mInitialPoint.x;
                    float diffY = ev.getY() - mInitialPoint.y;
                    mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                case MotionEvent.ACTION_CANCEL:
                    mBeganFromDownMotionEvent = false;
                    mTouchInterceptionListener.onUpOrCancelMotionEvent(ev,mIntercepting);

                    // Children's touches should be canceled regardless of
                    // whether or not this layout intercepted the consecutive motion events.
                    /*if (!mChildrenEventsCanceled) {
                        mChildrenEventsCanceled = true;
                        if (mDownMotionEventPended) {
                            mDownMotionEventPended = false;
                            MotionEvent event1 = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);
                            event1.setLocation(ev.getX(), ev.getY());
                            duplicateTouchEventForChildren(ev, event1);
                        } else {
                            duplicateTouchEventForChildren(ev);
                        }
                    }*/
                    break;
            }
            return true;
        }
        return super.onTouchEvent(ev);
    }
主要是在Action_Move中搞事情:這裏爲了更好的擴展自定義一個接口
 mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);
將移動的偏移量返回,再來看看具體實現
  @Override
            public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) {
               /* ViewDragHelper.create(slideRootFrameLayout, new ViewDragHelper.Callback() {
                    @Override
                    public boolean tryCaptureView(View child, int pointerId) {
                        return false;
                    }
                })*/
                doMoveHeadFloatTab(diffX, diffY);
            }
 /**
     * 處理當滑動時,懸浮的tab
     *
     * @param diffX
     * @param diffY
     */
    private void doMoveHeadFloatTab(float diffX, float diffY) {
        //最大隻能移動的距離是 llHeadParent.getHeight()
        float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);
        float translationY = getNegativeMaxValue(currTranstionY + diffY, -llHeadParent.getHeight(), 0);
        if (translationY <= 0 && translationY != currTranstionY) {//手指向上滑動,並且沒有滑動到頂部
            ViewHelper.setTranslationY(slideRootFrameLayout, translationY);
            //移動多上距離這個佈局就要增加多少佈局,否則會顯示不全,底部會留有一處空白
            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) slideRootFrameLayout.getLayoutParams();
            //一定要減去titleBar,如果沒有去掉Winow.xxx.Title,還要減去這個高度,否則會顯示不全
            lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));
            slideRootFrameLayout.requestLayout();//請求重繪,但是會有一閃一閃的情況
        }
    }
主要邏輯是在這個方法:

ViewHelper這個是一個工具包,其實裏邊就是屬性動畫的庫,直接使用就好了

/***
     * 手指上移過程dy是負數
     * 返回負數最大值:0是最大值,不可以超過
     *
     * @param value           移動的最終距離:上次的位置+當次移動的偏移量之和,就是本次要移動的最終的偏移量
     * @param canMoveMaxValue 可移動的最大值
     * @param maxValue
     * @return
     */
    public static float getNegativeMaxValue(final float value, final float canMoveMaxValue, final float maxValue) {
        return Math.min(maxValue, Math.max(canMoveMaxValue, value));
    }
這個方法是獲取滑動時的距離,向上滑動時dy是負數所以這裏比較最大值設置0
得到滑動的距離之後,接下來就是移動SlideRootFramelayout,其直接藉助viewHelper.setTranslationY搞事情就行,

注意:

//一定要減去titleBar,如果沒有去掉Winow.xxx.Title,還要減去這個高度,否則會顯示不全
            lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));
            slideRootFrameLayout.requestLayout();//請求重繪,但是會有一閃一閃的情況

SlideRootFramelayout佈局向上移動多少就要增加多少高度,否則會顯示不全,而且一定要重繪,否則不會更新

這樣就實現了懸浮的tab啦,是不是很簡單

  

    2、HorizontalScrollView中的tab居中

         (1)、我的思路是將屏幕寬分爲三分,即只顯示3個view

         (2)、當滑動viewpager或者選中當前的view時,通過獲取當前的view距離horizontalScrollview的距離,然後往左滑動一個view的寬度,選中的view就居中了

代碼如下:

private void init() {
        screenWidthOneThird = Tools.getScreenSize(context).x / 3;
        tabTextViewList = new ArrayList<TextView>();
    }
將屏幕分爲三份

然後根據tab數據源生成N個tabView

 /**
     * @description 添加tab欄:資源集合
     * @author zhongwr
     * @update 2015年9月1日 下午5:24:44
     */
    @SuppressLint("ResourceAsColor")
    public void addTabList(ArrayList<TabItem> allTabList) {
        if (!Tools.isListEmpty(allTabList)) {
            this.allTabList = allTabList;
            llTabContainer.setVisibility(View.VISIBLE);
            llTabContainer.removeAllViews();
            int size = allTabList.size();
            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            layoutParams.leftMargin = 30;
            layoutParams.rightMargin = 30;
            layoutParams.gravity = Gravity.CENTER_VERTICAL;
            layoutParams.width = screenWidthOneThird - 60;// 左右兩邊間距
            for (int i = 0; i < size; i++) {
                TabItem tabItem = allTabList.get(i);
                TextView tvTab = createTabTextView(tabItem, layoutParams);
                tvTab.setOnClickListener(new TabOnClickListener(tabItem.tabIndex));
                tabTextViewList.add(tvTab);
                if (1 == tabItem.selected) {// 當前選中的
                    currTabIndex = tabItem.tabIndex;
                    tvCurrTab = tvTab;
                    tvTab.setTextColor(context.getResources().getColor(R.color.red1));
                } else {
                    tvTab.setTextColor(context.getResources().getColor(R.color.gray2));
                }
                llTabContainer.addView(tvTab);
                // 增加豎線
                View line = new View(context);
                line.setBackgroundColor(context.getResources().getColor(R.color.color_line_e2));
                LinearLayout.LayoutParams layoutline = new LinearLayout.LayoutParams(1, 30);
                line.setLayoutParams(layoutline);
                llTabContainer.addView(line);
            }
            if (null != onClickTabListener) {
                onClickTabListener.onDefualtTab(currTabIndex, allTabList.get(currTabIndex));
            }
            scrollToPosition(currTabIndex);
        } else {
            llTabContainer.setVisibility(View.GONE);
        }
    }
這裏是通過動態加載的tabView,llTabContainer是HorizontalScrollview的子view是tabView的父類容器

/**
     * @description 設置定位到指定的位置,左右滑動都是往左滑動一個view的寬度,選中的view就居中了
     * @author zhongwr
     * @update 2015-11-30 下午3:53:31
     */
    public void scrollToPosition(final int currTabIndex) {
        scrollView.post(new Runnable() {
            @Override
            public void run() {// 選中的view居中
                TextView textView = tabTextViewList.get(currTabIndex);
                int left = textView.getLeft();
                left = left - screenWidthOneThird;
                scrollView.scrollTo(left, 0);
            }
        });
    }
不管是點擊左邊還是右邊的tabView,都是按照向左邊滑動一個tabView的寬度,讓選中的tabView居中。

主要代碼就是這樣,是不是覺得難度其實也沒什麼,就是靠思路及計算

這些前期工作都已經搞完,解決滑動衝突才真正是個難點

    3、解決滑動懸浮tab和viewpager中的listView的衝突

              解決事件衝突的方式無非就是兩種:
          (1)、外部攔截法:父類控制是否要攔截事件,
                  重寫攔截方法onInterceptTouchEvent() 返回true 攔截事件  false:不攔截
          (2)、內部攔截法:子類通知父類是否需要攔截,
                 requestDisallowInterceptToucheEvent(boolean)  false:攔截  true :不攔截

              基於上邊兩個方法規則,這裏我選用第一種方法:外部攔截法

         解決衝突還是要一步步分析,什麼時候攔截,什麼時候不攔截?

         《1》當向上滑動的時:

                  1、剛進到頁面還沒滑動,則直接攔截

                  2、已滑動,但是tab還沒置頂懸浮,則直接攔截,所以1和2可以合起來,tab還沒置頂懸浮直接攔截

                  3、當tab已懸浮,則不再進行攔截,把事件交給子view(這裏是交給listview)

        《2》當向下滑動時:

                 1、當tab懸浮時:

                       <1> listview已經滑動,則不攔截,讓listview回到初始位置:即position = 0;

                      <2> listview已經在初始位置(回到初始位置或者不曾滑動過)則,直接通知父類攔截事件

                 2、當tab未懸浮時:

                       <1> 剛進入,tab還是初始位置,則不攔截,將事件交給子view(listview)可以滑動

                       <2>已滑動,但並未置頂懸浮,只是滑動到一半,則直接攔截,讓tab回到初始位置

     基本就是這樣,分析完成之後,接下來就是直接擼碼了。

SlideRootFrameLayout:在外部攔截,這都是交給自定義的接口實現

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mTouchInterceptionListener == null) {
            return false;
        }

       
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitialPoint = new PointF(ev.getX(), ev.getY());
                mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);
                mDownMotionEventPended = true;
                mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);
                mBeganFromDownMotionEvent = mIntercepting;
                mChildrenEventsCanceled = false;
                return mIntercepting;
            case MotionEvent.ACTION_MOVE:
                // ACTION_MOVE will be passed suddenly, so initialize to avoid exception.
                if (mInitialPoint == null) {
                    mInitialPoint = new PointF(ev.getX(), ev.getY());
                }

                // diffX and diffY are the origin of the motion, and should be difference
                // from the position of the ACTION_DOWN event occurred.
                float diffX = ev.getX() - mInitialPoint.x;
                float diffY = ev.getY() - mInitialPoint.y;
                mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);
                return mIntercepting;
        }
        return false;
    }
自定義的接口實現
TouchInterceptionListener.shouldInterceptTouchEvent():

 @Override
            public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {
                return doInterceptEvent(diffX, diffY);

            }

所有的處理都交給了doInterceptEvent():

/**
     * 處理攔截事件
     *
     * @param diffX
     * @param diffY
     * @return
     */
    private boolean doInterceptEvent(float diffX, float diffY) {
        float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);
        float headHeight = -llHeadParent.getHeight();
        if (Math.abs(diffY) > Math.abs(diffX)) {//上下滑動
            if (diffY < 0) {//手指向上滑動
                if (Math.abs(currTranstionY) >= Math.abs(headHeight)) {//移動到頂端(tab懸浮)
                    isUpInterception = false;
                    isTabFloat = true;
                } else {//還沒移動到頂部所以還是要攔截
                    isUpInterception = true;
                    isTabFloat = false;
                }
//                        return isUpInterception;
            } else if (diffY > 0) {//手指向下滑動
                if (isTabFloat) {//如果tab懸浮着,手指要向下滑動,要攔截將tab復原
                    if (!viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()) {//listview已經滑動了
                        isUpInterception = false;
                    } else {
                        isUpInterception = true;
                        if (Math.abs(currTranstionY) <= 0) {//向下滑動復原
                            isTabFloat = false;
                        }
                    }
                } else {//tab未懸浮,兩種可能性:一個是可能剛進入時手指向下滑動時不攔截,一個是滑動到一半時要攔截
                    if (Math.abs(currTranstionY) <= 0) {//剛進入時,手指向下滑動,不攔截
                        isUpInterception = false;
                    } else if (Math.abs(currTranstionY) < Math.abs(headHeight)) {//滑動到一半,手指向下滑動要復原,則攔截
                        isUpInterception = true;
                    }
                }
//                        return isUpInterception;

            }
            return isUpInterception;
        } else {//左右滑動不攔截
            return false;
        }
    }
以上的處理邏輯就是跟我之前分析的一樣,這裏需要還有一處地方就是,listview是否已經滑動了或者是否已經回到初始位置了,需要獲取或者釋放事件主動權要告知父類,當然也是要自定義實現的,這裏只對外部提供一個方法:

viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()
這方法就是通知外部是否需要攔截事件;

由於tab未置頂懸浮或不在初始位置時,listview是不可以滑動的,所以只有在tab置頂浮或回到初始位置時,纔可以滑動,纔有獲取或者釋放事件的主動權;

接下來分析在什麼情況下,listview需要掌握主動權:

(1)、當向上滑動的時,外部會在之前的規則不攔截事件,此時listview可以任意向上滑動,這種情況可以不管

(2)、當向下滑動時,要回到初始位置,既是第一個位置 position=0;因爲只有到了初始位置才通知外部攔截事件,否則不可以攔截事件。

滑動的話,我們立即想到的就是ScrollListener

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
           
            if (0 == firstVisibleItem) {
                if (getChildCount() > 0) {
                    View firstView = getChildAt(0);
                    if (0 == firstView.getTop()) {
                        isViewIntercepted = true;
                    }else{
                        isViewIntercepted = false;
                    }
                
                }
            } else {
                isViewIntercepted = false;
            }
        }

onScroll方法:因爲它可以直接獲取到一個可見view的position,所以當時position=0時,可以通知攔截;但是這裏直接攔截會有bug,因爲會出現firstView沒顯示全就被攔截;所以這裏拿到firstView.getTop();這個top值如果不是0則表示沒顯示全,則不攔截,顯示全則通知攔截;主要是isViewIntercepted這個標誌了,以下是對外的方法,外部通過Fragment間接調用的;


/**
     * 當前類是否要被攔截
     */
    private boolean isViewIntercepted = false;

    /**
     * 當前view是否被攔截
     */
    public boolean isViewIntercepted() {
        return isViewIntercepted;
    }


這篇文章講的這裏算是結束了。

說說這裏遇到的最大的坑

這裏設計的知識點以及坑尤其是ViewPager的無限循環使用Fragment會有許多坑;比如循環使用更新數據、緩存數據、listview定位的緩存此外最坑的是 onSelectedPage執行時Fragment並沒有完全綁定activity,這時就要考慮什麼時間點去更新數據,因爲沒綁定時可能會出現getActivity爲null等等問題,所以如果不是特別大的話加載量的話,不建議使用無限循環的Fragment,以上的緩存數據也很難管理,此外選中tabView時的定位,要對應上的頁數也需要很大的功夫,所以還是建議使用老套方法,有多少個tab就創建多少個Fragment,只要控制懶加載就好了,其它都很好管理,畢竟那麼點東西android的內存還是妥妥的,而且一般用戶都有自己喜歡的某個tab,用戶很少去把所有的tab都點了個遍。不使用無限循環可以通過這個demo去改造就好了,改起來應該比較好改。


demo如下:http://download.csdn.net/detail/zhongwn/9732910

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章