全能型自定义tabLayout(1_尊重原著)

从这段中可以得出,我们的文本内容,最终传给了 参数 textView : textView.setText(text);

追踪**updateTextAndIcon()**的调用位置,看看这个textView是什么。

经过4处调用位置的检查,

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-bx380UCL-1715850254864) .assets/image-20200320174011680-1585550589010.png)

发现它是:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-AdsOFwBu-1715850254877) .assets/image-20200320174106769-1585550590865.png)

这两个属性之一。TabView的两个TextView类型的成员变量。

那么只需要关心这两个TextView是如何添加到TabView中去的。最后发现 customTextView没有被addView,而唯一一处addView(textView)的代码如下:

private void inflateAndAddDefaultTextView() {
ViewGroup textViewParent = this;
if (BadgeUtils.USE_COMPAT_PARENT) {
textViewParent = createPreApi18BadgeAnchorRoot();
addView(textViewParent);
}
this.textView =
(TextView)
LayoutInflater.from(getContext())
.inflate(R.layout.design_layout_tab_text, textViewParent, false);
textViewParent.addView(textView);// 在这里添加的
}

所以,可以断定,我们之前设置的 title0 这个文本被设置到 了 内部类TabView(一个线性布局)的TextView成员中。

所以 title0 这个文本 的所在对象,从小到大,依次是,

原生TextView -> 内部类TabView -> 内部类Tab -> 原生TabLayout

基于这样的认知,要探究一下是不是存在文字被重新绘制的可能性。

要想对TextView根据需求重新绘制,那么除非可以像ViewPager一样,把View以及当前Position反馈到最外层。Position暂且不管,先看 最终的TextView.

经过一番搏斗,发现并没有这样的接口。。。所以没办法了。TabView 在TabLayout中是如何 添加 进去的 的探索结果表明,谷歌并没有给机会让我们 定制文本部分的内容特效。所以,放弃吧。

然后从头看起,如果以 Indicator在TabLayout中是如何 绘制 进去的 为准来进行探索。

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-mYfueIiK-1715850254878) .assets/image-20200321201914158-1585550594385.png)

这里有两个方法,configureTab 方法,只是对tab对象进行了 保存。看addTabView方法。

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-Zs6PJweL-1715850254880) .assets/image-20200321202058037-1585550596434.png)

这里的tab.view 是 TabView对象,它最终添加到了 slidingTabIndicator 中去。而 slidingTabIndicator 它则是一个 内部类,同样是线性布局,方向为横向,它把TabView对象添加进去之后,多个TabView就会横向排列。而底下那一个横向的indicator,则是由 画笔 selectedIndicatorPaint 绘制而成。根据如下:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-LTTs667R-1715850254881) .assets/image-20200321203148966-1585550597890.png)

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-Zajn1It8-1715850254882) .assets/image-20200321203307642-1585550599417.png)

得出最终结论:TabLayout的设计布局如下图:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%85%A8%E8%83%BD%E5%9E%8B%E8%87%AA%E5%AE%9A%E4%B9%89tabLayout(%E5%85%A8%E6%96%87&pos_id=img-dxv3kIHo-1715850254883) .assets/image-20200321204132776-1585550603705.png)

最后我探索了一下,indicator 横条,谷歌是不是有提供对外接口来编辑特效。倒是 内部类 SlidingTabIndicator 有一个ValueAnimator indicatorAnimator 在控制 横条滑动的位置动画,使用的是 FastOutSlowInInterpolator 插值器。但是对我们自定义特效没啥用。

最后结论,放弃治疗了。在TabLayout上,谷歌确实不给机会。


开发思路

谷歌工程师设计的控件是针对全世界的开发者和使用者,肯定会考虑周全,支持很多自定义属性,细节细致入微,所以代码看上起会显得非常复杂,难以读懂,而且这么多英文注释,你懂的,反正我看他们的注释一边看一边猜。

然而我们的UI姐姐有自己的要求,所以如果我们可以做自己的UI控件,就可以摆脱谷歌源码的控制,随心所欲地控制TabLayout的视觉效果。

今天本文的最终目的:

是开发一个 绿色版的 GreenTabLayout,去掉谷歌原本一些繁杂的设定,增添开发常用的自定义属性,并且开放 自定义效果的接口,让其他开发者可以在不改动我原本代码的前提下,编辑自己的动画特效。

上面TabLayout UI层级图,展示了谷歌工程师的设计思路,此思路没有问题,我们可以参照它。

但是一步达成最终效果不太可能,我们分阶段来达成效果:

  • 尊重原著

GreenTabLayout 必须与原TabLayout相差不大,要有文字title标题,要有indicator横条

  • 联动滑动

自定义TabLayout必须能够和ViewPager一样,产生同样的联动滑动效果,包括横条的滑动和 标题部分的滑动

  • 特效解耦

自定义TabLayout 把 标题栏的View,indicator横条View,对外提供方便的动画特效定制接口,符合开闭法则.


开始搬砖

确定了基本思路,接下来就要脚踏实地了。在Kotlin语言如此之香的潮流下,我也追求一波时尚,开发将采用Kotlin编码,最大程度节省代码量,使用kotlin**“域”**的概念隔离程序逻辑,尽可能使源码可读性提高。

一. 尊重原著

要实现与原生TabLayout一样的效果,可以抄谷歌的作业, 原本的UI层级,照搬即可。

下载源码之后,git checkout 4ed2 运行看效果

从外到内有三层:

最外层

它的最外层是一个横向可滚动的 HorizontalScrollView 的子类,同时它提供addTabView方法 供外界添加item

/**

  • 最外层
    */
    class HankTabLayout : HorizontalScrollView {
    constructor(ctx: Context) : super(ctx) {
    init()
    }

constructor(ctx: Context, attributes: AttributeSet) : super(ctx, attributes) {
init()
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}

private lateinit var indicatorLayout: IndicatorLayout

private fun init() {
indicatorLayout = IndicatorLayout(context)
val layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT)
addView(indicatorLayout, layoutParams)

overScrollMode = View.OVER_SCROLL_NEVER
isHorizontalScrollBarEnabled = false
}

fun addTabView(text: String) {
indicatorLayout.addTabView(text)
}

}

中间层

中间层,是一个横向线性布局,宽度自适应,根据内容而定,提供addTabView方法,用来添加 TabView到自身,同时 绘制 indicator横条, 横条与当前选中的tabView等长并处于最下方.

绘制横条可能有多种方式,这里借鉴了谷歌的思路,使用Drawable.draw(canvas) ,好处就是,可以指定drawable图片,使用图片内容绘制在canvas上。后续会有体现。

/**

  • 中间层 可滚动的
    */
    class IndicatorLayout : LinearLayout {
    constructor(ctx: Context) : super(ctx) {
    init()
    }

private fun init() {
setWillNotDraw(false) // 如果不这么做,它自身的draw方法就不会调用
}

var indicatorLeft = 0
var indicatorRight = 0

/**

  • 作为一个viewGroup,有可能它不会执行自身的draw方法,这里有一个值去控制,好像是 setWillNotDraw
    */
    override fun draw(canvas: Canvas?) {
    val indicatorHeight = dpToPx(context, 4f)// 指示器高度
    // 现在貌似应该去画indicator了
    // 要绘制,首先要确定范围,左上右下
    var top = height - indicatorHeight
    var bottom = height
    Log.d(“drawTag”, “$indicatorLeft $indicatorRight $top $bottom”)
    // 现在只考虑在底下的情况
    var selectedIndicator: Drawable = GradientDrawable()
    selectedIndicator.setBounds(indicatorLeft, top, indicatorRight, bottom)
    DrawableCompat.setTint(selectedIndicator, resources.getColor(R.color.c2))
    selectedIndicator.draw(canvas!!)
    super.draw(canvas)
    }

fun updateIndicatorPosition(tabView: TabView, left: Int, right: Int) {
indicatorLeft = left
indicatorRight = right
postInvalidate()// 刷新自身,调用draw
// 把其他的都设置成未选中状态
for (i in 0 until childCount) {
val current = getChildAt(i) as TabView
if (current.hashCode() == tabView.hashCode()) {// 如果是当前被点击的这个,那么就不需要管
current.setSelectedStatus(true) // 选中状态
} else {// 如果不是
current.setSelectedStatus(false)// 非选中状态
}
}
}

/**

  • 但是onDraw一定会执行
    */
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    }

// 对外提供方法,添加TabView
fun addTabView(text: String) {
val tabView = TabView(context, this)
val param = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
param.setMargins(dpToPx(context, 10f))

val textView = TextView(context)
textView.setBackgroundDrawable(resources.getDrawable(R.drawable.my_tablayout_textview_bg))
textView.text = text
textView.gravity = Gravity.CENTER
textView.setPadding(dpToPx(context, 15f))
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
textView.setTextColor(resources.getColor(TabView.unselectedTextColor))
tabView.setTextView(textView)

addView(tabView, param)
postInvalidate()

if (childCount == 1) {
val tabView0 = getChildAt(0) as TabView
tabView0.performClick()
}
}
}

最里层

说是最里层,其实这里分为两小层,一个是TabView(继承线性布局),一个是TextView(用来展示 title),

提供点击事件,和状态切换的方法setSelectedStatus(boolean)

/**

  • 最里层TabView
    */
    class TabView : LinearLayout {
    private lateinit var titleTextView: TextView
    private var selectedStatue: Boolean = false
    private var parent: IndicatorLayout

companion object {
const val selectedTextColor = R.color.cf
const val unselectedTextColor = R.color.c1
}

constructor(ctx: Context, parent: IndicatorLayout) : super(ctx) {
init()
this.parent = parent
}

fun setTextView(textView: TextView) {
titleTextView = textView
removeAllViews()
val param = LayoutParams(WRAP_CONTENT, MATCH_PARENT)
addView(titleTextView, param)

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
887)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值