先看一张效果图:
当时第一次接到这种需求,首先想到的是重写LinearLayout,然后监听ViewPager和Tab的点击事件滚动布局,但是写出来以后,遇到一个问题,就是顶部Tab只能随着点击事件或者ViewPager来滚动。
于是分析了一下TabLayout的源码,
TabLayout继承自HorizontalScrollView,如果UI要求不严格,基本的功能都可以实现。但是有一点不好的地方在于指示器(文字下的线)的宽度没有提供修改方法,且指示器位移动画也没办法自定义。
源自TabLayout的灵感,于是自定义了一个MyTabIndicator继承自HorizontalScrollVIew。
代码的基本结构:
MyTabIndicator.java:
public class MyTabIndicator extends HorizontalScrollView{
private class TabLayout extends LinearLayout{}
}
MyTabIndicator 嵌套子view TabLayout。
支持的自定义属性:
上面的xml,主要为了自定义属性,text的正常模式及大小,选中颜色及大小,背景色,指示器颜色,指示器宽度(指示器宽度最大值为Tab的宽度)
1.MyTabIndicator的构造方法
public MyTabIndicator(Context context) { this(context, null); } public MyTabIndicator(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyTabIndicator(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //读取xml中 if(attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTabIndicator); TEXTNORMALCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_textNormalColor, 0x80FFFFFF); TEXTSELECTEDCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_textSelectedColor, 0xFFFFFFFF); TEXTNORMALSIZE = typedArray.getDimension(R.styleable.MyTabIndicator_textNormalSize, 13); TEXTSELECTEDSIZE = typedArray.getDimension(R.styleable.MyTabIndicator_textSelectedSize, 15); BACKGROUNDCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_backGroundColor, 0xFF242425); INDICATORCOLOR = typedArray.getColor(R.styleable.MyTabIndicator_indicatorColor, 0xFFFFFFFF); INDICATORWIDTH = typedArray.getDimension(R.styleable.MyTabIndicator_indicatorWidth, 0); typedArray.recycle(); }else { TEXTNORMALCOLOR = Color.parseColor("#80FFFFFF"); TEXTSELECTEDCOLOR = Color.parseColor("#FFFFFF"); TEXTNORMALSIZE = 13; TEXTSELECTEDSIZE = 15; BACKGROUNDCOLOR = Color.parseColor("#242425"); INDICATORCOLOR = Color.parseColor("#FFFFFF"); INDICATORWIDTH = 0; } //隐藏滚动条 setHorizontalScrollBarEnabled(false); //添加tab容器 mTabLayout = new TabLayout(context); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mTabLayout.setLayoutParams(params); addView(mTabLayout); //初始化title宽度 mTitleWidth = getScreenWidth() / mTabVisibleCount; }
然后在TabIndicator中添加Tab容器TabLayout,这里需要设置mTabLayout的LayoutParams,宽度和高度都是matchparent。
接下来是初始化Tab标题的宽度,getScreenWidth()是屏幕的宽度。
2.TabLayout构造方法
public TabLayout(Context context) { this(context, null); } public TabLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(HORIZONTAL); setBackgroundColor(BACKGROUNDCOLOR); mPaintIndicator = new Paint(); mPaintIndicator.setColor(INDICATORCOLOR); mPaintIndicator.setStyle(Paint.Style.FILL); mPaintIndicator.setStrokeWidth(Local.dip2px(2)); }
这里主要用来初始化TabLayout的布局方向及背景颜色(这里遇到一个问题,如果不设置背景色,则onDraw方法得不到执行,为什么读者可以自行百度)。
接下来就是指示器画笔初始化,其中setStrokeWidth()方法是设置指示器的高度。
3.TabLayout的onDraw方法:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制指示器 canvas.save(); float x1 = mInitTranslationX + mTranslationX; float x2 = x1 + mIndicatorWidth; float y1, y2; y1 = y2 = getHeight() - 9; canvas.drawLine(x1, y1, x2, y2, mPaintIndicator); canvas.restore(); }
这里绘制指示器,读者也可以自己修改这段代码来绘制自己想要的指示器样式,我这里绘制的是line。
这个方法中mInitTranslationx 是指示器的偏移量,作用是让指示器与文字居中对齐。
mTranslationx是指示器的滑动时的偏移量,作用是控制指示器距离父布局左边的位移。
两个变量相加就可以控制指示器左侧的x坐标值。
y1,y2,是指示器的Y坐标值。
(对于Cavas的用法请读者自行百度)
4.setTitles设置Tab的数量及标题
/** * 填充标题 * @param datas */ public void setTitles(Listdatas){ this.mTabTitles = datas; // 如果传入的list有值,则移除布局文件中设置的view if (datas != null && datas.size() > 0) { mTabLayout.removeAllViews(); this.mTabTitles = datas; mTabVisibleCount = Math.min(datas.size(), COUNT_DEFAULT_MAX_TAB); mTitleWidth = getScreenWidth() / mTabVisibleCount; mIndicatorWidth = (int) (mTitleWidth * RADIO_TRIANGEL); if(INDICATORWIDTH > 0){ mIndicatorWidth = Math.min(mTitleWidth, (int)INDICATORWIDTH); } // 初始时的偏移量 mInitTranslationX = (int) ((mTitleWidth - mIndicatorWidth) / 2.0); for (String title : mTabTitles) { // 添加view mTabLayout.addView(getTitleView(title)); } // 设置item的click事件 setTitleItemClickEvent(); highLightTextView(0); } }
这个方法很关键,只有设置了此方法,才能有tab及title显示。计算tab宽度,指示器的初始偏移量及title的点击事件。
5.标题点击事件
/** * 标题点击事件 */ private void setTitleItemClickEvent() { int cCount = mTabLayout.getChildCount(); for (int i = 0; i < cCount; i++) { final int j = i; View view = mTabLayout.getChildAt(i); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(!isClick){ isClick = true; if(mViewPager == null){ Message msg = handler.obtainMessage(); mTranslationX++; msg.what = 3; msg.arg1 = j; handler.sendMessage(msg); resetTextViewColor(); highLightTextView(j); }else { mViewPager.setCurrentItem(j, false); } }else { if(!handler.hasMessages(3)) { isClick = false; } } } }); } }
点击事件进行了有没有ViewPager的判断,如果用户不需要与ViewPager联动,这里也默认保留了动画效果。如果用户想要点击事件与其它行为进行联动,可以在此方法中写一个回调接口实现自己的需求。
6.指示器的滑动
int speed = -1; /** * 回滚的速度 */ public int SCROLL_SPEED = -10; private Handler handler = new Handler(){ @Override public void handleMessage(final Message msg) { switch (msg.what){ case 3: if(!isClick)break; final int position = msg.arg1; float a = getWidth() / mTabVisibleCount * position - mTranslationX; if(speed == -1) { speed = (int) (a / mIndicatorWidth * 2); speed = Math.abs(speed); if(speed < 20){ speed = 20; } } if(a > 0){ SCROLL_SPEED = speed; }else if(a < 0){ SCROLL_SPEED = -speed; } mTranslationX += SCROLL_SPEED; if(a > -speed && a < speed){ mTranslationX = getWidth() / mTabVisibleCount * position; isClick = false; speed = -1; }else{ Message msg1 = handler.obtainMessage(); msg1.what = 3; msg1.arg1 = position; handler.sendMessage(msg1); } Log.i("mTranslationX", "" + mTranslationX); mTabLayout.invalidate(); break; } } };
hanlder的作用不断通过传递消息,重绘界面来达到指示器移动的效果。
speed的作用是用来确定速度,如果用户点击来tab,handler第一次传递消息就会确定一个滚动速度。
isClick是用来做点击事件的屏蔽,如果指示器滚动中,isClick为true。
7.ViewPager的滚动监听及对外回调接口
/** * 对外的ViewPager的回调接口 */ public interface PageChangeListener { void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); void onPageSelected(int position); void onPageScrollStateChanged(int state); } // 对外的ViewPager的回调接口 private PageChangeListener onPageChangeListener; // 对外的ViewPager的回调接口的设置 public void setOnPageChangeListener(PageChangeListener pageChangeListener) { this.onPageChangeListener = pageChangeListener; } // 设置关联的ViewPager public void setViewPager(ViewPager mViewPager, int pos) { this.mViewPager = mViewPager; mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { // 设置字体颜色高亮 resetTextViewColor(); highLightTextView(position); // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageSelected(position); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // 滚动 scroll(position, positionOffset); // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageScrollStateChanged(int state) { // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageScrollStateChanged(state); } } }); // 设置当前页 mViewPager.setCurrentItem(pos); // 高亮 highLightTextView(pos); }
8. ViewPager不为空情况下指示器的滚动控制方法:
/** * 指示器跟随手指滚动,以及容器滚动 * * @param position * @param offset */ public void scroll(final int position, float offset) { if(offset != 0.0){ isClick = false; } int tabWidth = getScreenWidth() / mTabVisibleCount; int last = 1; // 容器滚动,当移动到倒数最后一个的时候,开始滚动 if (offset > 0 && position >= (mTabVisibleCount - last) && mTabLayout.getChildCount() > mTabVisibleCount) { if (mTabVisibleCount != 1) { scrollTo((position - (mTabVisibleCount - last)) * tabWidth + (int) (tabWidth * offset), 0); } else { // 为count为1时 的特殊处理 scrollTo(position * tabWidth + (int) (tabWidth * offset), 0); } } else if (position < (mTabVisibleCount - last)) { scrollTo(0, 0); } if(!isClick) { // 不断改变偏移量,invalidate mTranslationX = getWidth() / mTabVisibleCount * (position + offset); mTabLayout.invalidate(); }else { Message msg = handler.obtainMessage(); mTranslationX++; msg.what = 3; msg.arg1 = position; handler.sendMessage(msg); } }
使用方法:
myTabIndicator.setTitles(mDatas); mViewPager.setAdapter(mAdapter); //设置关联的ViewPager myTabIndicator.setViewPager(mViewPager, 0);
源码下载地址:
https://github.com/736791050/TabIndicator