思路还是很简单的关键步骤如下
页面搭建这里就不写了,主要讲一下动画效果:
上面的思路知道,这个动画核心是需要一个0-1渐变的值,而这个值正好在滑动viewPager的时候就能获取到:
给ViewPager.addOnPageChangeListener(),然后在onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int)方法中会返回三个参数,这里主要用到前两个
通过打印Log得到值变化的规律如下:
1.第一个页面 -> 第二个页面 position 一直0最后突变1 ; positionOffset 0 渐变 1 突变 0
2.第二个页面 -> 第一个页面 position 一直0 ; positionOffset 1 渐变 0
那么我们就可以通过如下方法获取到当前选中的控件,以及将要选中的控件,并且传入0-1的值:
//页面一到二 最后突变为1时positionOffset = 0.0,此时特殊情况不做处理
if (positionOffset != 0.0f) {
btmBtns[position].alphaNum = 1 - positionOffset
btmBtns[position + 1].alphaNum = positionOffset
} else { //等于0.0说明静止 此时position就是选中的页面
changeBtmChecked(position)
btmBtns.forEachIndexed { index, bottomTabBtn ->
bottomTabBtn.checked = position == index
}
}
0到1的值有了,控件也获取到了,接下来是动画:
图标动画很简单:
//alphaNum 就是上面获取到的0-1的值
mainTabBottomIconG.imageAlpha = (alphaNum * 255).toInt()
mainTabBottomIconB.imageAlpha = ((1 - alphaNum) * 255).toInt()
文字的动画:
我们知道Android里面有个动画是这么写的
//对背景色颜色进行改变,操作的属性为"backgroundColor",此处必须这样写,不能全小写,后面的颜色为在对应颜色间进行渐变
val animator = ObjectAnimator.ofInt(view, "backgroundColor",0x00ff0000, 0x6600ff00)
animator.duration = 3000
animator.setEvaluator(ArgbEvaluator())
}
然后调用属性动画start方法,就可以在3秒内执行view颜色渐变
但是,我们这里需要的渐变要根据一个0到1的值来改变,不要自动执行,自己手动写一个其实还是挺复杂的,而且一不小心出错那么我们动一下脑,既然ObjectAnimator有这个功能那么它的源码里应该有这个方法吧,看这里有这么一句animator.setEvaluator(ArgbEvaluator()),那么通过查看源码ArgbEvaluator源码果然找到了这么一个方法:
/**
* 根据一个0-1的值 改变颜色 从源码ArgbEvaluator类中拷贝
*/
private fun evaluate(fraction: Float, startValue: Int, endValue: Int): Int {
val startA = (startValue shr 24 and 0xff) / 255.0f
var startR = (startValue shr 16 and 0xff) / 255.0f
var startG = (startValue shr 8 and 0xff) / 255.0f
var startB = (startValue and 0xff) / 255.0f
val endA = (endValue shr 24 and 0xff) / 255.0f
var endR = (endValue shr 16 and 0xff) / 255.0f
var endG = (endValue shr 8 and 0xff) / 255.0f
var endB = (endValue and 0xff) / 255.0f
// convert from sRGB to linear
startR = Math.pow(startR.toDouble(), 2.2).toFloat()
startG = Math.pow(startG.toDouble(), 2.2).toFloat()
startB = Math.pow(startB.toDouble(), 2.2).toFloat()
endR = Math.pow(endR.toDouble(), 2.2).toFloat()
endG = Math.pow(endG.toDouble(), 2.2).toFloat()
endB = Math.pow(endB.toDouble(), 2.2).toFloat()
// compute the interpolated color in linear space
var a = startA + fraction * (endA - startA)
var r = startR + fraction * (endR - startR)
var g = startG + fraction * (endG - startG)
var b = startB + fraction * (endB - startB)
// convert back to sRGB in the [0..255] range
a *= 255.0f
r = Math.pow(r.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
g = Math.pow(g.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
b = Math.pow(b.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
return Math.round(a) shl 24 or (Math.round(r) shl 16) or (Math.round(g) shl 8) or Math.round(b)
}
拿过来直接用就完事了,这里我只是改了下返回值类型
自定义自组合控件实现底部图标加文字控件
可能会有难度的上面都分析完了,这里直接上完整代码了:
/**
* @author : GuZhC
* @date : 2019/6/20 10:12
* @description : BottomTabBtn
*/
class BottomTabBtn : LinearLayout {
private var iconChecked: Int = R.mipmap.ic_launcher
private var iconNoChecked: Int = R.mipmap.ic_launcher
private var textBtm: Int = R.string.app_name
internal var alphaNum: Float = 0f
set(value) {
field = value
setBtnAlphaNum(alphaNum)
}
internal var checked: Boolean = false
set(value) {
field = value
alphaNum = if (value) 1f
else 0f
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet?) {
LayoutInflater.from(context).inflate(R.layout.modle_main_layout_botom_btn, this, true)
attrs?.let {
val typedArray = context.obtainStyledAttributes(it, R.styleable.MainBtmTabBtn)
iconChecked = typedArray.getResourceId(R.styleable.MainBtmTabBtn_iconChecked, 0)
iconNoChecked = typedArray.getResourceId(R.styleable.MainBtmTabBtn_iconNoChecked, 0)
textBtm = typedArray.getResourceId(R.styleable.MainBtmTabBtn_textBtm, 0)
//回收资源,这一句必须调用
typedArray.recycle()
}
}
override fun onFinishInflate() {
super.onFinishInflate()
//不能setBackgroundResource
mainTabBottomIconG.setImageResource(iconChecked)
mainTabBottomIconB.setImageResource(iconNoChecked)
mainTabBottomText.text = resources.getString(textBtm)
}
private fun setBtnAlphaNum(alphaNum: Float) {
// L.e("$alphaNum")
mainTabBottomText.setTextColor(evaluate(alphaNum, Color.parseColor("#000000"), Color.parseColor("#0ac668")))
mainTabBottomIconG.imageAlpha = (alphaNum * 255).toInt()
mainTabBottomIconB.imageAlpha = ((1 - alphaNum) * 255).toInt()
}
/**
* 根据一个0-1的值 改变颜色 从源码ArgbEvaluator类中拷贝
*/
private fun evaluate(fraction: Float, startValue: Int, endValue: Int): Int {
val startA = (startValue shr 24 and 0xff) / 255.0f
var startR = (startValue shr 16 and 0xff) / 255.0f
var startG = (startValue shr 8 and 0xff) / 255.0f
var startB = (startValue and 0xff) / 255.0f
val endA = (endValue shr 24 and 0xff) / 255.0f
var endR = (endValue shr 16 and 0xff) / 255.0f
var endG = (endValue shr 8 and 0xff) / 255.0f
var endB = (endValue and 0xff) / 255.0f
// convert from sRGB to linear
startR = Math.pow(startR.toDouble(), 2.2).toFloat()
startG = Math.pow(startG.toDouble(), 2.2).toFloat()
startB = Math.pow(startB.toDouble(), 2.2).toFloat()
endR = Math.pow(endR.toDouble(), 2.2).toFloat()
endG = Math.pow(endG.toDouble(), 2.2).toFloat()
endB = Math.pow(endB.toDouble(), 2.2).toFloat()
// compute the interpolated color in linear space
var a = startA + fraction * (endA - startA)
var r = startR + fraction * (endR - startR)
var g = startG + fraction * (endG - startG)
var b = startB + fraction * (endB - startB)
// convert back to sRGB in the [0..255] range
a *= 255.0f
r = Math.pow(r.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
g = Math.pow(g.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
b = Math.pow(b.toDouble(), 1.0 / 2.2).toFloat() * 255.0f
return Math.round(a) shl 24 or (Math.round(r) shl 16) or (Math.round(g) shl 8) or Math.round(b)
}
}
xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/mainTabBottomIconB"
android:layout_width="24dp"
tools:src="@mipmap/main_tab_news_b"
android:layout_centerHorizontal="true"
android:layout_height="24dp"/>
<ImageView
android:id="@+id/mainTabBottomIconG"
android:layout_width="24dp"
android:layout_centerHorizontal="true"
tools:src="@mipmap/main_tab_news_g"
android:layout_height="24dp"/>
<TextView
android:id="@+id/mainTabBottomText"
android:layout_below="@id/mainTabBottomIconG"
android:layout_width="wrap_content"
tools:text="消息"
android:layout_marginTop="2dp"
android:textSize="@dimen/text_size_10sp"
android:layout_centerHorizontal="true"
android:textColor="@color/black"
android:layout_height="wrap_content"/>
RelativeLayout>
自定义属性:
<declare-styleable name="MainBtmTabBtn">
<attr name="iconChecked" format="reference"/>
<attr name="iconNoChecked" format="reference"/>
<attr name="textBtm" format="reference"/>
declare-styleable>
接下来在activity里面使用就很简单了:
activity代码:
class MainActivity : AppCompatActivity(), View.OnClickListener, ViewPager.OnPageChangeListener {
val btmBtns: List<BottomTabBtn> by lazy {
arrayListOf(mainRbNews, mainRbTv, mainRbTvNow, mainRbMe)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity_main)
initView()
}
private fun initView() {
val mainViewPagerAdapter = MainViewPagerAdapter(supportFragmentManager)
mainVp.adapter = mainViewPagerAdapter
mainVp.addOnPageChangeListener(this)
mainRbNews.setOnClickListener(this)
mainRbTv.setOnClickListener(this)
mainRbTvNow.setOnClickListener(this)
mainRbMe.setOnClickListener(this)
//默认选中第一个
changeBtmChecked(0)
}
override fun onClick(view: View?) {
when (view) {
mainRbNews -> {
changeBtmChecked(0)
}
mainRbTv -> {
changeBtmChecked(1)
}
mainRbTvNow -> {
changeBtmChecked(2)
}
mainRbMe -> {
changeBtmChecked(3)
}
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
//第一个页面 -> 第二个页面 position 一直0最后突变1 positionOffset 0 渐变 1 突变 0
//第二个页面 -> 第一个页面 position 一直0 positionOffset 1 渐变 0
//页面一到二 最后突变为1时positionOffset = 0.0,此时特殊情况不做处理
if (positionOffset != 0.0f) {
btmBtns[position].alphaNum = 1 - positionOffset
btmBtns[position + 1].alphaNum = positionOffset
} else { //等于0.0说明静止 此时position就是选中的页面
changeBtmChecked(position)
btmBtns.forEachIndexed { index, bottomTabBtn ->
bottomTabBtn.checked = position == index
}
}
}
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageSelected(position: Int) {}
/**
* 改变底部选中状态
* p 选中位置
*/
private fun changeBtmChecked(p: Int) {
//可以控制点击是否是否开启动画
mainVp.setCurrentItem(p, true)
}
}
xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:btmtab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:orientation="vertical"
android:background="@color/white"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.viewpager.widget.ViewPager
android:id="@+id/mainVp"
android:layout_width="match_parent"
android:layout_height="@dimen/size_0dp"
android:layout_weight="1"
android:orientation="vertical"/>
<LinearLayout
android:id="@+id/mainLlBottom"
android:layout_width="match_parent"
android:background="@color/gray_f6"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_height="@dimen/size_50dp">
<com.iyao.module_main.BottomTabBtn
android:id="@+id/mainRbNews"
btmtab:textBtm="@string/main_btm_news"
android:layout_weight="1"
btmtab:iconChecked="@mipmap/main_tab_news_g"
btmtab:iconNoChecked="@mipmap/main_tab_news_b"
android:layout_width="@dimen/size_0dp"
android:layout_height="wrap_content"/>
<com.iyao.module_main.BottomTabBtn
android:id="@+id/mainRbTv"
btmtab:textBtm="@string/main_btm_tv"
android:layout_weight="1"
btmtab:iconChecked="@mipmap/main_tab_contract_g"
btmtab:iconNoChecked="@mipmap/main_tab_contact_b"
android:layout_width="@dimen/size_0dp"
android:layout_height="wrap_content"/>
<com.iyao.module_main.BottomTabBtn
android:id="@+id/mainRbTvNow"
btmtab:textBtm="@string/main_btm_tvnow"
btmtab:iconChecked="@mipmap/main_tab_find_g"
btmtab:iconNoChecked="@mipmap/main_tab_find_b"
android:layout_weight="1"
android:layout_width="@dimen/size_0dp"
android:layout_height="wrap_content"/>
<com.iyao.module_main.BottomTabBtn
android:id="@+id/mainRbMe"
btmtab:textBtm="@string/main_btm_me"
android:layout_weight="1"
btmtab:iconChecked="@mipmap/main_tab_mine_g"
btmtab:iconNoChecked="@mipmap/main_tab_mine_b"
android:layout_width="@dimen/size_0dp"
android:layout_height="wrap_content"/>
LinearLayout>
LinearLayout>
这个效果还是很简单的,一般不会叫你写个同样的效果,但通过举一反三,以后遇到类似需求就能很快有思路
如果你想看更多代码请点击这里前往GitHub,这是我写的一个组件化项目,才处于开始阶段,本文涉及代码在module_demo下。