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如下

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.    public boolean onTouchEvent(MotionEvent ev) {  
  3.        if (mTouchInterceptionListener != null) {  
  4.            switch (ev.getActionMasked()) {  
  5.                case MotionEvent.ACTION_DOWN:  
  6.                    mInitialPoint = new PointF(ev.getX(), ev.getY());  
  7.                    MotionEvent event = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);  
  8.                    event.setLocation(ev.getX(), ev.getY());  
  9.                    mTouchInterceptionListener.onDownMotionEvent(event);  
  10.                    break;  
  11.                case MotionEvent.ACTION_MOVE:  
  12.                    float diffX = ev.getX() - mInitialPoint.x;  
  13.                    float diffY = ev.getY() - mInitialPoint.y;  
  14.                    mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);  
  15.                    break;  
  16.                case MotionEvent.ACTION_UP:  
  17.                    break;  
  18.                case MotionEvent.ACTION_CANCEL:  
  19.                    mBeganFromDownMotionEvent = false;  
  20.                    mTouchInterceptionListener.onUpOrCancelMotionEvent(ev,mIntercepting);  
  21.   
  22.                    // Children's touches should be canceled regardless of  
  23.                    // whether or not this layout intercepted the consecutive motion events.  
  24.                    /*if (!mChildrenEventsCanceled) { 
  25.                        mChildrenEventsCanceled = true; 
  26.                        if (mDownMotionEventPended) { 
  27.                            mDownMotionEventPended = false; 
  28.                            MotionEvent event1 = MotionEvent.obtainNoHistory(mPendingDownMotionEvent); 
  29.                            event1.setLocation(ev.getX(), ev.getY()); 
  30.                            duplicateTouchEventForChildren(ev, event1); 
  31.                        } else { 
  32.                            duplicateTouchEventForChildren(ev); 
  33.                        } 
  34.                    }*/  
  35.                    break;  
  36.            }  
  37.            return true;  
  38.        }  
  39.        return super.onTouchEvent(ev);  
  40.    }  

主要是在Action_Move中搞事情:这里为了更好的扩展自定义一个接口

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);  

将移动的偏移量返回,再来看看具体实现

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.           public void onMoveMotionEvent(MotionEvent ev, float diffX, float diffY) {  
  3.              /* ViewDragHelper.create(slideRootFrameLayout, new ViewDragHelper.Callback() { 
  4.                   @Override 
  5.                   public boolean tryCaptureView(View child, int pointerId) { 
  6.                       return false; 
  7.                   } 
  8.               })*/  
  9.               doMoveHeadFloatTab(diffX, diffY);  
  10.           }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.     * 处理当滑动时,悬浮的tab 
  3.     * 
  4.     * @param diffX 
  5.     * @param diffY 
  6.     */  
  7.    private void doMoveHeadFloatTab(float diffX, float diffY) {  
  8.        //最大只能移动的距离是 llHeadParent.getHeight()  
  9.        float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);  
  10.        float translationY = getNegativeMaxValue(currTranstionY + diffY, -llHeadParent.getHeight(), 0);  
  11.        if (translationY <= 0 && translationY != currTranstionY) {//手指向上滑动,并且没有滑动到顶部  
  12.            ViewHelper.setTranslationY(slideRootFrameLayout, translationY);  
  13.            //移动多上距离这个布局就要增加多少布局,否则会显示不全,底部会留有一处空白  
  14.            FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) slideRootFrameLayout.getLayoutParams();  
  15.            //一定要减去titleBar,如果没有去掉Winow.xxx.Title,还要减去这个高度,否则会显示不全  
  16.            lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));  
  17.            slideRootFrameLayout.requestLayout();//请求重绘,但是会有一闪一闪的情况  
  18.        }  
  19.    }  

主要逻辑是在这个方法:

 

ViewHelper这个是一个工具包,其实里边就是属性动画的库,直接使用就好了

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /*** 
  2.      * 手指上移过程dy是负数 
  3.      * 返回负数最大值:0是最大值,不可以超过 
  4.      * 
  5.      * @param value           移动的最终距离:上次的位置+当次移动的偏移量之和,就是本次要移动的最终的偏移量 
  6.      * @param canMoveMaxValue 可移动的最大值 
  7.      * @param maxValue 
  8.      * @return 
  9.      */  
  10.     public static float getNegativeMaxValue(final float value, final float canMoveMaxValue, final float maxValue) {  
  11.         return Math.min(maxValue, Math.max(canMoveMaxValue, value));  
  12.     }  

这个方法是获取滑动时的距离,向上滑动时dy是负数所以这里比较最大值设置0
得到滑动的距离之后,接下来就是移动SlideRootFramelayout,其直接借助viewHelper.setTranslationY搞事情就行,

 

注意:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. //一定要减去titleBar,如果没有去掉Winow.xxx.Title,还要减去这个高度,否则会显示不全  
  2.             lp.height = (int) (-translationY + Tools.getScreenSize(context).y - Tools.getStatusBarHeight(context));  
  3.             slideRootFrameLayout.requestLayout();//请求重绘,但是会有一闪一闪的情况  

 

SlideRootFramelayout布局向上移动多少就要增加多少高度,否则会显示不全,而且一定要重绘,否则不会更新

这样就实现了悬浮的tab啦,是不是很简单

  

    2、HorizontalScrollView中的tab居中

         (1)、我的思路是将屏幕宽分为三分,即只显示3个view

         (2)、当滑动viewpager或者选中当前的view时,通过获取当前的view距离horizontalScrollview的距离,然后往左滑动一个view的宽度,选中的view就居中了

代码如下:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. private void init() {  
  2.         screenWidthOneThird = Tools.getScreenSize(context).x / 3;  
  3.         tabTextViewList = new ArrayList<TextView>();  
  4.     }  

将屏幕分为三份

 

然后根据tab数据源生成N个tabView

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.     * @description 添加tab栏:资源集合 
  3.     * @author zhongwr 
  4.     * @update 2015年9月1日 下午5:24:44 
  5.     */  
  6.    @SuppressLint("ResourceAsColor")  
  7.    public void addTabList(ArrayList<TabItem> allTabList) {  
  8.        if (!Tools.isListEmpty(allTabList)) {  
  9.            this.allTabList = allTabList;  
  10.            llTabContainer.setVisibility(View.VISIBLE);  
  11.            llTabContainer.removeAllViews();  
  12.            int size = allTabList.size();  
  13.            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,  
  14.                    ViewGroup.LayoutParams.WRAP_CONTENT);  
  15.            layoutParams.leftMargin = 30;  
  16.            layoutParams.rightMargin = 30;  
  17.            layoutParams.gravity = Gravity.CENTER_VERTICAL;  
  18.            layoutParams.width = screenWidthOneThird - 60;// 左右两边间距  
  19.            for (int i = 0; i < size; i++) {  
  20.                TabItem tabItem = allTabList.get(i);  
  21.                TextView tvTab = createTabTextView(tabItem, layoutParams);  
  22.                tvTab.setOnClickListener(new TabOnClickListener(tabItem.tabIndex));  
  23.                tabTextViewList.add(tvTab);  
  24.                if (1 == tabItem.selected) {// 当前选中的  
  25.                    currTabIndex = tabItem.tabIndex;  
  26.                    tvCurrTab = tvTab;  
  27.                    tvTab.setTextColor(context.getResources().getColor(R.color.red1));  
  28.                } else {  
  29.                    tvTab.setTextColor(context.getResources().getColor(R.color.gray2));  
  30.                }  
  31.                llTabContainer.addView(tvTab);  
  32.                // 增加竖线  
  33.                View line = new View(context);  
  34.                line.setBackgroundColor(context.getResources().getColor(R.color.color_line_e2));  
  35.                LinearLayout.LayoutParams layoutline = new LinearLayout.LayoutParams(1, 30);  
  36.                line.setLayoutParams(layoutline);  
  37.                llTabContainer.addView(line);  
  38.            }  
  39.            if (null != onClickTabListener) {  
  40.                onClickTabListener.onDefualtTab(currTabIndex, allTabList.get(currTabIndex));  
  41.            }  
  42.            scrollToPosition(currTabIndex);  
  43.        } else {  
  44.            llTabContainer.setVisibility(View.GONE);  
  45.        }  
  46.    }  

这里是通过动态加载的tabView,llTabContainer是HorizontalScrollview的子view是tabView的父类容器

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * @description 设置定位到指定的位置,左右滑动都是往左滑动一个view的宽度,选中的view就居中了 
  3.      * @author zhongwr 
  4.      * @update 2015-11-30 下午3:53:31 
  5.      */  
  6.     public void scrollToPosition(final int currTabIndex) {  
  7.         scrollView.post(new Runnable() {  
  8.             @Override  
  9.             public void run() {// 选中的view居中  
  10.                 TextView textView = tabTextViewList.get(currTabIndex);  
  11.                 int left = textView.getLeft();  
  12.                 left = left - screenWidthOneThird;  
  13.                 scrollView.scrollTo(left, 0);  
  14.             }  
  15.         });  
  16.     }  

不管是点击左边还是右边的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:在外部拦截,这都是交给自定义的接口实现

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.   public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.       if (mTouchInterceptionListener == null) {  
  4.           return false;  
  5.       }  
  6.   
  7.        
  8.       switch (ev.getActionMasked()) {  
  9.           case MotionEvent.ACTION_DOWN:  
  10.               mInitialPoint = new PointF(ev.getX(), ev.getY());  
  11.               mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);  
  12.               mDownMotionEventPended = true;  
  13.               mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);  
  14.               mBeganFromDownMotionEvent = mIntercepting;  
  15.               mChildrenEventsCanceled = false;  
  16.               return mIntercepting;  
  17.           case MotionEvent.ACTION_MOVE:  
  18.               // ACTION_MOVE will be passed suddenly, so initialize to avoid exception.  
  19.               if (mInitialPoint == null) {  
  20.                   mInitialPoint = new PointF(ev.getX(), ev.getY());  
  21.               }  
  22.   
  23.               // diffX and diffY are the origin of the motion, and should be difference  
  24.               // from the position of the ACTION_DOWN event occurred.  
  25.               float diffX = ev.getX() - mInitialPoint.x;  
  26.               float diffY = ev.getY() - mInitialPoint.y;  
  27.               mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);  
  28.               return mIntercepting;  
  29.       }  
  30.       return false;  
  31.   }  

自定义的接口实现
TouchInterceptionListener.shouldInterceptTouchEvent():

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.            public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {  
  3.                return doInterceptEvent(diffX, diffY);  
  4.   
  5.            }  


所有的处理都交给了doInterceptEvent():

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * 处理拦截事件 
  3.      * 
  4.      * @param diffX 
  5.      * @param diffY 
  6.      * @return 
  7.      */  
  8.     private boolean doInterceptEvent(float diffX, float diffY) {  
  9.         float currTranstionY = ViewHelper.getTranslationY(slideRootFrameLayout);  
  10.         float headHeight = -llHeadParent.getHeight();  
  11.         if (Math.abs(diffY) > Math.abs(diffX)) {//上下滑动  
  12.             if (diffY < 0) {//手指向上滑动  
  13.                 if (Math.abs(currTranstionY) >= Math.abs(headHeight)) {//移动到顶端(tab悬浮)  
  14.                     isUpInterception = false;  
  15.                     isTabFloat = true;  
  16.                 } else {//还没移动到顶部所以还是要拦截  
  17.                     isUpInterception = true;  
  18.                     isTabFloat = false;  
  19.                 }  
  20. //                        return isUpInterception;  
  21.             } else if (diffY > 0) {//手指向下滑动  
  22.                 if (isTabFloat) {//如果tab悬浮着,手指要向下滑动,要拦截将tab复原  
  23.                     if (!viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()) {//listview已经滑动了  
  24.                         isUpInterception = false;  
  25.                     } else {  
  26.                         isUpInterception = true;  
  27.                         if (Math.abs(currTranstionY) <= 0) {//向下滑动复原  
  28.                             isTabFloat = false;  
  29.                         }  
  30.                     }  
  31.                 } else {//tab未悬浮,两种可能性:一个是可能刚进入时手指向下滑动时不拦截,一个是滑动到一半时要拦截  
  32.                     if (Math.abs(currTranstionY) <= 0) {//刚进入时,手指向下滑动,不拦截  
  33.                         isUpInterception = false;  
  34.                     } else if (Math.abs(currTranstionY) < Math.abs(headHeight)) {//滑动到一半,手指向下滑动要复原,则拦截  
  35.                         isUpInterception = true;  
  36.                     }  
  37.                 }  
  38. //                        return isUpInterception;  
  39.   
  40.             }  
  41.             return isUpInterception;  
  42.         } else {//左右滑动不拦截  
  43.             return false;  
  44.         }  
  45.     }  

以上的处理逻辑就是跟我之前分析的一样,这里需要还有一处地方就是,listview是否已经滑动了或者是否已经回到初始位置了,需要获取或者释放事件主动权要告知父类,当然也是要自定义实现的,这里只对外部提供一个方法:

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()  

这方法就是通知外部是否需要拦截事件;

 

由于tab未置顶悬浮或不在初始位置时,listview是不可以滑动的,所以只有在tab置顶浮或回到初始位置时,才可以滑动,才有获取或者释放事件的主动权;

接下来分析在什么情况下,listview需要掌握主动权:

(1)、当向上滑动的时,外部会在之前的规则不拦截事件,此时listview可以任意向上滑动,这种情况可以不管

(2)、当向下滑动时,要回到初始位置,既是第一个位置 position=0;因为只有到了初始位置才通知外部拦截事件,否则不可以拦截事件。

滑动的话,我们立即想到的就是ScrollListener

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {  
  2.              
  3.             if (0 == firstVisibleItem) {  
  4.                 if (getChildCount() > 0) {  
  5.                     View firstView = getChildAt(0);  
  6.                     if (0 == firstView.getTop()) {  
  7.                         isViewIntercepted = true;  
  8.                     }else{  
  9.                         isViewIntercepted = false;  
  10.                     }  
  11.                   
  12.                 }  
  13.             } else {  
  14.                 isViewIntercepted = false;  
  15.             }  
  16.         }  

 

onScroll方法:因为它可以直接获取到一个可见view的position,所以当时position=0时,可以通知拦截;但是这里直接拦截会有bug,因为会出现firstView没显示全就被拦截;所以这里拿到firstView.getTop();这个top值如果不是0则表示没显示全,则不拦截,显示全则通知拦截;主要是isViewIntercepted这个标志了,以下是对外的方法,外部通过Fragment间接调用的;

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * 当前类是否要被拦截 
  3.      */  
  4.     private boolean isViewIntercepted = false;  
  5.   
  6.     /** 
  7.      * 当前view是否被拦截 
  8.      */  
  9.     public boolean isViewIntercepted() {  
  10.         return isViewIntercepted;  
  11.     }  

 

 

这篇文章讲的这里算是结束了。

说说这里遇到的最大的坑

这里设计的知识点以及坑尤其是ViewPager的无限循环使用Fragment会有许多坑;比如循环使用更新数据、缓存数据、listview定位的缓存此外最坑的是 onSelectedPage执行时Fragment并没有完全绑定activity,这时就要考虑什么时间点去更新数据,因为没绑定时可能会出现getActivity为null等等问题,所以如果不是特别大的话加载量的话,不建议使用无限循环的Fragment,以上的缓存数据也很难管理,此外选中tabView时的定位,要对应上的页数也需要很大的功夫,所以还是建议使用老套方法,有多少个tab就创建多少个Fragment,只要控制懒加载就好了,其它都很好管理,毕竟那么点东西Android的内存还是妥妥的,而且一般用户都有自己喜欢的某个tab,用户很少去把所有的tab都点了个遍。不使用无限循环可以通过这个demo去改造就好了,改起来应该比较好改。

 

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

posted @ 2017-04-25 10:45  天涯海角路  阅读(672)  评论(0)    收藏  举报