效果图

app中下面这样的控件很常见,像默认的TabHost表现上不够灵活,下面就简单写一个可以结合ViewPager切换内容显示,提供底部“滑动条”指示所显示页签的效果。

这里控件应对的场景是“水平等长度”的若干标题,标题不可滚动。

控件设计

下面是要实现的控件TabIndicator的组成部分:

  1. 底部指示器:也就是蓝色滑动条,记为Indicator。
  2. 分割线,宽度固定为1px的线条,可以不显示。记为Divider。
  3. 页签标题:记为TabView。
  4. 最底部的边框线,高度固定1px,就是给整个View的bottom部分一个分割线。

整体思路

整个TabIndicator是一个LinearLayout的子类,它包含水平方向的TabView——用来显示页签标题。

分割线、底部的指示器、底部的水平边框线都直接在TabIndicator.onDraw()中绘制。

方式很多,这里尽可能使用更少的View实现目标。当然标题文本可以不使用TextView自己绘制。如果需要按下标签时的背景切换效果,使用TextView更好些,而且文本换行,大小等也好控

制。

TabIndicator的设置

TabIndicator作为一个ViewGroup,它需要绘制内容的话就需要设置属性setWillNotDraw(false);以保证它的onDraw()被执行。

要知道childView绘制会覆盖ViewGroup本身的内容,所以这里的思路是利用paddingBottom为要绘制的底部Indicator和BorderLine预留空间。

在其构造方法中:

  1. public TabIndicator(Context context, AttributeSet attrs) {
  2. ...
  3. setWillNotDraw(false);
  4. setGravity(Gravity.CENTER_VERTICAL);
  5. setPadding(0, 0, 0, mIndicatorHeight);
  6. }

标签标题:TabView

将要显示的标题使用TextView进行显示,为了让水平方向等分宽度,childView设置weight为1。

然后为了显示容器绘制的Divider,俩个TabView之间需要预留空间,使用marginRight即可。

  1. private void buildTabStrip() {
  2. removeAllViews();
  3. PagerAdapter adapter = mViewPager.getAdapter();
  4. TabClickListener tabClickListener = new TabClickListener();
  5. int tabCount = adapter.getCount();
  6. int dividerWidth = (int) mDividerWidth;
  7. for (int i = 0; i < tabCount; i++) {
  8. LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
  9. params.weight = 1;
  10. if (dividerWidth > 0) {
  11. if (i != 0) {
  12. // use marginRight to make space for divider line.
  13. params.setMargins(dividerWidth, 0, 0, 0);
  14. }
  15. }
  16. TextView tabTitleView = createTabTitleView(params);
  17. tabTitleView.setText(adapter.getPageTitle(i));
  18. tabTitleView.setOnClickListener(tabClickListener);
  19. addView(tabTitleView);
  20. }
  21. }
  22. private TextView createTabTitleView(LinearLayout.LayoutParams params) {
  23. TextView textView = new TextView(getContext());
  24. textView.setGravity(Gravity.CENTER);
  25. textView.setBackgroundColor(Color.WHITE);
  26. textView.setLayoutParams(params);
  27. return textView;
  28. }

代码中params.weight、params.setMargins()的调用完成了上述操作。

要显示的TabView的个数是根据ViewPager关联的PagerAdapter.getCount()决定的,这里明确

一点:此处的TabIndicator不会像ActionBar自带Tabs视图那样水平滚动,它是一个等宽的页签指示器控件,适合2-6个TabView这样的场景,如果需求不是这样的,这里仅仅是一个思路。

TabClickListener用来监听各个TabView的点击,然后将ViewPager切换到对应位置:

  1. private class TabClickListener implements View.OnClickListener {
  2. @Override
  3. public void onClick(View v) {
  4. for (int i = 0; i < getChildCount(); i++) {
  5. if (v == getChildAt(i)) {
  6. mViewPager.setCurrentItem(i);
  7. return;
  8. }
  9. }
  10. }
  11. }

底部边界线

具体的绘制操作在onDraw()中进行。

边界线就是一条紧贴TabIndicator底部bottom的一个线条,canvas.drawLine()可以完成。

只需要注意一点:绘制的BorderLine的位置必须在TabIndicator的区域内,所以这里应该让

line的y坐标是TabIndicator本身的y减去1。

  1. protected void onDraw(Canvas canvas) {
  2. ...
  3. canvas.drawLine(getLeft(), tabHostHeight - 1, getRight(), tabHostHeight - 1, mBottomLinePaint);
  4. }

分割线:Divider

Divider需要在每两个TabView的中间进行绘制,在创建各个TabView时,已经使用marginRight预留了它的显示位置。其高度会在上下各减去一定的值int mDividerPadding,为了美观:

  1. protected void onDraw(Canvas canvas) {
  2. ...
  3. if (mEnableDivider && mDividerWidth > 0 && tabCount > 1) {
  4. View tab = getChildAt(0);
  5. if (mDividerPadding > tab.getHeight()) {
  6. mDividerPadding = tab.getHeight() / 2.0f;
  7. }
  8. float startY = tab.getY() + mDividerPadding;
  9. float stopY = tab.getY() + tab.getHeight() - mDividerPadding;
  10. mDividerPaint.setStrokeWidth(mDividerWidth);
  11. float halfDividerWidth = mDividerWidth / 2.0f;
  12. for (int i = 0; i < tabCount - 1; i++) {
  13. tab = getChildAt(i);
  14. canvas.drawLine(tab.getRight() + halfDividerWidth,
  15. startY, tab.getRight() + halfDividerWidth,
  16. stopY,
  17. mDividerPaint);
  18. }
  19. }
  20. }

同样是一个canvas.drawLine()指令进行绘制,其参数的计算代码是最好的解释。

底部指示器:滑动条

滚动条是有厚度的,所以使用canvas.drawRect()来进行绘制,方法需要绘制的矩形的四个坐标。

top、bottom是固定的。

left、right需要根据ViewPager的拖动进行确定:

假设从n滑动到n+1,那么计算出两个childView之间的水平距离,然后监听ViewPager的切换进度得到offset即可。

监听ViewPager的拖动使用OnPageChangeListener接口,这里为需要的交互规则定义了它的实现类:

  1. private class PageChangeListener extends ViewPager.SimpleOnPageChangeListener {
  2. private int mScrollState;
  3. @Override
  4. public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
  5. int tabCount = getChildCount();
  6. if ((tabCount == 0) || (position < 0) || (position >= tabCount)) {
  7. return;
  8. }
  9. onViewPagerPageChanged(position, positionOffset);
  10. if (mOuterPageListener != null) {
  11. mOuterPageListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
  12. }
  13. }
  14. @Override
  15. public void onPageScrollStateChanged(int state) {
  16. mScrollState = state;
  17. if (mOuterPageListener != null) {
  18. mOuterPageListener.onPageScrollStateChanged(state);
  19. }
  20. }
  21. @Override
  22. public void onPageSelected(int position) {
  23. // this is called before the onPageScrolled progress finished.
  24. // do not conflict with drag or setting-scroll.
  25. // ViewPager.setCurrentItem(index, animating) may need this?
  26. if (mScrollState == ViewPager.SCROLL_STATE_IDLE) {
  27. onViewPagerPageChanged(position, 0f);
  28. }
  29. if (mOuterPageListener != null) {
  30. mOuterPageListener.onPageSelected(position);
  31. }
  32. }
  33. }

为了让使用TabIndicator的代码可以继续监听ViewPager页面切换的事件,mOuterPageListener

用来保存外部代码提供的监听器。

回调方法onPageScrolled()用来通知ViewPager的拖动进度,positionOffset就是当前页面和目标页面切换的进度:0~1的一个float值。

监听器调用onViewPagerPageChanged()来做处理:

  1. public void onViewPagerPageChanged(int position, float positionOffset) {
  2. if (mSelectedPosition == position
  3. && mIndicatorOffset == positionOffset) return;
  4. mSelectedPosition = position;
  5. mIndicatorOffset = positionOffset;
  6. invalidate();
  7. }

记录下位置mSelectedPosition和切换进度mIndicatorOffset,然后通知当前TabIndicator进行绘制即可。紧接着在onDraw()中:

  1. protected void onDraw(Canvas canvas) {
  2. ...
  3. if (tabCount > 0) {
  4. int left = selectedTitle.getLeft();
  5. int right = selectedTitle.getRight();
  6. if (mIndicatorOffset > 0f && mSelectedPosition < (tabCount - 1)) {
  7. int offsetPixels = (int) (tabWidth * mIndicatorOffset);
  8. left += offsetPixels;
  9. right += offsetPixels;
  10. }
  11. canvas.drawRect(left, tabHostHeight - mIndicatorHeight, right,
  12. tabHostHeight, mIndicatorPaint);
  13. }
  14. }

对offsetPixels的计算很简单——这里的TabView是等宽的!!!

如果不是等宽的TabView,那么它们之间的水平位置差就是偏移的基准量。

NOTE

在PageChangeListener.onPageSelected()中的调用onViewPagerPageChanged(position, 0f)用来通知ViewPager发生的瞬间切换,这个在无动画的ViewPager.setCurrentItem()时会发生。------我没实验,这里为了以防万一。

记得对onViewPagerPageChanged()的调用为了不和onPageScrolled()中的调用冲突,它只在

ViewPager处在SCROLL_STATE_IDLE状态时进行。

小结

以上就是TabIndicator的所有内容,这类控件实在是可以很简单,更多的功能意味着更多的代码。

这里没有提供各种property/attrs的代码,保持关键代码的简单。

实际上不一定需要结合ViewPager,代码稍微修改,就可以满足一般的TabHost这类效果的需求。

源码在这里:

https://github.com/everhad/ViewPagerTabIndicator

(本文使用Atom编写)

[BOT]自定义ViewPagerStripIndicator的更多相关文章

  1. CSS实现自定义手型气泡提示

    实现自定义的手型气泡提示 <html> <head> <meta charset="utf-8"> <title></titl ...

  2. 恶意软件/BOT/C2隐蔽上线方式研究

    catalogue . 传统木马上线方式 . 新型木马上线方式 . QQ昵称上线 . QQ空间资料上线 . 第三方域名上线 . UDP/TCP二阶段混合上线 . Gmail CNC . NetBot两 ...

  3. IRC(Internet Relay Chat Protocol) Protocal Learning && IRC Bot

    catalogue . Abstract . INTRODUCTION . 通信协议Connection Registration Action . 通信协议Channel operations Ac ...

  4. jq自定义裁剪,代码超级简单,易修改

    1.自定义宽高效果 1.html 代码  index.html <!DOCTYPE html> <html lang="en"> <head> ...

  5. iOS 从xib中加载自定义视图

    想当初在学校主攻的是.NET,来到公司后,立马变成java开发,之后又跳到iOS开发,IT人这样真的好么~~  天有不测风云,云还有变幻莫测哎,废话Over,let's go~ 新学iOS开发不久,一 ...

  6. 关于Unity3D自定义编辑器的学习

    被人物编辑器折腾了一个月,最终还是交了点成品上去(还要很多优化都还么做).  刚接手这项工作时觉得没概念,没想法,不知道.后来就去看<<Unity5.X从入门到精通>>中有关于 ...

  7. 一起学微软Power BI系列-使用技巧(5)自定义PowerBI时间日期表

    1.日期函数表作用 经常使用Excel或者PowerBI,Power Pivot做报表,时间日期是一个重要的纬度,加上做一些钻取,时间日期函数表不可避免.所以今天就给大家分享一个自定义的做日期表的方法 ...

  8. JavaScript自定义浏览器滚动条兼容IE、 火狐和chrome

    今天为大家分享一下我自己制作的浏览器滚动条,我们知道用css来自定义滚动条也是挺好的方式,css虽然能够改变chrome浏览器的滚动条样式可以自定义,css也能够改变IE浏览器滚动条的颜色.但是css ...

  9. [BOT] 一种android中实现“圆角矩形”的方法

    内容简介 文章介绍ImageView(方法也可以应用到其它View)圆角矩形(包括圆形)的一种实现方式,四个角可以分别指定为圆角.思路是利用"Xfermode + Path"来进行 ...

随机推荐

  1. kaggle首秀之intel癌症预测(续篇)

    之前写了这篇文章.现在把他搬到知乎live上了.书非借不能读也,因此搞了点小费用,如果你觉得贵,加我微信我给你发红包返回给你. 最近的空余时间拿去搞kaggle了, 好久没更新文章了.今天写写kagg ...

  2. python_08 函数式编程、高阶函数、map、filter、reduce函数、内置函数

    函数式编程 编程方法论: 1.面向过程 找到解决问题的入口,按照一个固定的流程去模拟解决问题的流程 (1).搜索目标,用户输入(配偶要求),按照要求到数据结构内检索合适的任务 (2)表白,表白成功进入 ...

  3. Java Day26进程01天

    Java开启多个线程有两种方法,一种继承Thread类,一种实现Runnable接口.具体示例如下: 01继承Thread类 02实现Runnable接口

  4. 吴裕雄 python深度学习与实践(8)

    import cv2 import numpy as np img = cv2.imread("G:\\MyLearning\\TensorFlow_deep_learn\\data\\le ...

  5. java实现两个不同list对象合并后并排序

    工作上遇到一个要求两个不同list对象合并后并排序1.问题描述从数据库中查询两张表的当天数据,并对这两张表的数据,进行合并,然后根据时间排序.2.思路从数据库中查询到的数据放到各自list中,先遍历两 ...

  6. dagScheduler

    由一个action动作触发sparkcontext的runjob,再由此触发dagScheduler.runJob,然后触发submitJob,封装一个JobSubmitted放入一个队列.然后再通过 ...

  7. VueJs相关学习网址

      麦子学院 http://www.maiziedu.com/course/916/   慕课网-vue.js入门基础 https://www.imooc.com/learn/694   查阅的网址 ...

  8. react组件回顶部

    在挂载更新里面判断滚动条的距离(滚动条不能overflow: auto 踩坑) componentDidMount(){ window.addEventListener('scroll' , ()=& ...

  9. 二、putty的下载安装和基本使用方法教程

    转载自:https://baijiahao.baidu.com/s?id=1597811787635071952&wfr=spider&for=pc PuTTY是一款开源(Open S ...

  10. linux学习第八天 (Linux就该这么学)

    今天学了,mount 挂载,umount撤销挂载,.fdisk 命令 管理硬盘 交换分区swap,硬盘配额 xfs_quota命令 今天工作,手机看了,看的不全,回头看录播了.