parent.updateIndicatorPosition(this, left, right)
}
}
private fun init() {
}
fun setSelectedStatus(selected: Boolean) {
selectedStatue = selected
if (selected) {
titleTextView.setTextColor(resources.getColor(R.color.cf))
} else {
titleTextView.setTextColor(resources.getColor(R.color.c1))
}
}
}
初阶效果
做完这些,基本就呈现出下图的状态:
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](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-PW7BK0ad-1712376392344) .assets/尊重原著.gif)
上一半是原生TabLayout,用来对比,下一半是刚刚完成的效果。但是和上面的原生TabLayout比起来. 第一步完成。从开始写代码,到完成这个效果,一直参考的 谷歌的代码。
二. 联动滑动
下载源码之后,git checkout a132 运行看效果
布局层级已经完成,现在需要联动Viewpager的滑动参数,让GreenTabLayout 跟随ViewPager一起滑动。
注册监听
要实现联动,首先要知道,谷歌源码中,TabLayout是如何与ViewPager发生联动的,它们的联结点在哪里,请看代码:
tabLayout.setupWithViewPager(viewpager)
平时我们用 原生TabLayout,两者唯一发生交集的地方就是这里,进入看源码:
显然他们的交集可能是某个回调监听,顺着这个线索,最终确定,上面的 pageChangeListener
就是 联动滑动的交界点,这里把监听器传给ViewPager,ViewPager则可以把自己的滑动参数传递给TabLayout,TabLayout则做出相应的行为。
监听器的源码为:
private TabLayoutOnPageChangeListener pageChangeListener;
public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
…
}
@Override
public void onPageSelected(final int position) {
…
}
@Override
public void onPageScrollStateChanged(final int state) {
…
}
}
了解到这里,我们可以给 GreenTabLayuot 直接加上 这个接口实现
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
…
}
@Override
public void onPageSelected(final int position) {
…
}
@Override
public void onPageScrollStateChanged(final int state) {
…
}
}
然后提供一个 相同的 setupWithViewPager(viewpager)
方法, 在内部,给ViewPager绑定监听,同时根据 viewPager的adapter内的 page数目,决定TabView的数目和每一个的标题。
fun setupWithViewPager(viewPager: ViewPager) {
this.mViewPager = viewPager
viewPager.addOnPageChangeListener(this)// 注册监听
val adapter = viewPager.adapter ?: return
val count = adapter!!.count // 栏目数量
for (i in 0 until count) {
val pageTitle = adapter.getPageTitle(i)
addTabView(pageTitle.toString())// 根据adapter的item数目,决定TabView的数目和每一个标题
}
}
参数分析
注册监听之后,Viewpager可以把自己的滑动参数的变化告知TabLayout,但是TabLayout如何去处理这个参数变化,还需要从参数的规律上去着手。重点分析 监听的 onPageScrolled
方法, 重点中的重点,则是前两个参数:position(当前page的index) 和 positionOffset(当前page的偏移百分比,小数表示的)
为了研究规律,我们用上面刚刚完成的代码把GreenTabLayout和ViewPager连结上,然后打印日志onPageScrolled
:
基本得出一个结论:
position为0的,为当前选中的这个page,当慢慢从当前page划走时,它的positionOffset会从0慢慢变成1
并且,如果手指分方向滑动试验,可知:
当手指向左,positionOffset会递增,从0到极限值1,到达极限之后归0,同时 position递加1
反之,手指向右,positionOffset会递减,从1 递减到0,从递减的那一刻开始,position递减1
基于上面的规律,我们可以调试出 indicator横条动画的代码:
…
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
scrollTabLayout(position, positionOffset)
}
private fun scrollTabLayout(position: Int, positionOffset: Float) {
// 如果手指向左划,indicator横条应该从当前位置,滑动到 下一个子view的位置上去,position应该+1
// 如果手指向右滑动,position立即减1,indicator横条应该从当前位置向左滑动
val currentTabView = indicatorLayout.getChildAt(position) as GreenTabView
val currentLeft = currentTabView.left
val currentRight = currentTabView.right
val nextTabView = indicatorLayout.getChildAt(position + 1)
if (nextTabView != null) {
val nextLeft = nextTabView.left
val nextRight = nextTabView.right
Log.d(“scrollTabLayout”,“当前index: p o s i t i o n l e f t : {position} left: positionleft:{currentLeft} right: c u r r e n t R i g h t " + " 目标 i n d e x : {currentRight} " +" 目标index: currentRight"+"目标index:{position + 1} left: n e x t L e f t r i g h t : {nextLeft} right: nextLeftright:{nextRight} positionOffset:${positionOffset}” )
val leftDiff = nextLeft - currentLeft
val rightDiff = nextRight - currentRight
indicatorLayout.updateIndicatorPosition(
currentLeft + (leftDiff * positionOffset).toInt(),
currentRight + (rightDiff * positionOffset).toInt()
)
}
}
为什么这样就能正确区分滑动的方向?把日志打印出来一看就明白:
这是手指向左划一格:
-
观察positionOffset的变化,从0 变为1,然后归零。
-
而看横条的当前 left = 26,right=170, 以及 目标left=222,right=380 ,随着positionOffset的递增,横条会慢慢向右。
-
而到达最后,positionOffset归零了,当前left 也变成了 目标的left = 222,right=380.
横条向右平移完成。
而手指向右划一格,日志如下:
-
position先直接减1,positionOffset则从1慢慢变成0.
-
横条从 left=26 right=170 的起始位置,向 目标 left=222,righ=380 移动,但是由于positionOffset是递减的,所以,横条的移动方向反而是 向左。一直到positionOffset为0,到达 left=26 right=170.
横条向左平移也完成。
整体平移
横条虽然可以跟着viewPager的滑动而滑动,但是如果TabView已经排满了当前屏幕,横条到达了当前屏幕最右侧,viewPager上右侧还有内容还可以让手指向左滑动。此时,就必须滚动最外层布局,来让TabView显示出来。
通过观察原生TabLayout,它会尽量让 当前选中的tabView位于 控件的横向居中的位置。而随着 ViewPager的当前page的变化,最外层GreenTabLayout也要发生横向滚动。
所以我选择在 回调函数onPageSelected中执行滚动:
class GreenTabLayout: HorizontalScrollView, ViewPager.OnPageChangeListener {
…
override fun onPageSelected(position: Int) {
val tabView = indicatorLayout.getChildAt(position) as GreenTabView
if (tabView != null) {
indicatorLayout.updateIndicatorPositionByAnimator(tabView, tabView.left, tabView.right)
}
}
}
执行滚动的思路为:
- 确定 当前选中的tabView的 矩形范围
tabView.getHitRect(tabViewBounds)
- 确定 确定最外层GreenTbaLayout的矩形范围
getHitRect(parentBounds)
- 计算两个矩形的x轴的中点,然后计算出两个中点的差值,差值就是需要滚动的距离
- 使用属性动画进行平滑滚动
/**
- 用动画平滑更新indicator的位置
- @param tabView 当前这个子view
*/
fun updateIndicatorPositionByAnimator(
tabView: GreenTabView,
targetLeft: Int,
targetRight: Int) {
…
// 处理最外层布局( HankTabLayout )的滑动
parent.run {
tabView.getHitRect(tabViewBounds) //确定 当前选中的tabView的 矩形范围
getHitRect(parentBounds) // 确定最外层GreenTbaLayout的矩形范围
val scrolledX = scrollX // 已经滑动过的距离
val tabViewRealLeft = tabViewBounds.left - scrolledX // 真正的left, 要算上scrolledX
val tabViewRealRight = tabViewBounds.right - scrolledX // 真正的right, 要算上scrolledX
val tabViewCenterX = (tabViewRealLeft + tabViewRealRight) / 2
val parentCenterX = (parentBounds.left + parentBounds.right) / 2
val needToScrollX = -parentCenterX + tabViewCenterX // 差值就是需要滚动的距离
startScrollAnimator(this, scrolledX, scrolledX + needToScrollX)
}
}
/**
- 用动画效果平滑滚动过去
*/
private fun startScrollAnimator(tabLayout: GreenTabLayout, from: Int, to: Int) {
if (scrollAnimator != null && scrollAnimator.isRunning) scrollAnimator.cancel()
scrollAnimator.duration = 200
scrollAnimator.interpolator = FastOutSlowInInterpolator()
scrollAnimator.addUpdateListener {
val progress = it.animatedValue as Float
val diff = to - from
val currentDif = (diff * progress).toInt()
tabLayout.scrollTo(from + currentDif, 0)
}
scrollAnimator.start()
}
二阶效果
完成到这里,就能达成下图中的效果:
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](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-56Cc5Cl4-1712376392344) .assets/联动滑动.gif)
上半部分为原生TabLayout效果,下把那部分为 刚刚完成的效果,几乎没有差别了。
当然,我们这是把TabLayout本体话,完成这些,仅仅用了kotlin 300多行代码。可见Kotlin在省代码方面,确实是一绝,比java简洁很多。
三.特效解耦
这一阶段主要做2件事:
- 支持开发中的常用的UI设计要求,这个可以做成自定义属性
- 开放无耦合接口,使得开发者可以使用该接口编辑 indicator横条 / TabView文本 的滑动特效,而不用改动GreenTabLayout的内部实现
第一点,都是一些基础性的改造,就不赘述了,关于自定义属性的添加和使用,都是死框架,没什么好说的,下面,总结一下 我所支持的所有属性:
盘点自定义属性
TabView标题栏部分:
属性名 | 意义 | 取值类型 |
---|---|---|
tabViewTextSize | 标题字体大小 | dimension|reference |
tabViewTextSizeSelected | 选中后的标题字体大小 | dimension|reference |
tabViewTextColor | 标题字体颜色 | color|reference |
tabViewTextColorSelected | 选中后的标题字体颜色 | color|reference |
tabViewBackgroundColor | 标题区域背景色 | color|reference |
tabViewTextPaddingLeft | 标题区内边距左 | dimension|reference |
tabViewTextPaddingRight | 标题区内边距右 | dimension|reference |
tabViewTextPaddingTop | 标题区内边距上 | dimension|reference |
tabViewTextPaddingBottom | 标题区内边距下 | dimension|reference |
tabViewDynamicSizeWhenScrolling | 是否允许滚动时的字体大小渐变 | boolean |
Indicator横条部分:
属性名 | 意义 | 取值类型 |
---|---|---|
indicatorColor | 横条颜色 | color|reference |
indicatorLocationGravity | 横条位置 | 枚举:TOP 放在顶部 / BOTTOM 放在底部 |
indicatorMargin | 横条间距,当indicatorLocationGravity为TOP时表示距离顶端的距离,BOTTOM时表示距离底部的距离 | dimension|reference |
indicatorWidthMode | 横条宽度模式 | 枚举:RELATIVE_TAB_VIEW 取TabView宽度的倍数 / EXACT 取精确值 |
indicatorWidthPercentages | 横条宽度百分比,当indicatorWidthMode 为 RELATIVE_TAB_VIEW时才会生效,表示横条宽度占TabView宽度的百分比 | float(大于0) |
indicatorExactWidth | 横条宽度精确值,当indicatorWidthMode 为 EXACT时才会生效,表示横条的精确宽度 | dimension|reference |
indicatorHeight | 横条高度 | dimension|reference |
indicatorAlignMode | 横条对其模式 | 枚举: LEFT / CENTER / RIGHT |
indicatorDrawable | 横条drawable,可以指定横条的内容为图片 | reference |
indicatorElastic | 是否开启滚动时横条的弹性效果 | boolean |
indicatorElasticBaseMultiple | 当indicatorElastic开启时生效,表示弹性倍数,数字越大,弹性越明显 | float |
其中大部分属性的处理都是基于非常基础的View控件知识和简单的数学计算,只有几点需要讲解说明:
- tabViewDynamicSizeWhenScrolling 是否允许滚动时的字体大小渐变
- indicatorElastic 是否开启滚动时横条的弹性效果
这两点,都与 viewPager滑动时的参数变化有关系,所以处理这两个特性,需要结合参数变化规律
较复杂属性处理
tabViewDynamicSizeWhenScrolling
viewPager滚动时,标题的字体大小会发生渐变:
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener {
…
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int){
Log.d(“positionOffset”, “$positionOffset”)
scrollTabLayout(position, positionOffset)
}
fun scrollTabLayout(position: Int, positionOffset: Float) {
val currentTabView = indicatorLayout.getChildAt(position) as GreenTabView
val currentLeft = currentTabView.left
val currentRight = currentTabView.right
val nextTabView = indicatorLayout.getChildAt(position + 1) // 目标TabView
if (nextTabView != null) {
val nextGreenTabView = nextTabView as GreenTabView
dealAttrTabViewDynamicSizeWhenScrolling(// 关键代码
positionOffset,
currentTabView,
nextGreenTabView
)
…
}
}
/**
- 处理属性 tabViewDynamicSizeWhenScrolling
*/
private fun dealAttrTabViewDynamicSizeWhenScrolling(
positionOffset: Float,
currentTabView: GreenTabView,
nextTabView: GreenTabView
) {
if (tabViewAttrs.tabViewDynamicSizeWhenScrolling) {
if (positionOffset != 0f) {
// 在这里,让当前字体变小,next的字体变大
val diffSize =
tabViewAttrs.tabViewTextSizeSelected - tabViewAttrs.tabViewTextSize
when (mScrollState) {
ViewPager.SCROLL_STATE_DRAGGING -> {
currentTabViewTextSizeRealtime =
tabViewAttrs.tabViewTextSizeSelected - diffSize * positionOffset
currentTabView.titleTextView.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
currentTabViewTextSizeRealtime
)
nextTabViewTextSizeRealtime =
tabViewAttrs.tabViewTextSize + diffSize * positionOffset
nextTabView.titleTextView.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
nextTabViewTextSizeRealtime
)
settingFlag = false
}
ViewPager.SCROLL_STATE_SETTLING -> {
// OK,定位到问题,在 mScrollState 为setting状态时,positionOffset的变化没有 dragging时那么细致
// 只要不处理 SETTING下的字体大小变化,也可以达成效果
if (!settingFlag)
indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView)
settingFlag = true
}
}
}
}
}
}
处理思路依旧是围绕 onPageScrolled 的参数变化,核心方法为:dealAttrTabViewDynamicSizeWhenScrolling(…), 让当前tabView的文本渐渐变小,而nextTabView的文本逐渐变大。这里如果有疑问可以参照上文的 参数分析小章节。
但是,有一个坑,就是当拖拽停止的时候,viewpager会有一个自动的回弹动作,如果这里没处理好,就会出现,字体大小突变的情况,和我要的平滑动画过渡不相符,所以,这里我做了一个特殊处理,当拖拽停止,也就是手指松开的时候,抓准 ViewPager的 SCROLL_STATE_SETTLING 状态刚刚进入的时机,使用属性动画平滑改变字体,核心代码就是上文代码块中的:indicatorLayout.resetTabViewsStatueByAnimator(indicatorLayout[mCurrentPosition] as GreenTabView)
这句话可以让 tabView的文本字体平滑地从 当前值(不确定,因为dragging状态是用户人为控制),变为 目标值(这是确定值,要么是 正常状态下的字体大小,要么是选中状态下的字体大小),由此完美解决字体平滑变化的问题。
indicatorElastic
滚动时,横条会拉伸和回缩,也是跟随onPageScrolled
的参数变化而变化
关键代码在 SlidingIndicatorLayout.kt
中的 draw方法:
override fun draw(canvas:Canvas?){
…
val baseMultiple = parent.indicatorAttrs.indicatorElasticBaseMultiple // 基础倍数,决定拉伸
val indicatorCriticalValue = 1 + baseMultiple
val ratio =
if (parent.indicatorAttrs.indicatorElastic) {
when {
positionOffset >= 0 && positionOffset < 0.5 -> {
1 + positionOffset * baseMultiple // 拉伸长度
}
else -> {// 如果到了下半段,当offset越过中值之后ratio的值
indicatorCriticalValue - positionOffset * baseMultiple
}
}
} else 1f
// 可以开始绘制
selectedIndicator.run {
setBounds(
((centerX - indicatorWidth * ratio / 2).toInt()),
top,
((centerX + indicatorWidth * ratio / 2).toInt()),
bottom
)// 规定它的边界
draw(canvas!!)// 然后绘制到画布上
}
…
}
这一段提出来特别说明,因为它代表了一种解题思路,我需要的效果是:
viewPager滚动1格,我需要它在滚动一半的时候,横条拉伸到最长,从一半滚完的时候,横条回缩到应该的宽度
但是,viewPager滚1格,positionOffset的变化是从0 到1(手指向右),或者是从1到0(手指向左),我需要把positionOffset在到达0.5的时候当作一个临界时间点,计算出 这个临界时间点上,indicator横条应该的长度。
关键在于:在临界点0.5上,前半段的0->0.5的最终值,必须等于 后半段 0.5->1 的 开始值,
由于我是按照倍数来拉伸,所以,原始倍率是1。我还想用参数控制拉伸的程度,所以设计一个变量 baseMultiple
(拉伸倍数,数值越大,拉伸越明显)
列出公式:
-
前半段的ratio最终值 = 1(
原始倍率
)+ 0.5 *baseMultiple
-
后半段的ratio值 =
indicatorCriticalValue
(临界值
) - 0.5 *baseMultiple
-
前半段的ratio最终值 = 后半段的ratio值
计算得出,indicatorCriticalValue
(临界值) = 1 (原始倍率
)+ baseMultiple
于是就写出了上面的代码。
三阶效果
说了这么多,不如亲眼看一眼效果更佳实在,以上各项属性,下面的动态图基本都有体现, 具体效果可以按需定制,基本可以满足UI姐姐的各种骚操作要求,如果还不行,可以拿我的代码自行修改,我的代码注释应该比谷歌大佬要亲民很多。,欢迎fork,star…
开放无耦合特效接口
为什么生出这种想法?这个是源自:ViewPager的无耦合动画接口。
Viewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
viewPager的setPageTransformer,可以传入一个 PageTransformer(接口)的实现类,从而控制ViewPager滑动时的动画,开发者可以自由定制效果,而不用关心ViewPager的内部实现。符合程序设计的开闭法则,让控件开发者和 控件使用者都省心省力。
GreenTabView接口
我在Demo中,提供了 GreenTabLayout的setupWithViewPager泛型方法,使用者可以传入 GreenTextView的子类.两段关键代码如下:
open class GreenTextView : AppCompatTextView {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
/**
- 可重写,接收来自viewpager的position参数,做出随心所欲的textView特效
- @param isSelected 是不是当前选中的TabView
- @param positionOffset 偏移值 0<= positionOffset <=1
*/
open fun handlerPositionOffset(positionOffset: Float, isSelected: Boolean) {}
/**
- 如果发生了滑动过程中特效残留的情况,可以重写此方法用来清除特效
*/
open fun removeShader(oldPosition: Int, newOldPosition: Int) {}
/**
- 添加特效
*/
open fun addShader(oldPosition: Int, newOldPosition: Int) {}
/**
- 通知,viewPager 即将进入setting状态
- @param positionOffset 当前offset
- @param isSelected 是否是被选择的TabView
- @param direction 滑动方向,大于0 表示向右回弹,小于0 表示向左回弹
*/
open fun onSetting(positionOffset: Float, isSelected: Boolean, direction: Int) {}
}
class GreenTabLayout : HorizontalScrollView, ViewPager.OnPageChangeListener{
…
fun setupWithViewPager(viewPager: ViewPager, t: T?) {
…
}
}
你可以按照下面的模板使用这个接口:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyPagerAdapter(supportFragmentManager)
hankViewpager.adapter = adapter
hankViewpager.offscreenPageLimit = 3
hankViewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
//关键代码
hankTabLayout.setupWithViewPager(hankViewpager, GradientTextView(this))
//*****************************************
hankTabLayout2.setupWithViewPager(hankViewpager)
}
…
}
GradientTextView是GreenTabView的一个子类,它的源码是:
/**
- 提供颜色渐变的TextView
*/
class GradientTextView : GreenTextView {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context) : super(context)
private var mLinearGradient: LinearGradient? = null
private var mGradientMatrix: Matrix? = null
private lateinit var mPaint: Paint
private var mViewWidth = 0f
private var mTranslate = 0f
private val mAnimating = true
private val fontColor = Color.BLACK
private val shaderColor = Color.YELLOW
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (mViewWidth == 0f) {
mViewWidth = measuredWidth.toFloat()
if (mViewWidth > 0) {
mPaint = paint
mLinearGradient = LinearGradient(
0f,// 初始状态,是隐藏在x轴负向,一个view宽的距离
0f,
mViewWidth,
0f,
intArrayOf(fontColor, shaderColor, shaderColor, fontColor),
floatArrayOf(0f, 0.1f, 0.9f, 1f),
Shader.TileMode.CLAMP
)
mPaint.shader = mLinearGradient
mGradientMatrix = Matrix()
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mAnimating && mGradientMatrix != null) {
mGradientMatrix!!.setTranslate(mTranslate, 0f)
mLinearGradient!!.setLocalMatrix(mGradientMatrix)
}
}
private inline fun dealSwap(positionOffset: Float, isSelected: Boolean) {
// 如果不是初始值,那说明已经赋值过,那么用 参数positionOffset 和 它对比,来得出滑动的方向
Log.d(
“setMatrixTranslate”,
" positionOffset:
p
o
s
i
t
i
o
n
O
f
f
s
e
t
i
s
S
e
l
e
c
t
e
d
:
positionOffset isSelected:
positionOffsetisSelected:isSelected "
)
// 来,先判定滑动的方向,因为方向会决定从哪个角度
mTranslate = if (mPositionOffset < positionOffset) {// 手指向左
if (isSelected) {// 如果当前是选中状态,那么 offset会从0到1 会如何变化?
mViewWidth * positionOffset // OK,没问题。
} else {
-mViewWidth * (1 - positionOffset)
}
} else {// 手指向右
if (isSelected) {// 如果当前是选中状态,那么 offset会从0到1 会如何变化?
-mViewWidth * (1 - positionOffset) // OK,没问题。
} else {
mViewWidth * positionOffset
}
}
postInvalidate()
}
/**
- 由外部参数控制shader的位置
- @param positionOffset 只会从0到1变化
- @param isSelected 是否选中
*/
override fun handlerPositionOffset(positionOffset: Float, isSelected: Boolean) {
if (mPositionOffset == -1f) {// 如果你是初始值
mPositionOffset = positionOffset // 那就先赋值
} else {
dealSwap(positionOffset, isSelected)
}
}
override fun removeShader(direction: Int) {
Log.d(“removeShaderTag”, “要根据它当前的mTranslate位置决定从哪个方向消失 mTranslate:$mTranslate”)
mTranslate = mViewWidth
postInvalidate()
}
override fun addShader(direction: Int) {
// 属性动画实现shader平滑移动
val from =
if (direction < 0) {
-mViewWidth
} else {
mViewWidth
}
startAnimator(from, 0f)
}
override fun onSetting(positionOffset: Float, isSelected: Boolean, direction: Int) {
Log.d(
“onSettingTag”,
“isSelected:
i
s
S
e
l
e
c
t
e
d
p
o
s
i
t
i
o
n
O
f
f
s
e
t
:
isSelected positionOffset:
isSelectedpositionOffset:positionOffset direction:$direction”
)
mPositionOffset = -1f
val targetTranslate = if (isSelected) {
0f
} else {
if (direction > 0f) {// 向右回弹
mViewWidth
} else {
Log.d(“onSettingTag2”, “难道这里还要分情况么?mTranslate:
m
T
r
a
n
s
l
a
t
e
m
V
i
e
w
W
i
d
t
h
:
mTranslate mViewWidth:
mTranslatemViewWidth:mViewWidth”)
if (mTranslate == mViewWidth || mTranslate == -mViewWidth) {
mTranslate // 如果已经到达了最右边,那就保持你这个样子就行了, 可是你是怎么到最右边的?
} else
-mViewWidth
}
}
val thisTranslate = mTranslate
startAnimator(thisTranslate, targetTranslate)
}
private fun startAnimator(from: Float, targetTranslate: Float) {
if (animator != null) animator?.cancel()
// 属性动画实现shader平滑移动
animator = ValueAnimator.ofFloat(from, targetTranslate)
animator?.run {
duration = animatorDuration
addUpdateListener {
mTranslate = it.animatedValue as Float
postInvalidate()
}
start()
}
}
private var mPositionOffset: Float = -1f
private val animatorDuration = 200L
private var animator: ValueAnimator? = null
}
运行效果:请注意看下图的上面半部分,下半部分只是没有加特效的对比。理论上,利用现在的参数,可以定制出想要的任何效果,下图只是我的一些效果测试。
注意,使用了Shader特效之后,原本的 titleTextView字体颜色可能会失效,这是由shader机制决定的,但是依然可以用shader控制字体的颜色,运行Demo,阅读源码,很快就能得出答案。
既然这是一个开放接口,那么所能达成的效果,就不仅仅是上图中所示, 利用 handlerPositionOffset的几个参数,发挥想象力(或者UI姐姐发挥想象力),想要做出任何你希望的效果,只是时间问题。
Indicator接口
同样,针对Indicator横条的绘制,你也可以完全自定义,使用自己的实现方式,强制接管 原代码中的绘制逻辑。
接口在 GreenTabLayout.kt 中,入口方法为:
/**
- 注意,使用了此方法,传入了非空的CustomDrawHandler实现类对象,
- 原本indicator的所有属性都会失效,因为indicator的绘制工作,全部由CustomDrawHandler接管
*/
fun setIndicatorDrawHandler(customDrawHandler: SlidingIndicatorLayout.CustomDrawHandler?) {
indicatorLayout.customDrawHandler = customDrawHandler
}
接口为:SlidingIndicatorLayout.kt
类中的 CustomDrawHandler
,提供一个draw方法,方法内提供2个关键参数,第一个是 **SlidingIndicatorLayout **对象,第二个是,画布canvas对象, 前者可以让我们拿到任何想要拿的参数,后者,让我们可以动用想象力,把想象的特效,绘制在画布上。
interface CustomDrawHandler {
fun draw(indicatorLayout: SlidingIndicatorLayout, canvas: Canvas?)
}
var customDrawHandler: CustomDrawHandler? = null
使用方法:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = MyPagerAdapter(supportFragmentManager)
hankViewpager.adapter = adapter
hankViewpager.offscreenPageLimit = 3
hankViewpager.setPageTransformer(true, MyPageTransformer(this, adapter.count))
hankTabLayout.setupWithViewPager(hankViewpager, GradientTextView(this))
hankTabLayout.setIndicatorDrawHandler(CustomDrawHandlerImpl(this))
hankTabLayout2.setupWithViewPager(hankViewpager)
}
class CustomDrawHandlerImpl : SlidingIndicatorLayout.CustomDrawHandler {
val context: Context
constructor(context_: Context) {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
题外话
我们见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
题外话
我们见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
注意:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-RnIr0dO5-1712376392347)]