全能型自定义tabLayout(3_特效解耦) ,写给1-3年安卓程序员的几点建议

本文介绍了一种为Android应用实现全能型自定义TabLayout的方法,包括三阶效果的实现和平滑字体过渡的处理。通过开放GreenTabView和Indicator接口,实现了动画效果的无耦合和高度定制,允许开发者自由控制tab文字和指示器的动画行为。文中详细解析了关键代码,提供了解决字体平滑变化和横条拉伸回缩问题的思路。
摘要由CSDN通过智能技术生成

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: positionOffsetisSelectedisSelected "
)
// 来,先判定滑动的方向,因为方向会决定从哪个角度
mTranslate = if (mPositionOffset < positionOffset) {// 手指向左
if (isSelecte
d) {// 如果当前是选中状态,那么 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接管
    */
    接口,那么所能达成的效果,就不仅仅是上图中所示, 利用 handlerPositionOffset的几个参数,发挥想象力(或者UI姐姐发挥想象力),想要做出任何你希望的效果,只是时间问题。
Indicator接口

同样,针对Indicator横条的绘制,你也可以完全自定义,使用自己的实现方式,强制接管 原代码中的绘制逻辑

接口在 GreenTabLayout.kt 中,入口方法为:

/**

  • 注意,使用了此方法,传入了非空的CustomDrawHandler实现类对象,
  • 原本indicator的所有属性都会失效,因为indicator的绘制工作,全部由CustomDrawHandler接管
    */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值