ViewPager翻页特效(1_向源码学习,图文详解

本文详细探讨了ViewPager的源码,揭示了其子视图在X轴上的位置变化,并介绍了PageTransformer参数的探索,展示了如何利用这些知识创建自定义滑动特效。通过设置PageTransformer,可以实现如旋转、倾斜等动态效果。同时,文章强调了面试和不断学习的重要性,分享了面试技巧和学习资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         }
    }

}


上面代码中,对 `count` 进行了两轮循环,其中第一轮是针对 `lp.isDecor` 为 `true`的,

意为:如果当前view是一个 decoration 装饰,并不是adapter提供的view 则返回 true

显然,我们要探讨的是 adapter提供的View 是如何摆放的,所以忽略这一块。

而在下面的循环中,可以看到 

```java
child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());

这个便是child的排布的核心代码,追溯这4个参数,可以得知:第 1,3 参数 表示 left ,right , 他们都和一个 int loff = (int) (childWidth * ii.offset); 挂钩,而 第2,4 参数表示 top,bottom , 则 并没有与 任何动态参数相挂钩。

因此可以断定,ViewPager的子View排布,只会存在X轴方向上的位置偏差,在Y方向上会保持上下平齐。

其实还可以继续追溯 int loff = (int) (childWidth * ii.offset); 看看 x轴方向上的位置偏差是如何造成的,但是目的已经达到,到有必要的时候再去追查。

确定是横向排布,那么左右滑动逻辑又是怎么样的呢?

找到 onTouchEvent() 方法, 并且在其中找到 ACTION_MOVE 逻辑分支:

case MotionEvent.ACTION_MOVE:
                if (!mIsBeingDragged) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    if (pointerIndex == -1) {
                        // A child has consumed some touch events and put us into an inconsistent
                        // state.
                        needsInvalidate = resetTouch();
                        break;
                    }
                    final float x = ev.getX(pointerIndex);
                    final float xDiff = Math.abs(x - mLastMotionX);
                    final float y = ev.getY(pointerIndex);
                    final float yDiff = Math.abs(y - mLastMotionY);
                    if (DEBUG) {
                        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    }
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        if (DEBUG) Log.v(TAG, "Starting drag!");
                        mIsBeingDragged = true;
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                                mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);

                        // Disallow Parent Intercept, just in case
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // Not else! Note that mIsBeingDragged can be set above.
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(activePointerIndex);
                    needsInvalidate |= performDrag(x);
                }
                break;

我们需要关注的只是 X方向上的拖拽有什么规律. 所以,顺着final float x = ev.getX(pointerIndex); 这个变量去找关键方法, 最终锁定:performDrag(x); 它是处理X方向上位移的关键入口。

private boolean performDrag(float x) {
        boolean needsInvalidate = false;

        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;

        ...
        // Don't lose the rounded component
        mLastMotionX += scrollX - (int) scrollX;
        scrollTo((int) scrollX, getScrollY()); // 关键代码1, 控件在画布上的横像滚动
        pageScrolled((int) scrollX);// 关键代码2,将 scrollX进一步往下传递

        return needsInvalidate;
    }

发现两句关键代码,一个是处理滑动的 scrolllTo,一个是把scrollX往下传递的 pageScrolled(scrollX). 前面一句都明白,但是这个第二句就有点不懂了,继续深入。

private boolean pageScrolled(int xpos) {
        ...
        final float pageOffset = (((float) xpos / width) - ii.offset)
                / (ii.widthFactor + marginOffset);
        final int offsetPixels = (int) (pageOffset * widthWithMargin);

        mCalledSuper = false;
        onPageScrolled(currentPage, pageOffset, offsetPixels);
        if (!mCalledSuper) {
            throw new IllegalStateException(
                    "onPageScrolled did not call superclass implementation");
        }
        return true;
    }

追踪 参数xpos得知,x方向上的偏移量信息,最后进入了 onPageScrolled(...) 方法.

    protected void onPageScrolled(int position, float offset, int offsetPixels) {
        // Offset any decor views if needed - keep them on-screen at all times.
        if (mDecorChildCount > 0) {
            ... // 这里还是在处理 装饰,所以不用看,而且参数也没进入到这里
        }

        dispatchOnPageScrolled(position, offset, offsetPixels);

        if (mPageTransformer != null) {
            final int scrollX = getScrollX();
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                if (lp.isDecor) continue;
                final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
                mPageTransformer.transformPage(child, transformPos);
            }
        }

        mCalledSuper = true;
    }

又是两句关键代码:

dispatchOnPageScrolled(position, offset, offsetPixels);

点进去看了之后,发现只是 调用了 OnPageChangeListener 监听回调.

如果我们设置了滑动监听,就可以在滑动的时候,收到回调。相信大家都用过这个。

mPageTransformer.transformPage(child, transformPos);

这里就比较奇怪了。这句代码把子view,以及子view当前的位置信息返回到了外界。

那么外界拿到这两个参数值之后可以做什么事呢?理论上,可以做任何事

二,探索源码结论

  1. ViewPager的初始子view摆放,都是横向的。在纵向上是上下平齐。

  2. ViewPager将 子view以及子view的当前位置参数,通过PageTransformer.transformPage(view,position)反馈到外界,能做很多。比如说,让横着排放的子view变成竖着放,又或者 让即将滑出屏幕的子view以倾斜的角度以某个加速度飞出去,为所欲为。这个就是我们可以完成这个动画的基础。

三,PageTransformer参数规律探索

ViewPager 提供了一个DIY滑动特效的可能性。不过在动手做动画之前,还需要了解 这两个参数的变化规律。

新建一个android工程,写好ViewPager+TabLayout 的代码和布局。运行起来大概是这个效果:

同时,我们给viewpager加上setPageTransformer(…)方法,并且打印日志。

viewPager.adapter = MyFragmentPagerAdapter(supportFragmentManager);
viewPager.offscreenPageLimit = 3 // 最少缓存3个,让左右两边都显示出来        
viewPager.setPageTransformer(true, ViewPager.PageTransformer { view, position ->
    Log.d("setPageTransformer", "view:${view.hashCode()} | position:${position}")
})

然后启动app,看看日志:

03-12 14:14:46.222 1583-1583/? D/setPageTransformer: view:136851691 | position:0.0
03-12 14:14:46.222 1583-1583/? D/setPageTransformer: view:147234376 | position:1.0
03-12 14:14:46.222 1583-1583/? D/setPageTransformer: view:75203809 | position:2.0
03-12 14:14:46.222 1583-1583/? D/setPageTransformer: view:35279366 | position:3.0

可以看到,在一开始,有4个子view被初始化,位置信息分别是 0.0 / 1.0 / 2.0 / 3.0 . 这是由于我设置了offscreenPageLimit 为3 ,所以除了当前view之外,还会初始化3个屏幕之外的view 。这就意味着:当前view的position是0,而往右边,position会递增,每递增1个view,就会加1.0, 反过来,我们也可以推导,往左边,每过一个view,position会递减. 为了验证我们的推导,我们滑动一下,观察position的变化.

向左滑动一格。

总结:

面试是一个不断学习、不断自我提升的过程,有机会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不一定能说得很好。

有些东西有压力才有动力,而学到的知识点,都是钱(因为技术人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。

附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有帮助!

会还是出去面面,至少能想到查漏补缺效果,而且有些知识点,可能你自以为知道,但让你说,并不一定能说得很好。

有些东西有压力才有动力,而学到的知识点,都是钱(因为技术人员大部分情况是根据你的能力来定级、来发薪水的),技多不压身。

附上我的面试各大专题整理: 面试指南,满满的都是干货,希望对大家有帮助!
[外链图片转存中…(img-yDPqssT0-1630942021097)]
CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值