kotlin 仿微信 底部导航栏 动态切换效果

效果

在这里插入图片描述
可以看到,当页面切换时 下方图标以及文字跟着动态改变

实现思路

思路还是很简单的关键步骤如下

  • 页面的搭建
    采用Activity+viewPager+fragment 这种写法其实是有很多隐藏坑,如果有兴趣可以在看看我的另一篇小文章ViewPager+Fragment+Activit构建页面值得注意的点
  • 动画效果
    • 图标动画效果:两张相同大小不同颜色图标,根据一个0到1的值,动态改变透明度
    • 文字变色效果:通过一个0到1的值、初始颜色、结束颜色,动态改变文字颜色设置给TextView

具体实现

页面搭建这里就不写了,主要讲一下动画效果:

上面的思路知道,这个动画核心是需要一个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下。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近在学习kotlin,项目中正好用到了图片浏览,就用kotlin放照微信的做了一个,效果如下:大概就是这么个效果,图片从小到大的一个效果,然后滑动切换图片,点击后会返回对应图片的位置,其实比较容易,用到的是ActivityOptions,一个activity的转场动画,下面说一下怎么实现。github地址点这里关于kotlin的配置就不过多说了,网上好多教程(最近kotlin好火)布局就是一个recyclerviewmainactivity代码:可以看到就一个初始化布局管理器和设置适配器的代码没什么了(你说没看到初始化控件?看看这篇文章)到这里之前都很好理解,关键就是adapter和点击图片跳转的代码在API 21以后,我们可以使用内置的Activity切换动画。但是这样也就意味着只能兼容5.0之后的系统,ActivityOptions是一个静态类,它提供了为数不多的几种方法,我们正是用其中的makeSceneTransitionAnimation(Activity activity,Pair… sharedElements)来完成了这个效果,有兴趣的小伙伴移步至这里,先来说下传递的两个参数,第一个activity不用说,第二个pair,View是你开始动画view对象,string是他的动画名要和结束动画view名一致才可以,到这里应该可以理解,我们在创建适配器的时候给每个imageview起了一个transitionname当我们跳转至查看大图界面给当前的viewpage赋值相同的名称便能完成动画,然后在我们滑动时更改viewpage的transitionname为对应图片的transitionname这样在点击图片的时候就会显示对应图片的下标。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值