ViewPager实现源码分析

1、松手自动计算当前位置,并自动滑动到合适的position的页面

分析:松手属于View的事件分发机制,事件分发的入口在dispatchTouchEvent和onTouchEvent,大概的分发机制如下:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if(onInterceptTouchEvent(ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }

        return consume;
    }
    因此,可以在onTouchEvent中处理松手事件

思路1:查看RecyclerView的源码,看系统源码是否提供ACTION_UP的事件的回调接口,重写该接口,实现松手的相应业务逻辑

思路2:覆盖掉onTouchEvent,ACTION_UP事件自己处理,其他事件交由系统默认处理(本项目采用的方法)
    类比ViewPager的实现ACTION_UP时候的实现方式

思路3:查看下RecyclerView的事件处理是否交由其他专门的事件处理类去处理(既在onTouchEvent中是否将事件处理转发到其他专门类处理),查找相应类和回调接口并覆盖掉回调接口方法
    分析RecyclerView的onTouchEvent函数,并没发现使用其他事件处理类去接收事件,因此该方法排除。

2.松手自动滑动到指定page

方法一:根据松手的位置距离中心点的位置是否超过一半,确定最终位置,并用smoothScrollToPosition实现滑动到指定位置.

缺点:虽然能实现松手滑动到指定位置,但是滑动效果和ViewPager的松手滑动不大一样

实现代码:

     int widthWithMargin = getWidth() + mPageMargin;
        // calculate scrollX
        int scrollX = mMyScrollX;
        int currentPage = scrollX / widthWithMargin;
        int offset = scrollX - currentPage*widthWithMargin;
        final int dstPage = currentPage + ((offset>widthWithMargin/2) ? 1 : 0);

        getHandler().post(new Runnable() {
            @Override
            public void run() {
                LogUtils.i("run to position");
                smoothScrollToPosition(dstPage);
            }
        });

方法二:参考ViewPager的松手实现方法:VelocityTracker+Scroller+Interpolator+回调函数computeScroll,根据用户滑动速度动态改变松手滑动的快慢(本工程采用该方法)

参考源码:ViewPager源码

问题:滑动的自然性和使用ViewPager的滑动自然性不同?

分析:能够准确滑动,但是自然性不足,可能是因为滑动过程的算法参考的代码是4.0的ViewPager的滑动效果

解决:滑动过程的实现算法采用最新版本的ViewPager代码,滑动自然性体验和现有的ViewPager一致。

3.实现接口的回调(类OnPageChangeListener,如:当前滑动到第几页,当前滑动的状态)

源码中查看下该接口的三个函数,分别在哪个点被回调(使用AS的快捷键查看调用层级树)。然后在自定义的RecyclerView中相应的位置回调这三个方法(还未实现。。)


 /**
     * Callback interface for responding to changing state of the selected page.
     */
    public interface OnPageChangeListener {

        /**
         * This method will be invoked when the current page is scrolled, either as part
         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
         *
         * @param position Position index of the first page currently being displayed.
         *                 Page position+1 will be visible if positionOffset is nonzero.
         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
         * @param positionOffsetPixels Value in pixels indicating the offset from position.
         */
        void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

        /**
         * This method will be invoked when a new page becomes selected. Animation is not
         * necessarily complete.
         *
         * @param position Position index of the new selected page.
         */
        void onPageSelected(int position);

        /**
         * Called when the scroll state changes. Useful for discovering when the user
         * begins dragging, when the pager is automatically settling to the current page,
         * or when it is fully stopped/idle.
         *
         * @param state The new scroll state.
         * @see ViewPager#SCROLL_STATE_IDLE
         * @see ViewPager#SCROLL_STATE_DRAGGING
         * @see ViewPager#SCROLL_STATE_SETTLING
         */
        void onPageScrollStateChanged(int state);
    }

3.1 ViewPager中mOnPageChangeListener.onPageScrolled的调用过程


    public void computeScroll()     // implement in ViewPagerRecyclerView
    private void completeScroll(boolean postEvents)
    private boolean performDrag(float x)    // implement in ViewPagerRecyclerView
    public void fakeDragBy(float xOffset)
        private boolean pageScrolled(int xpos) 
            -->protected void onPageScrolled(int position, float offset, int offsetPixels)
                -->private void dispatchOnPageScrolled(int position, float offset, int offsetPixels)
                    -->mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels);



    /*
        dispatchOnPageScrolled是mOnPageChangeListener.onPageScrolled的唯一调用入口
    */
     private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) {
            if (mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); // callback onPageScrolled
            }
            if (mOnPageChangeListeners != null) {
                for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
                    OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                    if (listener != null) {
                        listener.onPageScrolled(position, offset, offsetPixels);
                    }
                }
            }
            if (mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
            }
        }

3.2 ViewPager中mOnPageChangeListener.onPageSelected的调用过程


    public void onRestoreInstanceState(Parcelable state)
    public boolean onTouchEvent(MotionEvent ev) case MotionEvent.ACTION_UP:// implement in ViewPagerRecyclerView
    public void endFakeDrag()
    public void setAdapter(PagerAdapter adapter)
    public void setCurrentItem(int item)    // implement in ViewPagerRecyclerView
    void dataSetChanged()
        -->void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity)
            -->private void dispatchOnPageSelected(int position) 

            /*
                dispatchOnPageSelected是mOnPageChangeListener.onPageSelected的唯一调用入口
            */
            private void dispatchOnPageSelected(int position) {
            if (mOnPageChangeListener != null) {
                mOnPageChangeListener.onPageSelected(position);
            }
            if (mOnPageChangeListeners != null) {
                for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
                    OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                    if (listener != null) {
                        listener.onPageSelected(position);
                    }
                }
            }
            if (mInternalPageChangeListener != null) {
                mInternalPageChangeListener.onPageSelected(position);  // callback onPageSelected
            }
        }

3.3 ViewPager中mOnPageChangeListener.onPageScrollStateChanged的调用过程


    private final Runnable mEndScrollRunnable
    void smoothScrollTo(int x, int y, int velocity) // implement in ViewPagerRecyclerView
    public boolean onInterceptTouchEvent(MotionEvent ev)
    public boolean onTouchEvent(MotionEvent ev) // implement in ViewPagerRecyclerView
    public boolean beginFakeDrag() 
        private void setScrollState(int newState) 
            -->private void dispatchOnScrollStateChanged(int state) 



    /*
        dispatchOnPageSelected是mOnPageChangeListener.onPageSelected的唯一调用入口
    */
   private void dispatchOnScrollStateChanged(int state) {
        if (mOnPageChangeListener != null) {
            mOnPageChangeListener.onPageScrollStateChanged(state);
        }
        if (mOnPageChangeListeners != null) {
            for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) {
                OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                if (listener != null) {
                    listener.onPageScrollStateChanged(state);
                }
            }
        }
        if (mInternalPageChangeListener != null) {
            mInternalPageChangeListener.onPageScrollStateChanged(state);
        }
    }

问题:测试的时候,状态为IDLE的时候会有问题
问题:如何测试onPageScrolled

4.设置当前选中Item

ViewPager:如果当前是第一次设置的话,则启动requestLayout,并且设置当前选中的item,在布局中重新布局
requestLayout(); ===》使用标志位isFirstLayout

5.setPagerMargin的实现

5.1.ViewPager的setPagerMargin实现


/*
    Set the margin between pages.
    Parameters:
    marginPixels Distance between adjacent pages in pixels
*/
429     public void More ...setPageMargin(int marginPixels) {
430         final int oldMargin = mPageMargin;
            // record PageMargin for Layout
431         mPageMargin = marginPixels;
432 
433         final int width = getWidth();
434         recomputeScrollPosition(width, width, marginPixels, oldMargin);
435 
436         requestLayout();
437     }

899     @Override
900     protected void More ...onLayout(boolean changed, int l, int t, int r, int b) {
901         mInLayout = true;
902         populate();
903         mInLayout = false;
904 
905         final int count = getChildCount();
906         final int width = r-l;
907 
908         for (int i = 0; i < count; i++) {
909             View child = getChildAt(i);
910             ItemInfo ii;
                // Use the mPageMargin parameter in the layout
911             if (child.getVisibility() != GONE && (ii=infoForChild(child)) != null) {
912                 int loff = (width + mPageMargin) * ii.position;
913                 int childLeft = getPaddingLeft() + loff;
914                 int childTop = getPaddingTop();
                    ...
918                 child.layout(childLeft, childTop,
919                         childLeft + child.getMeasuredWidth(),
920                         childTop + child.getMeasuredHeight());
921             }
922         }
923         mFirstLayout = false;
924     }

5.2 RecyclerView的setPagerMargin的实现

思路:查看RecyclerView中如何布局子View。
问题:查看onLayout后发现布局过程有点复杂,代码中调用dispatchLayoutStep1();,dispatchLayoutStep2();,dispatchLayoutStep3();
分析:既然从入口函数到目标(View的布局过程)有点复杂(代码量有点多),那么就从后往前推。首先,先查看RecyclerView中哪个地方调用了子View的布局,
    既:RecyclerView中应该有调用child.layout或者view.layout,然后顺藤摸瓜,往上找出调用过程栈。经过查找,子view的布局过程如下:

    onFocusSearchFailed
    onLayoutChildren(LayoutManager中必须要覆盖的方法)
    LinearLayoutManager.scrollBy
        LinearLayoutManager.fill
            LinearLayoutManager.layoutChunk
                LayoutManager.layoutDecoratedWithMargins 该方法为public方法,可以覆盖掉
                    child.layout

想重新布局,但是RecyclerView的布局方式和ViewPager的布局方式不同,滚动的时候布局会乱。
解决:
 当用户设置pageMargin的时候,自定义PageMarginItemDecoration,让pageMargin的大小和ItemDecoration的mPageMarginWidth大小相等
 然后整个RecyclerView控件的高度 + 上下分割线的总和 
 让控件高度超出屏幕的高度
 在当前屏幕就看不到分割线了
 滑动的时候,又可以看到分割线
每个子View的测量和布局过程

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }

        // 1.measure child view
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }

        // 2.layout child view
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);

        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.isFocusable();
    }


 public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
      final LayoutParams lp = (LayoutParams) child.getLayoutParams();

      final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);//use ItemDecoration
      widthUsed += insets.left + insets.right;
      heightUsed += insets.top + insets.bottom;

      final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
              getPaddingLeft() + getPaddingRight() +
                      lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
              canScrollHorizontally());
      final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
              getPaddingTop() + getPaddingBottom() +
                      lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
              canScrollVertically());
      if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
          child.measure(widthSpec, heightSpec);
      }
  }

setMargin/setPadding均失败,查询代码,设置padding

转换思路:让RecyclerView控件的宽度大于屏幕宽度,而不是去考虑更改ItemDecoration的位置(源码不可动)

参考文献:

如何获取Android RecyclerView滑动的距离

如何获取 RecyclerView 的滑动距离?

RecyclerView

ViewPager

ViewPager源码

演示视频

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值