1. 程式人生 > >android之滑動懸浮tab&無限迴圈的viewPager

android之滑動懸浮tab&無限迴圈的viewPager

android之滑動懸浮tab&無限迴圈的viewPager

2017年01月10日 15:12:03 小鐘視野 閱讀數:2627 標籤: 真正的無效迴圈viewpager懸浮tab選中tab居中 更多

效果圖如下:

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

  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. // Children's touches should be canceled regardless of

  22. // whether or not this layout intercepted the consecutive motion events.

  23. /*if (!mChildrenEventsCanceled) {

  24. mChildrenEventsCanceled = true;

  25. if (mDownMotionEventPended) {

  26. mDownMotionEventPended = false;

  27. MotionEvent event1 = MotionEvent.obtainNoHistory(mPendingDownMotionEvent);

  28. event1.setLocation(ev.getX(), ev.getY());

  29. duplicateTouchEventForChildren(ev, event1);

  30. } else {

  31. duplicateTouchEventForChildren(ev);

  32. }

  33. }*/

  34. break;

  35. }

  36. return true;

  37. }

  38. return super.onTouchEvent(ev);

  39. }

主要是在Action_Move中搞事情:這裡為了更好的擴充套件自定義一個介面

 mTouchInterceptionListener.onMoveMotionEvent(ev, diffX, diffY);

將移動的偏移量返回,再來看看具體實現

  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. }

  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這個是一個工具包,其實裡邊就是屬性動畫的庫,直接使用就好了

  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搞事情就行,

注意:

  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就居中了

程式碼如下:

  1. private void init() {

  2. screenWidthOneThird = Tools.getScreenSize(context).x / 3;

  3. tabTextViewList = new ArrayList<TextView>();

  4. }

將螢幕分為三份

然後根據tab資料來源生成N個tabView

  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的父類容器

  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:在外部攔截,這都是交給自定義的介面實現

  1. @Override

  2. public boolean onInterceptTouchEvent(MotionEvent ev) {

  3. if (mTouchInterceptionListener == null) {

  4. return false;

  5. }

  6. switch (ev.getActionMasked()) {

  7. case MotionEvent.ACTION_DOWN:

  8. mInitialPoint = new PointF(ev.getX(), ev.getY());

  9. mPendingDownMotionEvent = MotionEvent.obtainNoHistory(ev);

  10. mDownMotionEventPended = true;

  11. mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, false, 0, 0);

  12. mBeganFromDownMotionEvent = mIntercepting;

  13. mChildrenEventsCanceled = false;

  14. return mIntercepting;

  15. case MotionEvent.ACTION_MOVE:

  16. // ACTION_MOVE will be passed suddenly, so initialize to avoid exception.

  17. if (mInitialPoint == null) {

  18. mInitialPoint = new PointF(ev.getX(), ev.getY());

  19. }

  20. // diffX and diffY are the origin of the motion, and should be difference

  21. // from the position of the ACTION_DOWN event occurred.

  22. float diffX = ev.getX() - mInitialPoint.x;

  23. float diffY = ev.getY() - mInitialPoint.y;

  24. mIntercepting = mTouchInterceptionListener.shouldInterceptTouchEvent(ev, true, diffX, diffY);

  25. return mIntercepting;

  26. }

  27. return false;

  28. }

自定義的介面實現 TouchInterceptionListener.shouldInterceptTouchEvent():

  1. @Override

  2. public boolean shouldInterceptTouchEvent(MotionEvent ev, boolean moving, float diffX, float diffY) {

  3. return doInterceptEvent(diffX, diffY);

  4. }

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

  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. return isUpInterception;

  41. } else {//左右滑動不攔截

  42. return false;

  43. }

  44. }

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

viewPagerManager.getCurrentFragment().isFragmentViewIntercepted()

這方法就是通知外部是否需要攔截事件;

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

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

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

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

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

  1. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

  2. if (0 == firstVisibleItem) {

  3. if (getChildCount() > 0) {

  4. View firstView = getChildAt(0);

  5. if (0 == firstView.getTop()) {

  6. isViewIntercepted = true;

  7. }else{

  8. isViewIntercepted = false;

  9. }

  10. }

  11. } else {

  12. isViewIntercepted = false;

  13. }

  14. }

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

  1. /**

  2. * 當前類是否要被攔截

  3. */

  4. private boolean isViewIntercepted = false;

  5. /**

  6. * 當前view是否被攔截

  7. */

  8. public boolean isViewIntercepted() {

  9. return isViewIntercepted;

  10. }

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

說說這裡遇到的最大的坑

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