TabLayout这个导航控件,父类关系如下
public class TabLayout extends HorizontalScrollView
public class HorizontalScrollView extends FrameLayout
里边子空间的类型
class TabView extends LinearLayout implements OnLongClickListener
private class SlidingTabStrip extends LinearLayout
tablayout的结构图
大概看下源码,整体布局的添加如下,首先加了一个SlidingTabStrip也就是个线性布局.
public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ThemeUtils.checkAppCompatTheme(context);
// Disable the Scroll Bar
setHorizontalScrollBarEnabled(false);
// Add the TabStrip
mTabStrip = new SlidingTabStrip(context);
super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
完事通过绑定viewPager或者直接addTab来添加TabView
private void addTabView(Tab tab) {
final TabView tabView = tab.mView;
mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
}
添加的tabView的params如下,根据mode和gravity设置为比重为1或是wrap
private LinearLayout.LayoutParams createLayoutParamsForTabs() {
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
updateTabViewLayoutParams(lp);
return lp;
}
private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
lp.width = 0;
lp.weight = 1;
} else {
lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
lp.weight = 0;
}
}
至于TabView,最上边也说了也是个线性布局,垂直布局的,简单看下,默认的布局,里边添加了一个图片和一个TextView,自定义的就不说了。添加自定义的也就是把这两个默认的隐藏,完事add那个自定义的控件到TabView里而已
if (mCustomView == null) {
// If there isn't a custom view, we'll us our own in-built layouts
if (mIconView == null) {
ImageView iconView = (ImageView) LayoutInflater.from(getContext())
.inflate(R.layout.design_layout_tab_icon, this, false);
addView(iconView, 0);
mIconView = iconView;
}
if (mTextView == null) {
TextView textView = (TextView) LayoutInflater.from(getContext())
.inflate(R.layout.design_layout_tab_text, this, false);
addView(textView);
mTextView = textView;
mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
}
TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
if (mTabTextColors != null) {
mTextView.setTextColor(mTabTextColors);
}
updateTextAndIcon(mTextView, mIconView);
}
说了半天貌似和线条都没关系,好吧,再看下SlidingTabStrip的代码。里边有个onDraw方法,线条就是在这里加的
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Thick colored underline below the current selection
if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
}
}
我们要做的就是如何修改这个线条的左右边界。下边先说下老的做法和思路
前提条件
下边都是为了修改mode=fixed,tabGravity="fill"这种,完事要求线条宽度和文字宽度差不多这种需求。
如果是mode=scollable这种,你要求文字和线条宽度一样,那么设置如下属性基本都能满足需求的
app:tabMinWidth="2dp"
app:tabPadding="1dp"
app:tabPaddingStart="1dp"
app:tabPaddingEnd="1dp"
简单分析下,一些默认属性
mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);//横屏的时候默认style里这个是center
final Resources res = getResources();
mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
//添加tabview的时候会setminwidth的,就是调用如下方法获取最小宽度
//可以看到tab是有个最小宽度的,design_tab_scrollable_min_width手机是72dp,pad之类的是160dp
private int getTabMinWidth() {
if (mRequestedTabMinWidth != INVALID_WIDTH) {
// If we have been given a min width, use it
return mRequestedTabMinWidth;
}
// Else, we'll use the default value
return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
}
老的做法和思路
最早比较笨的想法,既然你这线条是画出来了,那我也画一个好了,不过因为那线条左右边界是动态的,想着麻烦,就弄个固定的好了,也就是tab切换的时候才改变线条,少了滑动效果。
我们的需求是线条和文字宽度差不多,那第一步肯定是获取到文字的宽度了,文字的宽度哪来的,当然是获取到tabView里的那个TextView的宽度了。
获取方法有两种,第一种反射,第二种直接getChildAt
QQ截图20171025170903.png看下Tab里的那个mView就是我们要的TabView,里边就包含有TextView
下边就是重写TabLayout,给他画条线,缺点就是线不能滑动,我们通过监听tab选中状态的改变,来invalidate这个布局刷新线条。另外因为有了自己的线条了,所以需要把TabLayout的线条高度设置为0或者线条颜色弄为透明
那个factor就是线条长度和文字宽度的比例,为1就是一样,比1大就是稍微出去一点
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
TabCheck();
canvas.drawRect(rectF.left,getHeight()-indicatorHeight,rectF.right,getHeight(),paintLine);
}
RectF rectF=new RectF();
private void TabCheck(){
try {
//通过反射获取那个textView
Tab tab=getTabAt(getSelectedTabPosition());
Field field=tab.getClass().getDeclaredField("mView");
field.setAccessible(true);
LinearLayout linearLayout= (LinearLayout) field.get(tab);
/**child1就是tab上的文字控件,第一个是图片控件,第二个就是这个文本控件*/
View child1=linearLayout.getChildAt(1);//
float add=(factor-1)*child1.getWidth()/2;
rectF.left=linearLayout.getLeft()+child1.getLeft()-add;
rectF.right=linearLayout.getLeft()+child1.getRight()+add;
} catch (Exception e) {
e.printStackTrace();
}
//根据整体控件的结构,我们也能拿到那个textView
// int selectedPosition=getSelectedTabPosition();
// LinearLayout slidingTabStrip=(LinearLayout)getChildAt(0);
// LinearLayout tabView= (LinearLayout) slidingTabStrip.getChildAt(selectedPosition);
// View textView=tabView.getChildAt(1);
}
新的做法和思路
SlidingTabStrip里有下边的代码是来计算线条左右边距的,根据viewpager的偏移量动态改变
left 和right就是对应的线条的左右点位置
private void updateIndicatorPosition() {
final View selectedTitle = getChildAt(mSelectedPosition);
int left, right;
if (selectedTitle != null && selectedTitle.getWidth() > 0) {
left = selectedTitle.getLeft();
right = selectedTitle.getRight();
if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
// Draw the selection partway between the tabs
View nextTitle = getChildAt(mSelectedPosition + 1);
left = (int) (mSelectionOffset * nextTitle.getLeft() +
(1.0f - mSelectionOffset) * left);
right = (int) (mSelectionOffset * nextTitle.getRight() +
(1.0f - mSelectionOffset) * right);
}
} else {
left = right = -1;
}
setIndicatorPosition(left, right);
}
既然线条的宽度位置都和tabView有关,那么我们改变tabView的大小即可,默认的tabView的大小就是平分TabLayout的。
这里再强调下,我们的这个自定义只支持下边的属性,默认的就是这两个
app:tabGravity="fill"
app:tabMode="fixed"
完整的代码如下
import android.content.Context
import android.support.design.widget.TabLayout
import android.util.AttributeSet
import android.view.ViewTreeObserver
import android.widget.LinearLayout
import android.widget.TextView
import java.lang.reflect.Field
/**
* Created by Sage on 2017/10/25.
* Description:此控件只适用于 app:tabMode="fixed"
*/
class TabLayoutIndicatorShort : TabLayout {
constructor(context: Context?) : super(context) {
initSomeThing()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
initSomeThing()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initSomeThing()
}
private fun initSomeThing() {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
changeIndicator()
}
})
}
val factor = 1.1f
fun changeIndicator() {
if(tabCount==0){
return
}
val tabLayout:Class<*> = javaClass.superclass
var tabStrip: Field? = null
try {
tabStrip = tabLayout.getDeclaredField("mTabStrip")
} catch (e: Exception) {
e.printStackTrace()
return
}
tabStrip!!.isAccessible = true
var ll_tab: LinearLayout? = null
try {
ll_tab = tabStrip.get(this) as LinearLayout
} catch (e: Exception) {
e.printStackTrace()
return
}
/**每个tab的宽,总宽度除以tabCount*/
val widthTab = width / tabCount
for (i in 0..ll_tab.childCount - 1) {
val child = ll_tab.getChildAt(i)
child.setPadding(0, 0, 0, 0)
try {
val tv = (child as LinearLayout).getChildAt(1) as TextView
var margin = ((widthTab - tv.width * factor) / 2).toInt()
println("i==" + i + "==widthTab=" + widthTab + "==child w=" + tv.width + "==margin=" + margin)
if (margin < 0) {
margin = 0
}
val params = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, tv.width.toFloat())
params.leftMargin = margin
params.rightMargin = margin
child.setLayoutParams(params)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
最后看下修改后的效果和原生的效果。
QQ截图20171026094315.png原生的线条是平分的,点击范围也很大,我们修改后的线条是小了,可点击范围也小了。我还是喜欢原生的,可惜啊,很多时候ui设计的都是线条和文字宽度一样。如果不要求滚动的时候线条动画,我还是喜欢老的那种,直接画条线,也不影响点击范围。
最后的实现
以前懒得写啊,最近闲了,就抽空把这个实现吧
先看下效果图
image.png
移动到一半效果如下图
image.png
移动了超过一半,如下图
image.png
移动完成
image.png
简单说下思路:
隐藏掉原生画的线条【把颜色设置为透明,或者你把高度弄为0也可以】
然后给viewpager添加监听滑动的偏移量,我们来计算线条的位置
偏移量在0.5以下,
线条left位置从第一个textview的left位置移动到第一个的中心位置,
线条right位置从第一个textview的right位置移动到第二个textview的中心位置
偏移量0.5到1之间的话,
线条left位置是从第一个textview的中心位置到第二个textview的left位置
线条right位置是从第二个textview的中心位置到第二个textview的rigth位置
代码如下
import android.content.Context
import android.graphics.Canvas
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.util.AttributeSet
import android.graphics.Color
import android.graphics.Paint
import android.widget.LinearLayout
import android.graphics.RectF
import android.support.design.R
import android.support.v4.view.ViewCompat
class TabLayoutFixedFill : TabLayout {
constructor(context: Context) : super(context) {
initAttrs(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initAttrs(context, attrs)
}
private fun initAttrs(context: Context, attrs: AttributeSet?) {
val a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
0, R.style.Widget_Design_TabLayout)
indicotorHeight = a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)
paintLine.color = a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)
a.recycle()
setSelectedTabIndicatorColor(Color.TRANSPARENT)//隐藏掉原生画的线
}
var factor = 1f//线条的长度和文字宽度的比例,因为有的需求是比文字稍微长点。所以这里可以修改
var indicotorHeight = 2;//线条的高度
var paintLine = Paint()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(rectIndicator.left, getHeight().toFloat() - indicotorHeight, rectIndicator.right, getHeight().toFloat(), paintLine);
}
override fun setupWithViewPager(viewPager: ViewPager?, autoRefresh: Boolean) {
super.setupWithViewPager(viewPager, autoRefresh)
viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
updateIndicator(position, positionOffset)
}
override fun onPageSelected(position: Int) {
}
})
}
var rectIndicator = RectF()//记录下要画的线条的left和right位置
fun updateIndicator(position: Int, positionOffset: Float) {
if(position>=tabCount){
return
}
var rectF = getTextViewRect(position)
var rectF2 = rectF
if (position < tabCount - 1) {
rectF2 = getTextViewRect(position + 1)
}
if (positionOffset < 0.5) {
rectIndicator.left = rectF.left + rectF.width * positionOffset //第一个最左边移动到第一个的中心位置
rectIndicator.right = rectF.right + (rectF2.center - rectF.right) * positionOffset * 2 //移动范围,从第一个右边,移动到另一个控件的中心位置
} else {
rectIndicator.left = rectF2.left - (rectF2.left - rectF.center) * (1 - positionOffset) * 2 //移动范围,从第一个中心到另一个最左边
rectIndicator.right = rectF2.left + rectF2.width * positionOffset//第二个中心点到第二个的右边
}
ViewCompat.postInvalidateOnAnimation(this@TabLayoutFixedFill)
}
/**找出某个tabview里Textview的left和right位置*/
private fun getTextViewRect(selectedPosition: Int): ViewOption {
var slidingTabStrip = getChildAt(0) as LinearLayout
var tabView = slidingTabStrip.getChildAt(selectedPosition) as LinearLayout
var textView = tabView.getChildAt(1);
val add = (factor - 1) * textView.width / 2
return ViewOption(tabView.left + textView.left - add, tabView.left + textView.right + add)
}
/**记录下view的left,right,center ,and width*/
data class ViewOption(var left: Float, var right: Float, var center: Float = (right + left) / 2f, var width: Float = (right - left))
}
如果有人说我没有绑定viewpager咋办,也很简单,我上边的updateIndicator方法,你可以在切换tab的时候调用这个方法也可以。或者你也可以直接用最老的那种方法,就是画条线的那个也可以。
其他一些看源码的收获
如果我们不设置tablayout的高度的话,用个warp,那么他的高度其实是固定的。
图片文字同时存在,是72dp,只有文字的话48dp
网友评论