转载于 http://blog.csdn.net/qq_17766199/article/details/68941610
ChangeTabLayout是我模仿乐视LIVE App主界面的TabLayout效果实现的,希望大家多多支持。
1.效果展示与说明
原效果图
原效果图转为Gif过大,所以将录制的MP4效果视频已经放入了项目根目录的preview文件夹内,有兴趣可去查看。(高清无码哦~)
实现效果图
ChangeTabLayout
在打开状态时
- 垂直方向切换时,文字的颜色大小变化。
- 水平方向切换时,文字的渐变与图片的变化。
ChangeTabLayout
在收起状态时
- 垂直方向切换时,图片的变化。
- 点击
ChangeTabLayout
,切换为打开状态。
2.分析
首先利用HierarchyViewer查看一下层级:
上图我们可以知道,TabLayout是一个ScrollView
,内容区域则是垂直ViewPager
嵌套了一个水平方向的ViewPager
。图片颜色的变化则是使用两个ImageView
叠加实现的。知道了这些,我们的思路大致就有了。当然我们不一定完全一样,可以按自己的方式处理。
最后贴一张我实现的最终效果:
可以看到我的结构会比较简洁一些,因为图片部分的效果我使用了自定义Drawable
去实现,所以不需在叠加一个ImageView
,也就少了外层的FrameLayout
,其次指示器我是用Canvas
去绘制的。所以少了外层的RelativeLayout
。
3.准备工作
上面我们提到有用到了垂直方向滑动的ViewPager
,那么我顺利的在传说中最大的“同性交友网站”Github上找到了VerticalViewPager,可惜此项目年代久远,比如setOnPageChangeListener
已经过时,而有时我们需添加多个监听器,能同时生效。所以我参考了VerticalViewPager
的思路,重新对现有的ViewPager
(25.1.0)源码进行了修改。(真是个细致活)
其次我想起了我曾经用到的SmartTabLayout,觉得使用起来很便捷。所以提前阅读了它的源码。所以此项目的实现结构大量的借鉴了它。
对于图片的变化部分,我找到了这篇自定义Drawables在研究了代码之后,根据需求在此基础上添加了垂直方向的判断,去除了多余的代码部分。
感谢以上作者的分享!那么万事具备,开搞!!
4.实现流程
在准备工作之后,首先明确我们还缺什么,那么剩余的就是文字部分、指示器部分、与承载这些组件的容器了。
1.文字部分
根据观察效果图,文字的变化是被指示器覆盖的部分,文字变为白色。且在页面垂直移动时,文字会有大小的变化。当然页面水平切换时,文字的渐变我们可以利用setAlpha
去实现。
ChangeTextView核心代码:
public ChangeTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setTextAlign(Paint.Align.LEFT); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); mPaint.setXfermode(mode); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); resetting(); Bitmap srcBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas srcCanvas = new Canvas(srcBitmap); RectF rectF; if (level == 10000 || level == 0) { rectF = new RectF(0, 0, 0, 0); }else if (level == 5000) { rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); }else{ float value = (level / 5000f) - 1f; if(value > 0){ rectF = new RectF(0, getMeasuredHeight() * value + indicatorPadding, getMeasuredWidth(), getMeasuredHeight()); }else{ rectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight() * (1 - Math.abs(value)) - indicatorPadding); } } srcCanvas.save(); srcCanvas.translate(0, (getMeasuredHeight() - mStaticLayout.getHeight()) / 2); mStaticLayout.draw(srcCanvas); srcCanvas.restore(); mPaint.setColor(selectedTabTextColor); srcCanvas.drawRect(rectF, mPaint); canvas.drawBitmap(srcBitmap, 0, 0, null); } private void resetting(){ float size; if (level == 5000) { size = textSize * 1.1f; }else if(level == 10000 || level == 0){ size = textSize * 1f; }else{ float value = (level / 5000f) - 1f; size = textSize + textSize * (1 - Math.abs(value))* 0.1f; } mTextPaint.setTextSize(size); mTextPaint.setColor(defaultTabTextColor); int num = (getMeasuredWidth() - indicatorPadding) / (int) size; mStaticLayout = new StaticLayout(text, 0, text.length() > num * 2 ? num * 2 : text.length(), mTextPaint, getMeasuredWidth() - indicatorPadding, Layout.Alignment.ALIGN_NORMAL, 1.0F, 0.0F, false); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
计算部分就不介绍了,文字的变化主要利用了我们常见的PorterDuffXfermode
的SRC_IN
模式。也就是取两层绘制交集,显示上层。如下图:
比如文字是黑色,这个遮罩是淡蓝色,那么重叠部分的文字就会变为淡蓝色。同时其余遮罩部分不显示。如下示意图:
那么我们变化RectF
的大小就可以控制文字的颜色变化。
那么为了可以显示多行文字,同时让文字可以随大小变化自动换行。我使用了StaticLayout
去实现。使用起来很简单方便。
2.指示器部分
这里的指示器、背景、阴影部分都放到了ScrollView
的子容器LinearLayout
中。
ChangeTabStrip
核心代码:
class ChangeTabStrip extends LinearLayout{ public ChangeTabStrip(Context context, @Nullable AttributeSet attrs) { super(context); setWillNotDraw(false); setOrientation(VERTICAL); } @Override protected void onDraw(Canvas canvas) { drawShadow(canvas); drawBackground(canvas); drawDecoration(canvas); } private void drawDecoration(Canvas canvas) { final int tabCount = getChildCount(); if (tabCount > 0) { View selectedTab = getChildAt(selectedPosition); int selectedTop = selectedTab.getTop(); int selectedBottom = selectedTab.getBottom(); int top = selectedTop; int bottom = selectedBottom; if (selectionOffset > 0f && selectedPosition < (getChildCount() - 1)) { View nextTab = getChildAt(selectedPosition + 1); int nextTop = nextTab.getTop(); int nextBottom = nextTab.getBottom(); top = (int) (selectionOffset * nextTop + (1.0f - selectionOffset) * top); bottom = (int) (selectionOffset * nextBottom + (1.0f - selectionOffset) * bottom); } drawIndicator(canvas, top, bottom); } } /** * 绘制左边阴影 */ private void drawShadow(Canvas canvas){ final float width = shadowWidth * (1 - selectionOffsetX); LinearGradient linearGradient = new LinearGradient(0, getHeight(), width, getHeight(), new int[] {shadowColor, Color.TRANSPARENT}, new float[]{shadowProportion, 1f}, Shader.TileMode.CLAMP); shadowPaint.setShader(linearGradient); canvas.drawRect(0, 0, width, getHeight(), shadowPaint); } /** * 绘制背景 */ private void drawBackground(Canvas canvas){ final float width = getWidth() * selectionOffsetX; canvas.drawRect(0, 0, width, getHeight(), backgroundPaint); } /** * 绘制指示器 */ private void drawIndicator(Canvas canvas, int top, int bottom) { final float width = getWidth() * selectionOffsetX; top = top + indicatorPadding; bottom = bottom - indicatorPadding; float leftBorderThickness = this.leftBorderThickness - getWidth() * (1 - selectionOffsetX); if(leftBorderThickness < 0){ leftBorderThickness = 0; } borderPaint.setColor(leftBorderColor); canvas.drawRect(0, top, leftBorderThickness, bottom, borderPaint); indicatorPaint.setColor(indicatorColor); indicatorRectF.set(leftBorderThickness, top, width, bottom); canvas.drawRect(indicatorRectF, indicatorPaint); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
这里没有什么特别的,主要就是根据ViewPager
的移动不断绘制新的位置。
3.TabLayout部分
首先创建子容器:
ChangeTabStrip tabStrip = new ChangeTabStrip(context, attrs)addView(tabStrip , LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
根据传入的PagerAdapter
,利用adapter.getCount()
方法创建相应数量的TabView,并添加至ChangeTabStrip
。简化代码如下:
private void populateTabStrip() { final PagerAdapter adapter = viewPager.getAdapter(); int size = adapter.getCount(); for (int i = 0; i < size; i++) { LinearLayout tabView = createTabView(adapter.getPageTitle(i), icon[i], 0); if (tabView == null) { throw new IllegalStateException("tabView is null."); } tabStrip.addView(tabView); if (i == viewPager.getCurrentItem()) { ChangeTextView textView = (ChangeTextView) tabView.getChildAt(1); textView.setLevel(5000); } } }protected LinearLayout createTabView(CharSequence title, int icon) { LinearLayout mLinearLayout = new LinearLayout(getContext()); mLinearLayout.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, tabViewHeight)); ImageView imageView = new ImageView(getContext()); RevealDrawable drawable = new RevealDrawable(DrawableUtils.getDrawable(getContext(), icon), DrawableUtils.getDrawable(getContext(), selectIcon), RevealDrawable.VERTICAL); imageView.setImageDrawable(drawable); ChangeTextView textView = new ChangeTextView(getContext(), attrs); textView.setText(title.toString()); mLinearLayout.addView(imageView); mLinearLayout.addView(textView); return mLinearLayout;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
监听垂直方向ViewPager
viewPager.addOnPageChangeListener(new InternalViewPagerListener());private class InternalViewPagerListener implements VerticalViewPager.OnPageChangeListener { private int scrollState; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { int tabStripChildCount = tabStrip.getChildCount(); if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { return; } tabStrip.onViewPagerPageChanged(position, positionOffset); scrollToTab(position, positionOffset); } @Override public void onPageScrollStateChanged(int state) { scrollState = state; } @Override public void onPageSelected(int position) { if (scrollState == ViewPager.SCROLL_STATE_IDLE) { scrollToTab(position, 0); } page = position; for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) { ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1); if (position == i) { textView.setLevel(5000); }else { textView.setLevel(0); } } } }private void scrollToTab(int tabIndex, float positionOffset) { final int tabStripChildCount = tabStrip.getChildCount(); if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { return; } LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex); int titleOffset = tabViewHeight * 2; int extraOffset = (int) (positionOffset * selectedTab.getHeight()); int y = (tabIndex > 0 || positionOffset > 0) ? -titleOffset : 0; int start = selectedTab.getTop(); y += start + extraOffset; scrollTo(0, y); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
垂直滑动时图片,文字的动态变化部分:
private void scrollToTab(int tabIndex, float positionOffset) { LinearLayout selectedTab = (LinearLayout) getTabAt(tabIndex); if (0f <= positionOffset && positionOffset < 1f) { if(!tabLayoutState){ ImageView imageView = (ImageView) selectedTab.getChildAt(0); ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.VERTICAL); imageView.setImageLevel((int) (positionOffset * 5000 + 5000)); } ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1); textView.setLevel((int) (positionOffset * 5000 + 5000)); } if(!(tabIndex + 1 >= tabStripChildCount)){ LinearLayout tab = (LinearLayout) getTabAt(tabIndex + 1); if(!tabLayoutState){ ImageView img = (ImageView) tab.getChildAt(0); ((RevealDrawable)img.getDrawable()).setOrientation(RevealDrawable.VERTICAL); img.setImageLevel((int) (positionOffset * 5000)); } ChangeTextView text = (ChangeTextView) tab.getChildAt(1); text.setLevel((int) (positionOffset * 5000)); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
水平方向滑动时图片,文字的动态变化部分:
final int tabStripChildCount = tabStrip.getChildCount(); if (tabStripChildCount == 0 || page < 0 || page >= tabStripChildCount) { return; } LinearLayout selectedTab = (LinearLayout) getTabAt(page); ImageView imageView = (ImageView) selectedTab.getChildAt(0); ((RevealDrawable)imageView.getDrawable()).setOrientation(RevealDrawable.HORIZONTAL); if (0f < positionOffset && positionOffset <= 1f) { imageView.setImageLevel((int) ((1 - positionOffset) * 5000 + 5000)); } for (int i = 0, size = tabStrip.getChildCount(); i < size; i++) { ChangeTextView textView = (ChangeTextView) ((LinearLayout) tabStrip.getChildAt(i)).getChildAt(1); if (0f < positionOffset && positionOffset <= 1f) { textView.setAlpha((1 - positionOffset)); if(positionOffset > 0.9f){ textView.setVisibility(INVISIBLE); }else{ textView.setVisibility(VISIBLE); } } }tabStrip.onViewPagerPageChanged(positionOffset);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
到此位置,大体流程就完了。
5.一些小问题的解决
1.充满整个屏幕
如果TabView数量较少时,高度未能撑满整个屏幕时,显示效果是这样的。
这样看起来有点尴尬了。虽然我设置了高度为MATCH_PARENT
但是没有起作用。
addView(tabStrip, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
很简单添加setFillViewport(true)
即可。顾名思义,这个属性允许 ScrollView
中的组件大小在不足时去充满它。
2.点击问题
看效果我们知道ChangeTabLayout
在收起时,虽然文字已经隐藏掉了,但是它仍然消耗着手势操作。导致收起时,我们无法点击下方的 ViewPager
,并且可以滑动ChangeTabLayout
。
我的解决方法是,计算文字部分的区域,进行判断是否拦截。
@Override public boolean onTouchEvent(MotionEvent event) { if(tabLayoutState){ return super.onTouchEvent(event); }else { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: return false; case MotionEvent.ACTION_MOVE: if(tabImageHeight + (int) (20 * density) < event.getRawX()){ return true; } break; } return super.onTouchEvent(event); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
3.文字的显示异常
再点击ChangeTabLayout
进行切换页面时,有时会导致如下异常显示。
原因通过排查后发现,我使用了viewPager.setCurrentItem(i)
方法进行切换。导致ViewPager
再切换中有一个平滑的滚动,监听方法onPageScrolled
收到了部分页面的反馈数值。当然简单的解决方法是使用viewPager.setCurrentItem(i, false)
进行切换。
然而倔强的我选择不将就(互相折磨到白头,悲伤坚决不放手~~)。想到了这样的解决办法。
在触摸ViewPager
时将flag
改为true。在点击切换时设置为false。每次变化前进行判断。
/** * tabView切换是否需要文字实时变化 */private boolean flag = false;private class ViewPagerTouchListener implements OnTouchListener{ @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: flag = true; break; } return false; } } if(flag){ ChangeTextView textView = (ChangeTextView) selectedTab.getChildAt(1); textView.setLevel((int) (positionOffset * 5000 + 5000)); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
4.onPageScrolled监听不正常
在竖屏状态下水平滑动的ViewPage
的onPageScrolled
监听不正常。
正常的打印是这样的:(滑动结束时 n页 – 0.0)
结果竖屏状态下会这样(两种):
这个异常打印有知道的望告知一下。感谢!
解决办法:
public void setPageScrolled(int p, int position, float positionOffset) { if (positionOffset > 0.99 && positionOffset < 1){ positionOffset = 0; position = position + 1; }else if (positionOffset < 0.01 && positionOffset > 0.00001){ positionOffset = 0; } }
发布了已经有几天了,也收到了大家反馈的问题。在此非常感谢!突然觉得细还是大家细,我还是太粗了。。。趁着有时间整理了以上的实现思路,希望对感兴趣的你有帮助。
源码在此,多多点赞点星哦~~
0 0