闲着无聊,撸个微信导航栏的动画吧!

/   今日科技快讯   /

9月10日,按照去年的约定,马云正式辞去阿里巴巴董事局主席,由现任集团CEO张勇(逍遥子)接任,马云将退休。这不是马云的第一次“退休”。

实际上马云的第一次退休可以追溯到2006年11月,在引入职业经理人后,他把公司总裁的位置让给卫哲,第二年,在卫哲的努力下,阿里巴巴B2B在香港成功上市,发行价为13.5港元。

马云的第二次退休在2013年5月,马云辞任阿里巴巴集团CEO,由陆兆禧接任。48岁的马云在杭州黄龙体育场单膝跪地,向员工宣布:48岁以前,工作就是我的生活;48岁以后,生活就是我的工作,以后就拜托大家了。

而马云的第三次退休就是今天,马云辞去阿里巴巴董事局主席,阿里换帅。

/   作者简介   /

本篇文章来自不惜留恋_的投稿,分享了他对微信导航栏动画的实现,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

不惜留恋_的博客地址:

https://juejin.im/user/577e6c37165abd00554b2245

/   开始   /

微信自发布以来,底部导航栏的动画一直让开发者津津乐道,而且伴随着版本更新,底部导航栏的动画也一直在改进。我最近在闲暇之余,看了下微信的底部导航栏动画,于是思考了下这个动画的原理,感觉非常有意思,于是写下这篇文章。

下图就是我实现的效果,大家可以对比下微信的效果,几乎可以以假乱真。

/   动画过程   /

关于这个动画的过程,我刚开始了是瞅了老半天了,因为如果我们不了解动画的过程也是无从去实现了,所以动画过程很重要,这个动画其实有两个过程。

  1. 首先是默认图片的轮廓变色。
  2. 轮廓变色到一定程度后,整个图片出现了绿色的填充效果,也就是整个图片开始变绿,直到整个图片完全变为了绿色。其实这是两个图片的透明度变换的达成的效果。

/ 动画实现原理  /

首先我们从整体上看,滑动的页面可以用ViewPager实现,在滑动的过程中,通过监听ViewPager的滑动事件,可以获取一个滑动的比例值。

底部的导航栏的4个Tab可以用自定义一个View来实现,我把这个自定义的View叫做TabView。那么,在滑动的过程中,当前页面的TabView执行褪色动画,后一个页面执行变色动画。动画到底执行到哪一步,肯定就是由ViewPager的滑动比例值决定的。因此TabView需要一个接收动画进度比例值的方法来控制动画的程度。

/   代码实现   /

俗话说得好,Talk is cheap, show me the code!。那我们就通过代码来实现我们之前的猜想吧,这肯定是一段非常激情的旅程!

由于不想篇幅过大,因此我省略了ViewPager的一些样板代码,因为这些属于基本功。如果不会用ViewPager,在网上随便搜索就是一大堆的文章,很轻松就掌握了。那么本文主要就是解决如何自定义这个TabView。

自定义View有很多方式,我相信很多人比我还懂。而我选择的是组合系统控件的方式来实现这个自定义View。那么可能有人问我,如果为了更好的绘制性能,能不能完全的自定义一个View来实现呢?这当然是可以的,学完本文你就可以做这个牛逼的操作。然而,这点绘制性能的提升,其实在现在的高配置的手机上是可以忽略的。那么为了开发效率,组合系统控件应该是首选。

/   实现组合控件的布局   /

TabView需要的组合控件的布局如下。


// tab_layout.xml

<?xml version=
“1.0” encoding=
“utf-8”?>

<LinearLayout xmlns:android=
“http://schemas.android.com/apk/res/android”

    android:layout_width=
“match_parent”

    android:layout_height=
“40dp”

    android:gravity=
“center_horizontal”

    android:orientation=
“vertical”>

    <FrameLayout

        android:layout_width=
“wrap_content”

        android:layout_height=
“0dp”

        android:layout_weight=
“1”>

        <ImageView

            android:id=
“@+id/tab_image”

            android:layout_width=
“wrap_content”

            android:layout_height=
“match_parent” />

        <ImageView

            android:id=
“@+id/tab_image_top”

            android:layout_width=
“wrap_content”

            android:layout_height=
“match_parent” />

    </FrameLayout>

    <TextView

        android:id=
“@+id/tab_title”

        android:layout_width=
“wrap_content”

        android:layout_height=
“wrap_content”

        android:textSize=
“12sp” />

</LinearLayout>

布局的TextView肯定是用来显示标题的,然而还有两个ImageView,为何这样设计呢?这与我们动画的实现有关。

@+id/tab_image的ImageView在底部,它是用来显示一个默认的图片的,我称它为轮廓图,例如第一个页面的TabView的轮廓图如下。

我们需要对这个轮廓进行变色处理,大家可以观察一下动画的过程,第一个过程很显然是轮廓的变色。

@+id/tab_image_top的ImageView在上面,它是用来显示一个页面被选中后的图片,也是动画最终要显示的图片,例如第一个页面的TabView的选中图片如下。

现在来说明下如何用这个布局来实现动画。

  1. 首先所有的TabView都显示轮廓图,选中图都进行隐藏。如何隐藏呢,我选择使用透明度来隐藏选中图,因为整个动画过程有透明度的变换。
  2. 当滑动ViewPager的时候,TabView获取滑动的进度值,我们就让轮廓图的轮廓开始变色。那么怎么变色呢,有一个很方便的方法,就是Drawable.setTint()方法。这个方法的原理就是PorterDuff.Mode.DST_IN混合模式。如果大家有兴趣,可以去研究下原理。
  3. 当ViewPager滑动到一定距离的时候,如果松开手指,页面会自动滑动到下一个页面,这个比例值到底是多少呢?我暂时还没有考究,我假定是0.5吧。当滑动的比较超过0.5的时候,就要让轮廓图的透明度逐渐变是0,也就是慢慢地的看不见了,同时,选中图的透明度逐渐变为255,也是慢慢的清晰了。如此一来,就会出现轮廓图的整体颜色填充效果。

怎么样,实现思路是不是有点意思,那么我们来根据这个思路来实现这个自定义ViewTabView吧。

/   实现TabView   /

加载布局

既然有了布局,那么首先用在TabView的构造函数中来加载这个布局。


public TabView(Context context, @Nullable AttributeSet attrs) {

        
super(context, attrs);

        
// 加载布局

        inflate(context, R.layout.tab_layout, 
this);

}

自定义属性与解析

为了更好的在XML布局中使用TabView,我为TabView抽取的自定义属性。


// res/values/tabview_attrs.xml

<?xml version=
“1.0” encoding=
“utf-8”?>

<resources>

    <declare-styleable name=
“TabView”>

        <attr name=
“tabColor” format=
“color|integer” />

        <attr name=
“tabImage” format=
“reference” />

        <attr name=
“tabSelectedImage” format=
“reference” />

        <attr name=
“tabTitle” format=
“string|reference” />

    </declare-styleable>

</resources>

  • tabColor代表变色最终显示的颜色,这个颜色可以从选中图中用取色器获取。
  • tabImage代表默认显示的轮廓图。
  • tabSelectedImage代表选中后的图。
  • tabTitle代表要显示的标题。

有了这些自定义属性,那么在TabView中必须要解析这些自定义属性。


public TabView(Context context, @Nullable AttributeSet attrs) {

        
super(context, attrs);

        
// 加载布局

        inflate(context, R.layout.tab_layout, 
this);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabView);

        
for (
int i = 
0; i < a.getIndexCount(); i++) {

            
int attr = a.getIndex(i);

            
switch (attr) {

                
case R.styleable.TabView_tabColor:

                    
// 获取标题和轮廓最终的着色

                    mTargetColor = a.getColor(attr, DEFAULT_TAB_TARGET_COLOR);

                    
break;

                
case R.styleable.TabView_tabImage:

                    
// 获取轮廓图

                    mNormalDrawable = a.getDrawable(attr);

                    
break;

                
case R.styleable.TabView_tabSelectedImage:

                    
// 获取选中图

                    mSelectedDrawable = a.getDrawable(attr);

                    
break;

                
case R.styleable.TabView_tabTitle:

                    
// 获取标题

                    mTitle = a.getString(attr);

                    
break;

            }

        }

        a.recycle();

    }

自定义属性解析完毕后,就需要给用这些属性值给控件进行初始化。View的onFinishInflate()方法代表布局加载完成,因此在这里获取控件,并进行初始化。


  @Override

    
protected void onFinishInflate() {

        
super.onFinishInflate();

        
// 1.设置标题,默认着色为黑色

        mTitleView = findViewById(R.id.tab_title);

        mTitleView.setTextColor(DEFAULT_TAB_COLOR);

        mTitleView.setText(mTitle);

        
// 2.设置轮廓图片,不透明,默认着色为黑色

        mNormalImageView = findViewById(R.id.tab_image);

        mNormalDrawable.setTint(DEFAULT_TAB_COLOR);

        mNormalDrawable.setAlpha(
255);

        mNormalImageView.setImageDrawable(mNormalDrawable);

        
// 3.设置选中图片,透明,默认着色为黑色

        mSelectedImageView = findViewById(R.id.tab_selected_image);

        mSelectedDrawable.setAlpha(
0);

        mSelectedImageView.setImageDrawable(mSelectedDrawable);

    }

标题设置了一个默认颜色DEFAULT_TAB_COLOR,是黑色。同样,也为轮廓图的轮廓设置黑色。轮廓图的透明度初始为255,也就是完全可见,而选中图的透明度设置为0,也就是完全不可见。所有这一切就是动画的初始状态。

控制动画进度

在前面的讲解动画的原理的时候说到一个事情,TabView需要使用ViewPager滑动进度值来控制动画的进度,因此还要为TabView定义一个接收进度值的方法。


   /**

     * 根据进度值进行变色和透明度处理。

     *

     * 
@param percentage 进度值,取值[0, 1]。

     */

    
public void setXPercentage(float percentage) {

        
if (percentage < 
0 || percentage > 
1) {

            
return;

        }

        
// 1. 颜色变换

        
int finalColor = evaluate(percentage, DEFAULT_TAB_COLOR, mTargetColor);

        mTitleView.setTextColor(finalColor);

        mNormalDrawable.setTint(finalColor);

        
// 2. 透明度变换

        
if (percentage >= 
0.5 && percentage <= 
1) {

            
// 原理如下

            
// 进度值: 0.5 ~ 1

            
// 透明度: 0 ~ 1

            
// 公式: percentage – 1 = (alpha – 1) * 0.5

            
int alpha = (
int) Math.ceil(
255 * ((percentage – 
1) * 
2 + 
1));

            mNormalDrawable.setAlpha(
255 – alpha);

            mSelectedDrawable.setAlpha(alpha);

        } 
else {

            mNormalDrawable.setAlpha(
255);

            mSelectedDrawable.setAlpha(
0);

        }

        
// 3. 更新UI

        invalidateUI();

    }

在这个对外开放的接口中,首先我们要根据进度值来计算轮廓要使用的颜色。起始颜色是黑色,最终颜色是一个绿色,然后我们还有一个进度值,那么如何计算某个进度的对应的颜色值呢?其实在属性动画中有一个类,ArgbEvaluator,它提供了颜色的计算方法,代码如下。


  
public Object evaluate(float fraction, Object startValue, Object endValue) {

        
int startInt = (Integer) startValue;

        
float startA = ((startInt >> 
24) & 
0xff) / 
255.0f;

        
float startR = ((startInt >> 
16) & 
0xff) / 
255.0f;

        
float startG = ((startInt >>  
8) & 
0xff) / 
255.0f;

        
float startB = ( startInt        & 
0xff) / 
255.0f;

        
int endInt = (Integer) endValue;

        
float endA = ((endInt >> 
24) & 
0xff) / 
255.0f;

        
float endR = ((endInt >> 
16) & 
0xff) / 
255.0f;

        
float endG = ((endInt >>  
8) & 
0xff) / 
255.0f;

        
float endB = ( endInt        & 
0xff) / 
255.0f;

        
// convert from sRGB to linear

        startR = (
float) Math.pow(startR, 
2.2);

        startG = (
float) Math.pow(startG, 
2.2);

        startB = (
float) Math.pow(startB, 
2.2);

        endR = (
float) Math.pow(endR, 
2.2);

        endG = (
float) Math.pow(endG, 
2.2);

        endB = (
float) Math.pow(endB, 
2.2);

        
// compute the interpolated color in linear space

        
float a = startA + fraction * (endA – startA);

        
float r = startR + fraction * (endR – startR);

        
float g = startG + fraction * (endG – startG);

        
float b = startB + fraction * (endB – startB);

        
// convert back to sRGB in the [0..255] range

        a = a * 
255.0f;

        r = (
float) Math.pow(r, 
1.0 / 
2.2) * 
255.0f;

        g = (
float) Math.pow(g, 
1.0 / 
2.2) * 
255.0f;

        b = (
float) Math.pow(b, 
1.0 / 
2.2) * 
255.0f;

        
return Math.round(a) << 
24 | Math.round(r) << 
16 | Math.round(g) << 
8 | Math.round(b);

    }

熟悉属性动画的应该知道,参数float fraction的取值范围为0.f到1.f,所以可以把这个方法拷贝过来使用。

计算出颜色值后,就可以对标题和轮廓图着色了。

第二步,按照之前说的动画原理,当滑动的进度达到0.5后,要对轮廓图和选中图进行透明度的变换。

那么首先我们得计算出某个进度对应的透明度。很明显,这是一道数学题,进度的变化范围是从0.5到1.0,透明度的变换取0到1.0(之后于乘以255即可得到实际的透明度)。透明度和进度的比例值是2,那么就可以得出一个公式alpha – 1 = (percentage – 1.0) * 2。有了这个公式,就可以算出任意进度值对应的透明度了。

这一切就绪后,我们就使出杀手锏了,更新UI,让系统进行重绘。

/   与ViewPager联动   /

最重要的自定义View已经准备完毕,是时候来测试效果了。那么我们必须要知道如何获取ViewPager的滑动进度值了,我们可以为ViewPager设置滑动监听器。


     mViewPager.addOnPageChangeListener(
new ViewPager.SimpleOnPageChangeListener() {

            
@Override

            
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

        });

参数float positionOffset就是一个进度值,但是这个进度值使用起来还是需要点小技巧的,我们先从源码中看下解释。


   
/**
         * This method will be invoked when the current page is scrolled, either as part
         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
         *
         * @param position Position index of the first page currently being displayed.
         *                 Page position+1 will be visible if positionOffset is nonzero.
         * @param positionOffset Value from [0, 1) indicating the offset from the page at position.
         * @param positionOffsetPixels Value in pixels indicating the offset from position.
         */


        
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

从注释中可以看出,onPageScrolled方法是在滑动的时候调用,参数position代表当前显示的页面,这表解释很容易产生误解,其实无论是从左边往右边滑动,还是从右边往左边滑动,position始终代表左边的页面,因此position + 1始终代表右边的页面。

参数positionOffset代表滑动的进度值,并且还有很重要一点,大部分人都会忽略,如果参数positionOffset为非零值,表示右边的页面可见,也就是说,如果positionOffset的值是零,那么代表右边的页面是不可见的,这一点会在代码中体现出来。既然已经对参数有所了解,那么现在来看看实现。


        
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

                
// 左边View进行动画

                mTabViews.get(position).setXPercentage(
1 – positionOffset);

                
// 如果positionOffset非0,那么就代表右边的View可见,也就说明需要对右边的View进行动画

                
if (positionOffset > 
0) {

                    mTabViews.get(position + 
1).setXPercentage(positionOffset);

                }

            }

mTabViews是一个ArrayList,它保存了所有的TabView,我们页面中有四个TabView。mTabViews.get(posistion)获取的是滑动时左边的页面,mTabViews.get(position + 1)获取的就是右边的页面。

当从左边向右边滑动的时候,左边页面的positionOffset的值是从0到1的,此时我们需要左边的页面的TabView执行褪色动画。然而在我们设计的TabView中,进度值达到1的时候,执行的是变色动画,而不是褪色动画,因此左边页面的TabView的进度取值要改变下,取1 – positionOffset。那么右边的页面的进度取值自然就是positionOffset了。从右到左的滑动的原理其实与从左到右的滑动的原理是一样的,大家可以从Log中看出端倪。

然而,在为左边的TabView做动画的时候,我们一定要确保有右边的页面存在。我们前面讲解的时候说过,如果positionOffset为0的时候,右边的页面是不可见的,因此我们要做一些排除的动作,这在代码中有体现的。

/   代码优化   /

ViewPager可以自动滑动到下一个页面的进度值临界点是多少?TabView需要这个临界点来控制透明度的变换。

TabView只能通过XML的属性来控制图片的显示,控制最终显色的颜色等等功能,其实这些可以通过代码动态控制,我们可以实现一个对外的接口。

如果大家是个精益求精的人,可以对这两点进行考究和实现。

/   结束   /

本文把动画的原理,以及如何用代码实现这些原理讲解清楚了,这些都是关键部分。然而其它部分的代码我并没有给出。为了方便想查看demo的人,我把代码上传到 github。项目地址为:

https://github.com/buxiliulian/WeChatBottomNavigation

推荐阅读: 总是听到有人说AndroidX,到底什么是AndroidX?

真香系列,开箱即用的自定义Banner

看一看Facebook工程师是怎么评价《第一行代码》的

欢迎关注我的公众号 学习技术或投稿

长按上图,识别图中二维码即可关注

本文地址:https://blog.csdn.net/c10WTiybQ1Ye3/article/details/100773411

(0)
上一篇 2022年3月22日
下一篇 2022年3月22日

相关推荐