protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
…
for (int i = 0; i < count; i++) {
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = 0;
int childTop = 0;
if (lp.isDecor) {
…
}
}
}
…
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
int loff = (int) (childWidth * ii.offset);
int childLeft = paddingLeft + loff;
int childTop = paddingTop;
if (lp.needsMeasure) {
// This was added during layout and needs measurement.
// Do it now that we know what we’re working with.
lp.needsMeasure = false;
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidth * lp.widthFactor),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
if (DEBUG) {
Log.v(TAG, “Positioning #” + i + " " + child + " f=" + ii.object
- “:” + childLeft + “,” + childTop + " " + child.getMeasuredWidth()
- “x” + child.getMeasuredHeight());
}
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
}
}
}
}
上面代码中,对 count
进行了两轮循环,其中第一轮是针对 lp.isDecor
为 true
的,
意为:如果当前view是一个 decoration 装饰,并不是adapter提供的view 则返回 true
显然,我们要探讨的是 adapter提供的View 是如何摆放的,所以忽略这一块。
而在下面的循环中,可以看到
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当前的位置信息返回到了外界。
那么外界拿到这两个参数值之后可以做什么事呢?理论上,可以做任何事
二,探索源码结论
-
ViewPager的初始子view摆放,都是横向的。在纵向上是上下平齐。
-
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:
v
i
e
w
.
h
a
s
h
C
o
d
e
(
)
∣
p
o
s
i
t
i
o
n
:
{view.hashCode()} | position:
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的变化.
向左滑动一格。
日志节略如下:
hashCode为 136851691
的子view,它的position从 原本的0.0,,最终变成了 -1.0
03-12 14:22:11.836 1583-1583/? D/setPageTransformer: view:136851691 | position:-1.0
而,原本hashCode为147234376,position为1的子view,position则变成了 0.0
03-12 14:22:11.836 1583-1583/? D/setPageTransformer: view:147234376 | position:0.0
再试试向又滑动一格,hashCode为 136851691
的子view, 从 -0.99326146
变成了0.0 , 这里的小数大概是由于计算精度丢失造成的。可以认为是 从-1.0
变为了0.0
.
画图描述刚才的结论(粉色是当前视野):
OK,了解到这里,position的变化规律基本也掌握了,那么接下来可以进行动画拆分 编程实现.
关键代码
有了思路,那么IT民工现在开始搬砖。
一,动画拆分各个击破
- 子view重叠排布
原本的子view都是横向,从左到又排布,默认的排布方式并没有相互覆盖. 所以我们可以考虑使用视图动画
(? 为什么是视图动画,而不是属性动画?因为没必要,当前的需求我只需要视觉效果上的位置变化,不需要子view的交互事件,用属性动画理论上应该也可以,但是直觉会存在交互问题,有时间再试试).
使用视图动画,将所有子view层叠在一起。原本都是横向排布,所以只需要将所有的view进行x轴位移,即可。
上代码:
公式的推导很简单,就是让右边的子view向左平移 -position个自身宽度.
效果为:
滑动之后,不再出现其他子view。
- 让多个子view之间呈现x轴上的位置差
虽然重叠在了一起,但是我还需要让右边的子view呈现位置偏差. 并且,越往右,偏差越大。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
尾声
以薪资待遇为基础,以发展为最终目标,要在高薪资的地方,谋求最好的发展!
下面是有几位Android行业大佬对应上方技术点整理的一些进阶资料。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
码获取!!(备注:Android)**
尾声
以薪资待遇为基础,以发展为最终目标,要在高薪资的地方,谋求最好的发展!
下面是有几位Android行业大佬对应上方技术点整理的一些进阶资料。
[外链图片转存中…(img-VXZvtmcg-1712301662073)]