前言
2020年后第一篇,来点轻松的话题吧。在家办公,UI
美眉心血来潮要搞一个滑动特效。ViewPager+TabLayout
,老生常谈的东西了。 ViewPager
是基础的滑动切换控件,TabLayout
是和 ViewPager
配合使用的标题栏部分(但是 TabLayout
也可以脱离 ViewPager
独立使用)。根据查到的资料显示,谷歌工程师在 ViewPager
创立之时,就给风骚的动画特效预留了接口,我们可以很方便地去使用这个接口进行动画编程,但是 TabLayout
就比较悲情,不但动画没预留接口,甚至一些常规操作的接口都没有提供,所以网上也出现了一些人按照原 TabLayout
的代码,自己去创造新的 xxTabLayout
控件。
本文将提供 ViewPager+TabLayout
的实例效果,开发思路,以及**Demo **github
工程。有兴趣的童鞋们希望可以留言多多交流。
Demo地址:https://github.com/18598925736/StudyTabLayout/tree/hank_v1
正文大纲
-
参考效果
-
前置技能
-
实现思路
-
关键代码
-
思维拓展
正文
参考效果
上图 UI
美眉给的手机录屏,是蚂蚁财富 app
某一个版本上的滑动切换效果。
我们需要开发的是下面这一半这个滑动切换的控件。
前置技能
经过对 ViewPager
可能特效的研究,发现它自身就带有这种动画特效的可能性,不用我们去自定义控件。
但是上方的 TabLayout
字体大小变化,指示器indicator的长度和位置变化,谷歌给的TabLayout
貌似没法弄,所以只能自己 DIY
了.
要完成这个特效,两个技能必须就位:
-
android 视图动画 android体系中比较原始的一种动画类型。原理,是将view的绘制过程在指定区域,按照指定规则再进行一遍,但是原本view所携带的事件交互,则不受影响。由于无法真正地继承事件交互,所以被属性动画所取代。但是它仍然有自己的价值。在不涉及到交互,只考虑视觉效果的情况下,它的效率反而比属性动画更高。
-
数学建模思想 不要误会,这里说的数学建模是一种思维方式,把我们肉眼看到的现象,用数学公式的形式表达出来而已,并不是什么高深的操作。学过自定义控件并且深入实践过的童鞋应该能够体会到,要想真正从0开始完成一个
DIY
控件,会有大量的数学计算,而拥有好的数学思维能力,能够在自定义的时候如鱼得水。
实现思路
一、源码研究
要对ViewPager进行特效改造,那么首先我们要知道ViewPager是一个容器 ViewGroup
,它内部的子 View
是如何摆放的,虽然从视觉上我们能够感觉到 子view是横向摆放的,但是作为技术人,就要敢于追根究底,用源码说话。
进入源码,找到 onLayout
方法( 以下是我提炼的关键代码):
1. `protected void onLayout(boolean changed, int l, int t, int r, int b) {`
2. `final int count = getChildCount();`
3. `...`
4. `for (int i = 0; i < count; i++) {`
5. `if (child.getVisibility() != GONE) {`
6. `final LayoutParams lp = (LayoutParams) child.getLayoutParams();`
7. `int childLeft = 0;`
8. `int childTop = 0;`
9. `if (lp.isDecor) {`
10. `...`
11. `}`
12. `}`
13. `}`
14. `...`
15. `for (int i = 0; i < count; i++) {`
16. `final View child = getChildAt(i);`
17. `if (child.getVisibility() != GONE) {`
18. `final LayoutParams lp = (LayoutParams) child.getLayoutParams();`
19. `ItemInfo ii;`
20. `if (!lp.isDecor && (ii = infoForChild(child)) != null) {`
21. `int loff = (int) (childWidth * ii.offset);`
22. `int childLeft = paddingLeft + loff;`
23. `int childTop = paddingTop;`
24. `if (lp.needsMeasure) {`
25. `// This was added during layout and needs measurement.`
26. `// Do it now that we know what we're working with.`
27. `lp.needsMeasure = false;`
28. `final int widthSpec = MeasureSpec.makeMeasureSpec(`
29. `(int) (childWidth * lp.widthFactor),`
30. `MeasureSpec.EXACTLY);`
31. `final int heightSpec = MeasureSpec.makeMeasureSpec(`
32. `(int) (height - paddingTop - paddingBottom),`
33. `MeasureSpec.EXACTLY);`
34. `child.measure(widthSpec, heightSpec);`
35. `}`
36. `if (DEBUG) {`
37. `Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object`
38. `+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()`
39. `+ "x" + child.getMeasuredHeight());`
40. `}`
41. `child.layout(childLeft, childTop,`
42. `childLeft + child.getMeasuredWidth(),`
43. `childTop + child.getMeasuredHeight());`
44. `}`
45. `}`
46. `}`
47. `}`
上面代码中,对count
进行了两轮循环,其中第一轮是针对lp.isDecor
为true
的,意为:如果当前view是一个decoration装饰,并不是adapter提供的view则返回true。
显然,我们要探讨的是adapter提供的View是如何摆放的,所以忽略这一块。
而在下面的循环中,可以看到:
1. `child.layout(childLeft, childTop,`
2. `childLeft + child.getMeasuredWidth(),`
3. `childTop + child.getMeasuredHeight());`
这个便是child的排布的核心代码,追溯这4个参数,可以得知:第 1,3 参数表示left,right,他们都和一个 intloff=(int)(childWidth*ii.offset);
挂钩,而 第2,4参数表示 top,bottom,则并没有与任何动态参数相挂钩。
因此可以断定,ViewPager
的子View
排布,只会存在X轴方向上的位置偏差,在Y方向上会保持上下平齐。
其实还可以继续追溯 intloff=(int)(childWidth*ii.offset);
看看x轴方向上的位置偏差是如何造成的,但是目的已经达到,到有必要的时候再去追查。
确定是横向排布,那么左右滑动逻辑又是怎么样的呢?
找到 onTouchEvent()
方法,并且在其中找到 ACTION_MOVE 逻辑分支:
1. `case MotionEvent.ACTION_MOVE:`
2. `if (!mIsBeingDragged) {`
3. `final int pointerIndex = ev.findPointerIndex(mActivePointerId);`
4. `if (pointerIndex == -1) {`
5. `// A child has consumed some touch events and put us into an inconsistent`
6. `// state.`
7. `needsInvalidate = resetTouch();`
8. `break;`
9. `}`
10. `final float x = ev.getX(pointerIndex);`
11. `final float xDiff = Math.abs(x - mLastMotionX);`
12. `final float y = ev.getY(pointerIndex);`
13. `final float yDiff = Math.abs(y - mLastMotionY);`
14. `if (DEBUG) {`
15. `Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);`
16. `}`
17. `if (xDiff > mTouchSlop && xDiff > yDiff) {`
18. `if (DEBUG) Log.v(TAG, "Starting drag!");`
19. `mIsBeingDragged = true;`
20. `requestParentDisallowInterceptTouchEvent(true);`
21. `mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :`
22. `mInitialMotionX - mTouchSlop;`
23. `mLastMotionY = y;`
24. `setScrollState(SCROLL_STATE_DRAGGING);`
25. `setScrollingCacheEnabled(true);`
27. `// Disallow Parent Intercept, just in case`
28. `ViewParent parent = getParent();`
29. `if (parent != null) {`
30. `parent.requestDisallowInterceptTouchEvent(true);`
31. `}`
32. `}`
33. `}`
34. `// Not else! Note that mIsBeingDragged can be set above.`
35. `if (mIsBeingDragged) {`
36. `// Scroll to follow the motion event`
37. `final int activePointerIndex = ev.findPointerIndex(mActivePointerId);`
38. `final float x = ev.getX(activePointerIndex);`
39. `needsInvalidate |= performDrag(x);`
40. `}`
41. `break;`
我们需要关注的只是X方向上的拖拽
有什么规律。所以,顺着 finalfloatx=ev.getX(pointerIndex);
这个变量去找关键方法,最终锁定:performDrag(x);
它是处理X方向上位移的关键入口。
1. `private boolean performDrag(float x) {`
2. `boolean needsInvalidate = false;`
4. `final float deltaX = mLastMotionX - x;`
5. `mLastMotionX = x;`
7. `...`
8. `// Don't lose the rounded component`
9. `mLastMotionX += scrollX - (int) scrollX;`
10. `scrollTo((int) scrollX, getScrollY()); // 关键代码1, 控件在画布上的横像滚动`
11. `pageScrolled((int) scrollX);// 关键代码2,将 scrollX进一步往下传递`
13. `return needsInvalidate;`
14. `}`
发现两句关键代码,一个是处理滑动的scrolllTo
,一个是把 scrollX
往下传递的 pageScrolled(scrollX)
。前面一句都明白,但是这个第二句就有点不懂了,继续深入。
1. `private boolean pageScrolled(int xpos) {`
2. `...`
3. `final float pageOffset = (((float) xpos / width) - ii.offset)`
4. `/ (ii.widthFactor + marginOffset);`
5. `final int offsetPixels = (int) (pageOffset * widthWithMargin);`
7. `mCalledSuper = false;`
8. `onPageScrolled(currentPage, pageOffset, offsetPixels);`
9. `if (!mCalledSuper) {`
10. `throw new IllegalStateException(`
11. `"onPageScrolled did not call superclass implementation");`
12. `}`
13. `return true;`
14. `}`
追踪参数 xpos
得知,x方向上的偏移量信息,最后进入了onPageScrolled(...)
方法。
1. `protected void onPageScrolled(int position, float offset, int offsetPixels) {`
2. `// Offset any decor views if needed - keep them on-screen at all times.`
3. `if (mDecorChildCount > 0) {`
4. `... // 这里还是在处理 装饰,所以不用看,而且参数也没进入到这里`
5. `}`
7. `dispatchOnPageScrolled(position, offset, offsetPixels);`
9. `if (mPageTransformer != null) {`
10. `final int scrollX = getScrollX();`
11. `final int childCount = getChildCount();`
12. `for (int i = 0; i < childCount; i++) {`
13. `final View child = getChildAt(i);`
14. `final LayoutParams lp = (LayoutParams) child.getLayoutParams();`
16. `if (lp.isDecor) continue;`
17. `final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();`
18. `mPageTransformer.transformPage(child, transformPos);`
19. `}`
20. `}`
22. `mCalledSuper = true;`
23. `}`
又是两句关键代码:
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(…)方法,并且打印日志。
1. `viewPager.adapter = MyFragmentPagerAdapter(supportFragmentManager);`
2. `viewPager.offscreenPageLimit = 3 // 最少缓存3个,让左右两边都显示出来`
3. `viewPager.setPageTransformer(true, ViewPager.PageTransformer { view, position ->`
4. `Log.d("setPageTransformer", "view:${view.hashCode()} | position:${position}")`
5. `})`
然后启动app,看看日志:
1. `03-1214:14:46.2221583-1583/? D/setPageTransformer: view:136851691| position:0.0`
2. `03-1214:14:46.2221583-1583/? D/setPageTransformer: view:147234376| position:1.0`
3. `03-1214:14:46.2221583-1583/? D/setPageTransformer: view:75203809| position:2.0`
4. `03-1214:14:46.2221583-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。
1. `03-1214:22:11.8361583-1583/? D/setPageTransformer: view:136851691| position:-1.0`
而,原本hashCode为147234376,position为1的子view,position则变成了 0.0。
1. `03-1214:22:11.8361583-1583/? D/setPageTransformer: view:147234376| position:0.0`
再试试向又滑动一格,hashCode为 136851691
的子view,从 -0.99326146
变成了0.0 ,这里的小数大概是由于计算精度丢失造成的。可以认为是 从 -1.0
变为了 0.0
。
画图描述刚才的结论(粉色是当前视野):
OK,了解到这里,position的变化规律基本也掌握了,那么接下来可以进行动画拆分编程实现。下篇放送关键代码。
完结
Redis基于内存,常用作于缓存的一种技术,并且Redis存储的方式是以key-value的形式。Redis是如今互联网技术架构中,使用最广泛的缓存,在工作中常常会使用到。Redis也是中高级后端工程师技术面试中,面试官最喜欢问的问题之一,因此作为Java开发者,Redis是我们必须要掌握的。
Redis 是 NoSQL 数据库领域的佼佼者,如果你需要了解 Redis 是如何实现高并发、海量数据存储的,那么这份腾讯专家手敲《Redis源码日志笔记》将会是你的最佳选择。
从 -1.0
变为了 0.0
。
画图描述刚才的结论(粉色是当前视野):
[外链图片转存中…(img-vM05s8rj-1623621023381)]
OK,了解到这里,position的变化规律基本也掌握了,那么接下来可以进行动画拆分编程实现。下篇放送关键代码。
完结
Redis基于内存,常用作于缓存的一种技术,并且Redis存储的方式是以key-value的形式。Redis是如今互联网技术架构中,使用最广泛的缓存,在工作中常常会使用到。Redis也是中高级后端工程师技术面试中,面试官最喜欢问的问题之一,因此作为Java开发者,Redis是我们必须要掌握的。
Redis 是 NoSQL 数据库领域的佼佼者,如果你需要了解 Redis 是如何实现高并发、海量数据存储的,那么这份腾讯专家手敲《Redis源码日志笔记》将会是你的最佳选择。
[外链图片转存中…(img-g39uYpT2-1623621023382)]
感兴趣的朋友可以通过点赞+戳这里的方式免费获取腾讯专家手写Redis源码日志笔记pdf版本!