转载请注明出处: juejin.im/post/5d5365…php
微信自发布以来,底部导航栏的动画一直让开发者津津乐道,并且伴随着版本更新,底部导航栏的动画也一直在改进。我最近在闲暇之余,看了下微信的底部导航栏动画,因而思考了下这个动画的原理,感受很是有意思,因而写下这篇文章。java
下图就是我实现的效果,你们能够对比下微信的效果,几乎能够以假乱真。android
关于这个动画的过程,我刚开始了是瞅了老半天了,由于若是咱们不了解动画的过程也是无从去实现了,因此动画过程很重要,这个动画其实有两个过程git
首先咱们从总体上看,滑动的页面能够用ViewPager
实现,在滑动的过程当中,经过监听ViewPager
的滑动事件,能够获取一个滑动的比例值。github
底部的导航栏的4个Tab能够用自定义一个View来实现,我把这个自定义的View叫作TabView
。那么,在滑动的过程当中,当前页面的TabView
执行褪色动画,后一个页面执行变色动画。动画到底执行到哪一步,确定就是由ViewPager
的滑动比例值决定的。所以TabView
须要一个接收动画进度比例值的方法来控制动画的程度。微信
俗话说得好,Talk is cheap, show me the code!
。那咱们就经过代码来实现咱们以前的猜测吧,这确定是一段很是激情的旅程!ide
因为不想篇幅过大,所以我省略了ViewPager
的一些样板代码,由于这些属于基本功。若是不会用ViewPager
,在网上随便搜索就是一大堆的文章,很轻松就掌握了。那么本文主要就是解决如何自定义这个TabView
。函数
自定义View有不少方式,我相信不少人比我还懂。而我选择的是组合系统控件的方式来实现这个自定义View。那么可能有人问我,若是为了更好的绘制性能,能不能彻底的自定义一个View来实现呢?这固然是能够的,学完本文你就能够作这个牛逼的操做。然而,这点绘制性能的提高,其实在如今的高配置的手机上是能够忽略的。那么为了开发效率,组合系统控件应该是首选。布局
TabView
须要的组合控件的布局以下post
// 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
的选中图片以下
如今来讲明下如何用这个布局来实现动画。
TabView
都显示轮廓图,选中图都进行隐藏。如何隐藏呢,我选择使用透明度来隐藏选中图,由于整个动画过程有透明度的变换。ViewPager
的时候,TabView
获取滑动的进度值,咱们就让轮廓图的轮廓开始变色。那么怎么变色呢,有一个很方便的方法,就是Drawable.setTint()
方法。这个方法的原理就是PorterDuff.Mode.DST_IN
混合模式。若是你们有兴趣,能够去研究下原理。ViewPager
滑动到必定距离的时候,若是松开手指,页面会自动滑动到下一个页面,这个比例值究竟是多少呢?我暂时尚未考究,我假定是0.5吧。当滑动的比较超过0.5的时候,就要让轮廓图的透明度逐渐变是0,也就是慢慢地的看不见了,同时,选中图的透明度逐渐变为255,也是慢慢的清晰了。如此一来,就会出现轮廓图的总体颜色填充效果。怎么样,实现思路是否是有点意思,那么咱们来根据这个思路来实现这个自定义ViewTabView
吧。
既然有了布局,那么首先用在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,让系统进行重绘。
最重要的自定义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。所谓赠人玫瑰,手留余香,若是您以为代码写的还行,客官给个star或者fork吧~