刘正的技术博客

胆小认生,不易相处

擅长Android技能,深入研究移动端开发,此为博客一枚。


自定义TabLayout

本文为kotlin仿开眼视频Android客户端的后续补充内容,本篇为大家介绍如何对TabLayout进行定制使用,基于项目需求,本篇主要对部分功能进行了定制,如:指示器距离文字的距离、文字选中加粗、文字选中变大等

本文部分代码参考:FlycoTabLayout

效果图

效果-1

效果-2

参数详解

属性源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<declare-styleable name="MyTabLayout">
<attr name="textNormalSize" format="dimension" />
<attr name="textSelectSize" format="dimension" />
<attr name="textNormalColor" format="color" />
<attr name="textSelectColor" format="color" />
<attr name="underLineHeight" format="dimension" />
<attr name="underlineColor" format="color" />
<attr name="indicatorHeight" format="dimension" />
<attr name="indicatorWidth" format="dimension" />
<attr name="indicatorSpacing" format="dimension" />
<attr name="isTextBold" format="boolean" />
<attr name="tabPadding" format="dimension" />
<attr name="indicatorColor" format="color" />
<attr name="tabSpaceEqual" format="boolean" />
</declare-styleable>

属性参数详解

参数 用处
textNormalSize tab未被选中时字体大小
textSelectSize tab被选中时字体大小
textNormalColor tab未被选中时字体颜色
textSelectColor tab被选中时字体颜色
underLineHeight tablayout下面整个一条的线的高度
underlineColor tablayout下面整个一条的线的颜色
indicatorHeight 文字下方指示器的高度
indicatorWidth 文字下方指示器的宽度
indicatorSpacing 文字下方指示器距离上面文字的间距
indicatorColor 文字下方指示器的颜色
isTextBold 文字选中时是否加粗
tabPadding 文字左右间的间隙大小
tabSpaceEqual tab中的文字是否等分tablayout的宽度,参考效果-2

与viewpager合并使用

1
2
3
4
5
6
7
8
var mAdapter = MyPagerAdapter(fragmentManager)
viewpager.adapter = mAdapter
//tab列表
val stringArray = mTitle.toArray(arrayOfNulls<String>(0))
tab_layout.setViewPager(viewpager, stringArray as Array<String>)
viewpager.offscreenPageLimit = 3
viewpager.currentItem = 0
viewpager.addOnPageChangeListener(this)

源码

使用参考详见:https://github.com/momentslz/Eyepetizer

源码文件下载地址:
https://github.com/momentslz/Eyepetizer/blob/master/app/src/main/java/com/moment/eyepetizer/view/TabLayout.kt

或如代码如下:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
/**
* Created by moment on 2018/2/22.
* FlycoTabLayout 仿写,并提取部分功能原地址链接:
* https://github.com/H07000223/FlycoTabLayout
*/
open class TabLayout(context: Context, attrs: AttributeSet) : HorizontalScrollView(context, attrs), ViewPager.OnPageChangeListener {
private var mContext: Context? = null
private var textNormalSize: Float = 0.toFloat()
private var textSelectSize: Float = 0.toFloat()
private var textNormalColor: Int = 0
private var textSelectColor: Int = 0
private var underlineHeight: Float = 0.toFloat()
private var underlineColor: Int = 0
private var indicatorHeight: Float = 0.toFloat()
private var indicatorWidth: Float = 0.toFloat()
private var indicatorSpacing: Float = 0.toFloat()
private var isTextBold: Boolean = false
private var tabPadding: Float = 0.toFloat()
private var indicatorColor: Int = 0
private var tabSpaceEqual: Boolean = false


private var selectListener: OnTagSelectListener? = null
private var mTabsContainer: LinearLayout? = null
private var mCurrentTab = 0
private var mViewPager: ViewPager? = null
var tabCount: Int = 0
private set
private var mTitles: ArrayList<String>? = null

private val mRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)

private val mIndicatorDrawable = GradientDrawable()
/**
* indicator
*/

private val mIndicatorMarginLeft = 0f
private val mIndicatorMarginTop = 0f
private val mIndicatorMarginRight = 0f
/**
* 用于绘制显示器
*/
private val mIndicatorRect = Rect()

private var mCurrentPositionOffset: Float = 0.toFloat()

/**
* 用于实现滚动居中
*/
private val mTabRect = Rect()
private var mLastScrollX: Int = 0

init {
initResource(context, attrs)
}


private fun initResource(context: Context, attrs: AttributeSet) {
mContext = context
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTabLayout)
textNormalSize = typedArray.getDimension(R.styleable.MyTabLayout_textNormalSize, sp2px(context, 14f).toFloat())
textSelectSize = typedArray.getDimension(R.styleable.MyTabLayout_textSelectSize, textNormalSize)
textNormalColor = typedArray.getColor(R.styleable.MyTabLayout_textNormalColor, Color.GRAY)
textSelectColor = typedArray.getColor(R.styleable.MyTabLayout_textSelectColor, Color.BLACK)
underlineHeight = typedArray.getDimension(R.styleable.MyTabLayout_underLineHeight, dp2px(context, 0.5f).toFloat())
indicatorWidth = typedArray.getDimension(R.styleable.MyTabLayout_indicatorWidth, -1f)
indicatorHeight = typedArray.getDimension(R.styleable.MyTabLayout_indicatorHeight, dp2px(context, 3f).toFloat())
indicatorSpacing = typedArray.getDimension(R.styleable.MyTabLayout_indicatorSpacing, dp2px(context, 6f).toFloat())
isTextBold = typedArray.getBoolean(R.styleable.MyTabLayout_isTextBold, true)
underlineColor = typedArray.getColor(R.styleable.MyTabLayout_underlineColor, Color.parseColor("#EEEEEE"))
indicatorColor = typedArray.getColor(R.styleable.MyTabLayout_indicatorColor, Color.BLACK)
tabSpaceEqual = typedArray.getBoolean(R.styleable.MyTabLayout_tabSpaceEqual, false)
tabPadding = typedArray.getDimension(R.styleable.MyTabLayout_tabPadding, (if (tabSpaceEqual) dp2px(context, 5f) else dp2px(context, 20f)).toFloat())
typedArray.recycle()

mRectPaint.color = underlineColor
mIndicatorDrawable.setColor(indicatorColor)
mTabsContainer = LinearLayout(context)
val lp = ViewGroup.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
mTabsContainer!!.layoutParams = lp
mTabsContainer!!.gravity = Gravity.CENTER_VERTICAL
isFillViewport = true
setWillNotDraw(false)
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
addView(mTabsContainer)

}

//setter and getter
fun setCurrentTab(currentTab: Int) {
this.mCurrentTab = currentTab
mViewPager!!.currentItem = currentTab

}

fun setCurrentTab(currentTab: Int, smoothScroll: Boolean) {
this.mCurrentTab = currentTab
mViewPager!!.setCurrentItem(currentTab, smoothScroll)
}

fun addTab(position: Int, title: String?, textView: TextView?) {
if (textView != null) {
if (title != null) {
textView.gravity = Gravity.CENTER
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textNormalSize)
textView.setTextColor(textNormalColor)
textView.text = title
textView.setOnClickListener { v ->
val position = mTabsContainer!!.indexOfChild(v)
if (position != -1) {
if (mViewPager!!.currentItem != position) {
mViewPager!!.currentItem = position
} else {
if (selectListener != null) {
selectListener!!.onTagSelected(position, if (mTitles != null) mTitles!![position] else mViewPager!!.adapter.getPageTitle(position).toString())
}
}
}
}
}
}
val lp = if (tabSpaceEqual)
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f)
else
LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
lp.gravity = Gravity.CENTER
mTabsContainer!!.addView(textView, position, lp)

}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val height = height
val paddingLeft = paddingLeft
var top = 0
var bottom = height

if (mTabsContainer != null && mTabsContainer!!.getChildAt(mCurrentTab) != null) {
top = mTabsContainer!!.getChildAt(mCurrentTab).top
bottom = mTabsContainer!!.getChildAt(mCurrentTab).bottom
}

val tabHeight = top + bottom

canvas.drawRect(paddingLeft.toFloat(), height - underlineHeight, (mTabsContainer!!.width + paddingLeft).toFloat(), height.toFloat(), mRectPaint)

calcIndicatorRect()

if (indicatorHeight > 0 && indicatorWidth > 0) {
mIndicatorDrawable.setBounds(paddingLeft + mIndicatorMarginLeft.toInt() + mIndicatorRect.left,
tabHeight - tabPadding.toInt() * 2 + indicatorSpacing.toInt(),
paddingLeft + mIndicatorRect.right - mIndicatorMarginRight.toInt(),
tabHeight - tabPadding.toInt() * 2 + indicatorSpacing.toInt() + indicatorHeight.toInt())

mIndicatorDrawable.draw(canvas)
} else {
mIndicatorDrawable.setBounds(paddingLeft + mIndicatorMarginLeft.toInt() + mIndicatorRect.left,
height - indicatorHeight.toInt(),
paddingLeft + mIndicatorRect.right - mIndicatorMarginRight.toInt(),
height)
mIndicatorDrawable.draw(canvas)
}
}

private fun calcIndicatorRect() {
val currentTabView = mTabsContainer!!.getChildAt(this.mCurrentTab) ?: return
var left = currentTabView.left.toFloat()
var right = currentTabView.right.toFloat()

if (this.mCurrentTab < tabCount - 1) {
val nextTabView = mTabsContainer!!.getChildAt(this.mCurrentTab + 1)
val nextTabLeft = nextTabView.left.toFloat()
val nextTabRight = nextTabView.right.toFloat()

left += mCurrentPositionOffset * (nextTabLeft - left)
right += mCurrentPositionOffset * (nextTabRight - right)
}

mIndicatorRect.left = left.toInt()
mIndicatorRect.right = right.toInt()

mTabRect.left = left.toInt()
mTabRect.right = right.toInt()

if (indicatorWidth >= 0) {
//indicatorWidth大于0时,圆角矩形以及三角形
var indicatorLeft = currentTabView.left + (currentTabView.width - indicatorWidth) / 2

if (this.mCurrentTab < tabCount - 1) {
val nextTab = mTabsContainer!!.getChildAt(this.mCurrentTab + 1)
indicatorLeft += mCurrentPositionOffset * (currentTabView.width / 2 + nextTab.width / 2)
}

mIndicatorRect.left = indicatorLeft.toInt()
mIndicatorRect.right = (mIndicatorRect.left + indicatorWidth).toInt()
}
}

/**
* 关联ViewPager,用于不想在ViewPager适配器中设置titles数据的情况
*/
fun setViewPager(vp: ViewPager?, titles: Array<String>) {
if (vp == null || vp.adapter == null) {
throw IllegalStateException("ViewPager or ViewPager adapter can not be NULL !")
}

if (titles == null || titles.size == 0) {
throw IllegalStateException("Titles can not be EMPTY !")
}

if (titles.size != vp.adapter.count) {
throw IllegalStateException("Titles length must be the same as the page count !")
}

this.mViewPager = vp
mTitles = ArrayList()
Collections.addAll(mTitles!!, *titles)

this.mViewPager!!.removeOnPageChangeListener(this)
this.mViewPager!!.addOnPageChangeListener(this)
notifyDataSetChanged()
}

/**
* 关联ViewPager,用于连适配器都不想自己实例化的情况
*/
fun setViewPager(vp: ViewPager?, titles: Array<String>?, fa: FragmentActivity, fragments: ArrayList<Fragment>) {
if (vp == null) {
throw IllegalStateException("ViewPager can not be NULL !")
}

if (titles == null || titles.isEmpty()) {
throw IllegalStateException("Titles can not be EMPTY !")
}

this.mViewPager = vp
this.mViewPager!!.adapter = InnerPagerAdapter(fa.supportFragmentManager, fragments, titles)

this.mViewPager!!.removeOnPageChangeListener(this)
this.mViewPager!!.addOnPageChangeListener(this)
notifyDataSetChanged()
}

/**
* 更新数据
*/
private fun notifyDataSetChanged() {
mTabsContainer!!.removeAllViews()
this.tabCount = if (mTitles == null) mViewPager!!.adapter.count else mTitles!!.size
var tabView: TextView
for (i in 0 until tabCount) {
tabView = TextView(mContext)
val pageTitle = if (mTitles == null) mViewPager!!.adapter.getPageTitle(i) else mTitles!![i]
addTab(i, pageTitle.toString(), tabView)
}

updateTabStyles()
}

private fun updateTabStyles() {
for (i in 0 until tabCount) {
val v = mTabsContainer!!.getChildAt(i)
val tv_tab_title = v as TextView
if (tv_tab_title != null) {
tv_tab_title.setTextColor(if (i == mCurrentTab) textSelectColor else textNormalColor)
tv_tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, if (i == mCurrentTab) textSelectSize else textNormalSize)
tv_tab_title.setPadding(tabPadding.toInt(), tabPadding.toInt(), tabPadding.toInt(), tabPadding.toInt())
if (isTextBold) {
tv_tab_title.paint.isFakeBoldText = i == mCurrentTab
}

}
}
}

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
this.mCurrentTab = position
this.mCurrentPositionOffset = positionOffset
scrollToCurrentTab()
invalidate()
}

/**
* HorizontalScrollView滚到当前tab,并且居中显示
*/
private fun scrollToCurrentTab() {
if (tabCount <= 0) {
return
}

val offset = (mCurrentPositionOffset * mTabsContainer!!.getChildAt(mCurrentTab).width).toInt()
/**当前Tab的left+当前Tab的Width乘以positionOffset */
var newScrollX = mTabsContainer!!.getChildAt(mCurrentTab).left + offset

if (mCurrentTab > 0 || offset > 0) {
/**HorizontalScrollView移动到当前tab,并居中 */
newScrollX -= width / 2 - paddingLeft
calcIndicatorRect()
newScrollX += (mTabRect.right - mTabRect.left) / 2
}

if (newScrollX != mLastScrollX) {
mLastScrollX = newScrollX
/** scrollTo(int x,int y):x,y代表的不是坐标点,而是偏移量
* x:表示离起始位置的x水平方向的偏移量
* y:表示离起始位置的y垂直方向的偏移量
*/
scrollTo(newScrollX, 0)
}
}

override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle()
bundle.putParcelable("instanceState", super.onSaveInstanceState())
bundle.putInt("mCurrentTab", mCurrentTab)
return bundle
}

override fun onRestoreInstanceState(state: Parcelable?) {
var state = state
if (state is Bundle) {
val bundle = state as Bundle?
mCurrentTab = bundle!!.getInt("mCurrentTab")
state = bundle.getParcelable("instanceState")
if (mCurrentTab != 0 && mTabsContainer!!.childCount > 0) {
updateTabSelection(mCurrentTab)
scrollToCurrentTab()
}
}
super.onRestoreInstanceState(state)
}

private fun updateTabSelection(position: Int) {
for (i in 0 until tabCount) {
val tabView = mTabsContainer!!.getChildAt(i)
val isSelect = i == position
val tab_title = tabView as TextView

if (tab_title != null) {
tab_title.setTextColor(if (isSelect) textSelectColor else textNormalColor)
tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, if (isSelect) textSelectSize else textNormalSize)
if (isTextBold) {
tab_title.paint.isFakeBoldText = isSelect
}
}
}
}

override fun onPageSelected(position: Int) = updateTabSelection(position)

override fun onPageScrollStateChanged(state: Int) = Unit

internal inner class InnerPagerAdapter(fm: FragmentManager, fragments: ArrayList<Fragment>, private val titles: Array<String>) : FragmentPagerAdapter(fm) {
private var fragments = ArrayList<Fragment>()

init {
this.fragments = fragments
}

override fun getCount(): Int = fragments.size

override fun getPageTitle(position: Int): CharSequence = titles[position]

override fun getItem(position: Int): Fragment = fragments[position]

override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) =// 覆写destroyItem并且空实现,这样每个Fragment中的视图就不会被销毁
// super.destroyItem(container, position, object);
Unit

override fun getItemPosition(`object`: Any?): Int = PagerAdapter.POSITION_NONE
}

interface OnTagSelectListener {
fun onTagSelected(position: Int, tag: String)
}

fun addOnTagSelectListener(listener: OnTagSelectListener) {
selectListener = listener
}

private fun dp2px(context: Context, dp: Float): Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}

private fun sp2px(context: Context, sp: Float): Int {
val scale = context.resources.displayMetrics.scaledDensity
return (sp * scale + 0.5f).toInt()
}
}