文章目录

  • 前言
  • 分析
  • 实现字体缩放动画
  • 实现 Indicator 的长度变化动画
    • 一、准备工作
    • 二、让 TextView 撑满 TabView
    • 三、实现滑动时 Indicator 的动画效果
  • 写在最后
  • 完整 demo 地址
  • 参考

前言

最近在做的一个小说阅读 APP,打算模仿掌阅实现 TabLayout 切换时的动画效果。

首先看下掌阅的切换效果:

接下来是我的实现效果:

分析

切换动画主要有两部分组成:

  1. 字体的缩放动画:进入页面的字体逐渐放大,移除页面的字体逐渐缩小
  2. Indicator 的长度变化动画:在进行页面滑动时,Indicator 的长度由短边长再变短。以页面移出一半为分界线,前半部分 Indicator 由短变长,后半部分 Indicator 由长变短。

接下来的实现也分这两部分进行。

实现字体缩放动画

这里的重点是获取到当前页面移出屏幕和旁边页面进入屏幕的比例,我采用的方法是实现 ViewPager.PageTransformer 接口,通过里面 transformPage 方法的 position 参数获取页面移出(进入)屏幕的比例。

这里的难点是如何理解 position 参数的变化规律,怎么理解就不在这里讲了,要解释清楚需要很大篇幅,想要了解的话可以另外查资料,或者看下我写的这篇分析:ViewPager.PageTransformer 的 position 分析

具体的接口实现如下:

/*** @author Feng Zhaohao* Created on 2019/11/2*/
public class MyPageTransformer implements ViewPager.PageTransformer {public static final float MAX_SCALE = 1.3f;private TabLayout mTabLayout;@SuppressLint("UseSparseArrays")private HashMap<Integer, Float> mLastMap = new HashMap<>();public MyPageTransformer(TabLayout mTabLayout) {this.mTabLayout = mTabLayout;}@Overridepublic void transformPage(@NonNull View view, float v) {if (v > -1 && v < 1) {int currPosition = (int) view.getTag(); // 获取当前 View 对应的索引final float currV = Math.abs(v);if (!mLastMap.containsKey(currPosition)) {mLastMap.put(currPosition, currV);return;}float lastV = mLastMap.get(currPosition);// 获取当前 TabView 的 TextView LinearLayout ll = (LinearLayout) mTabLayout.getChildAt(0);TabLayout.TabView tb = (TabLayout.TabView) ll.getChildAt(currPosition);View textView = tb.getTextView();// 先判断是要变大还是变小// 如果 currV > lastV,则为变小;如果 currV < lastV,则为变大if (currV > lastV) {float leavePercent = currV; // 计算离开屏幕的百分比// 变小textView.setScaleX(MAX_SCALE - (MAX_SCALE - 1.0f) * leavePercent);textView.setScaleY(MAX_SCALE - (MAX_SCALE - 1.0f) * leavePercent);} else if (currV < lastV) {float enterPercent = 1 - currV; // 进入屏幕的百分比// 变大textView.setScaleX(1.0f + (MAX_SCALE - 1.0f) * enterPercent);textView.setScaleY(1.0f + (MAX_SCALE - 1.0f) * enterPercent);}mLastMap.put(currPosition, currV);}}
}

有几点说明一下。

  1. 为了通过 View 获取到其对应的位置,在给 Fragment 创建视图的时候,给它设置一个 tag,值为它的位置索引:
    @Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_test, null);view.setTag(index);     // index 为该 Fragment 的位置索引return view;}
  1. 在获取当前 TabView 的 TextView 时,我通过当前位置获取到的 TabView 可以直接进行转换:
            LinearLayout ll = (LinearLayout) mTabLayout.getChildAt(0);TabLayout.TabView tb = (TabLayout.TabView) ll.getChildAt(currPosition);

如果使用官方 TabLayout,是不能这样做的,因为它内部的 TabView 不是 public,你在外界不能访问到。我这里能这样做是因为这个 TabLayout 是我拷贝官方 TabLayout 到我的项目中,并对它进行了一些修改(之所以要修改官方 TabLayout,是为了实现 Indicator 的动画效果,之后会说到)。这里只需将内部类 TabView 的访问等级修改为 public 就行了。

  1. 这里我是通过 setScaleX 和 setScaleY 方法对 TextView 进行缩放操作。

其实一开始我是直接使用 setTextSize 来进行缩放的,不过这样做的话会很发生很明显的文字抖动,所以放弃了这种做法。

后来参考一个开源库(MagicIndicator)的实现,使用 setScaleX 和 setScaleY 方法进行缩放,发现这样做效果好了很多。最终采用了这种方式实现文字的缩放。

以下是我对于这种差别的原因猜测(不知道对不对):setScaleX 和 setScaleY 内部使用 invalidate(false) 重绘视图,而 setTextSize 使用 invalidate()(即 invalidate(true))重绘视图, invalidate 方法传入的参数表示是否让此视图的缓存无效,也就是说传入 true 时不使用缓存。所以我觉得是因为 setScaleX 和 setScaleY 使用了缓存,所以效率更高,特别是进行动画时需要重绘多次,使用缓存对于效率的提高就更加明显了。

实现 Indicator 的长度变化动画

实现这个还是挺不容易的。要实现长度变化动画,你首先就要改变它的长度,但 TabLayout 并没有直接提供设置 Indicator 宽度的方法,只能通过其他方式来设置。

总的来说有这几种方法:反射实现、自定义 View、sdk28+ 属性配置、layer-list。但是这几种方法都不能实现最终的动画效果。

反射不能让 Indicator 的宽度小于文本宽度,不然会压缩文本。sdk28+ 属性配置虽然简单,但 Indicator 的宽度只能等于文本宽度。自定义 View 实现麻烦,并且很难实现动画效果。layer-list 实现简单,但缺点是 Indicator 没有动画效果。

以上方法除了自定义 View,我都一一尝试过,后来发现很难满足自己的需求。后来看到这篇文章骚操作之改造TabLayout,修改指示线宽增加切Tab过渡动画,里面讲到可以通过修改官方 TabLayout 来实现功能,受此启发,我最终通过修改官方 TabLayout 实现了 Indicator 的长度变化动画。

下面看下具体过程:

一、准备工作

  1. 导入相关类

这里我使用的是 API26 的 TabLayout。

  1. 一开始发现 Indicator 不显示,需在 xml 文件设置 Indicator 的颜色和高度
    <com.feng.tablayoutdemo.tablayout.TabLayoutandroid:id="@+id/tab_layout"android:layout_width="match_parent"android:layout_height="wrap_content"app:tabIndicatorColor="@android:color/holo_blue_light"app:tabIndicatorHeight="3dp" />
  1. 一些分析

TabLayout 包含一个 SlidingTabStrip,SlidingTabStrip 中包含 n 个 TabView,TabView 包含:

        private Tab mTab;private TextView mTextView;private ImageView mIconView;

通过 TabView 的 update() 方法添加 mIconView 和 mTextView

TextView 不能放大到整个 tab,是因为它的父 View(TabView)默认设置了 padding。

二、让 TextView 撑满 TabView

默认情况下,TabView 是有 padding 的,所以文本之间才会有间距。但是这样会影响到我放大字体,字体放大是需要空间的,所以要在 TextView 内部设置 padding,但是由于 TabView 也有 padding,两个 padding 加起来会使得文本间距很大,很难看。所以我要取消 TabView 的 padding,让 TextView 撑满整个 TabView,然后在 TextView 内部设置 padding,留下放大的空间。

实现过程如下:

  1. 取消 TabView 默认的 padding:
        public TabView(Context context) {//            ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
//                    mTabPaddingEnd, mTabPaddingBottom);}
  1. 给 TabView 的 TextView 加上自己的布局:
        final void update() {if (mTextView == null) {//                    TextView textView = (TextView) LayoutInflater.from(getContext())
//                            .inflate(R.layout.design_layout_tab_text, this, false);TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(com.feng.tabdemo.R.layout.tab_text, this, false);}}

R.layout.tab_text:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:ellipsize="end"android:gravity="center"android:maxLines="2"android:paddingStart="10dp"android:paddingEnd="10dp"/>

通过 paddingStartpaddingEnd 设置字体间的间距。

  1. TabView 设置了一个最小宽度,为了防止 TextView 比较短时不能撑满,把它取消掉:
    private TabView createTabView(@NonNull final Tab tab) {//        tabView.setMinimumWidth(getTabMinWidth());}

三、实现滑动时 Indicator 的动画效果

动画效果:当从一个页面滑向另一个页面时,Indicator 的宽度由短变长再变短。

下面简单分析下动画过程

以页面滑出一半(另一个页面进来一半)为分界线,在前半段,由短变长:

页面滑动一半时,Indicator 的宽度达到最长:

在后半段,由长变短:

实现步骤:

  1. SlidingTabStrip 的修改
    public class SlidingTabStrip extends LinearLayout {// Indicator 的左右边界private float left;private float right;@Overridepublic void draw(Canvas canvas) {super.draw(canvas);canvas.drawRect(left, getHeight() - mSelectedIndicatorHeight,right, getHeight(), mSelectedIndicatorPaint);}}

在 SlidingTabStrip 中增加两个变量表示 Indicator 的左右边界,并且改写它的 draw 方法,这样做是为了方便滑动监听器修改 Indicator 的左右边界。

  1. 修改 TabLayoutOnPageChangeListener 的 onPageScrolled 方法

TabLayoutOnPageChangeListener 的 onPageScrolled 方法在页面滑动时回调,可以从中知道偏移页面和页面偏移量。知道偏移量就可以重新设置 Indicator 的左右边界并进行动画重绘。具体代码如下:

        /*** @param position 当前显示的第一页的索引(右侧页面进入时显示的是当前页面,左侧页面进入时显示的是左侧页面)* @param positionOffset 取值范围 [0, 1),表示 position 页面的偏移量(在屏幕外的比例)* @param positionOffsetPixels*/@Overridepublic void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels) {// ... 原有代码不用删,保留即可if (tabLayout == null) {return;}// Indicator 的宽度占 TabView 的比例float scale = 0.3f;// 左滑(右侧页面进入)的第一阶段// 以及右滑(左侧页面进入)的第二阶段if (positionOffset > 0 && positionOffset < 0.5) {tabLayout.mTabStrip.left = tabLayout.mTabStrip.getChildAt(position).getLeft()+ scale * tabLayout.mTabStrip.getChildAt(position).getWidth();float lr = tabLayout.mTabStrip.getChildAt(position).getRight()- scale * tabLayout.mTabStrip.getChildAt(position).getWidth();float rr = tabLayout.mTabStrip.getChildAt(position + 1).getRight()- scale * tabLayout.mTabStrip.getChildAt(position + 1).getWidth();tabLayout.mTabStrip.right = lr + (positionOffset / 0.5f) * (rr - lr);ViewCompat.postInvalidateOnAnimation(tabLayout.mTabStrip);}// 左滑(右侧页面进入)的第二阶段// 以及右滑(左侧页面进入)的第一阶段if (positionOffset > 0.5) {float rr = tabLayout.mTabStrip.getChildAt(position + 1).getRight()- scale * tabLayout.mTabStrip.getChildAt(position + 1).getWidth();// 先确保 Indicator 滑动最右if (tabLayout.mTabStrip.right < rr) {tabLayout.mTabStrip.right = rr;ViewCompat.postInvalidateOnAnimation(tabLayout.mTabStrip);}float ll = tabLayout.mTabStrip.getChildAt(position).getLeft()+ scale * tabLayout.mTabStrip.getChildAt(position).getWidth();float rl = tabLayout.mTabStrip.getChildAt(position + 1).getLeft()+ scale * tabLayout.mTabStrip.getChildAt(position + 1).getWidth();tabLayout.mTabStrip.left = ll + ((positionOffset - 0.5f) / 0.5f) * (rl - ll);ViewCompat.postInvalidateOnAnimation(tabLayout.mTabStrip);}// 滑动开始或结束if (positionOffset == 0) {tabLayout.mTabStrip.left = tabLayout.mTabStrip.getChildAt(position).getLeft()+ scale * tabLayout.mTabStrip.getChildAt(position).getWidth();tabLayout.mTabStrip.right = tabLayout.mTabStrip.getChildAt(position).getRight()- scale * tabLayout.mTabStrip.getChildAt(position).getWidth();ViewCompat.postInvalidateOnAnimation(tabLayout.mTabStrip);}}

到此为止,就实现了完整的 Indicator 动画效果。

字体的缩放动画和 Indicator 的动画是分开实现的,把它们两者合并起来就是最后的动画效果了,合并的时候并不会产生什么冲突,毕竟它们的实现原理不同,也没有什么耦合。

写在最后

实现完整的动画效果花了我三天时间,这三天包括还周末的两天,时间还是花了挺久的。不过总的来说还是值得的,因为在实现过程不断地发现问题不断地解决,学到了不少。对反射的作用有了新的理解,对 TabLayout 的布局更加清晰了,也体会到了改官方源码的快感(笑)。写这篇文章更多是为了趁刚写完把自己的理解记录下来,方便以后回看。如果能够帮助到有需要的人,那就更好了。

完整 demo 地址

  • Mrfzh/TabLayoutAnimatorDemo

参考

  • 骚操作之改造TabLayout,修改指示线宽增加切Tab过渡动画
  • 仿陌陌选项卡:文字大小变化的SlidingScaleTabLayout
  • 优雅地修改 TabLayout 指示线 Indicator 的宽度
  • 关于Android改变TabLayout 下划线(Indicator)宽度实践总结

仿掌阅实现 TabLayout 切换时的字体和 Indicator 动画相关推荐

  1. Android动画之仿美团加载数据等待时,小人奔跑进度动画对话框(附顺丰快递员奔跑效果)...

    Android动画之仿美团加载数据等待时,小人奔跑进度动画对话框(附顺丰快递员奔跑效果) 首句依然是那句老话,你懂得! finddreams :(http://blog.csdn.net/finddr ...

  2. 仿掌阅app打开书籍动画效果

    点击上方的终端研发部,右上角选择"设为星标" 每日早9点半,技术文章准时送上 公众号后台回复"学习",获取作者独家秘制精品资料 本文由作者:潇湘夜雨投稿 链接: ...

  3. 仿掌阅实现书籍打开动画

    一. 前言 上次打开掌阅的时候看到书籍打开动画的效果还不错,正好最近也在做阅读器的项目,所以想在项目中实现一下. 二. 思路 讲思路之前,先看一下实现效果吧: 书籍打开关闭动画.gif 看完实现效果, ...

  4. Android 仿掌阅 小说阅读器 书籍打开动画

    搜了半天 终于找到关键字 掌阅 . ireader  可惜放到项目炸了,,,, 完整代码 // 万能适配器compile 'com.github.CymChad:BaseRecyclerViewAda ...

  5. Android --- TabLayout 切换时,改变选项卡下字体的状态(大小、加粗、默认被选中第一个)

    文章目录 一.前言 二.源码实例 1.选项卡所在的布局文件 `fragment_course_selection.xml` 2.选项卡所在类 `CourseSelectionFragment.java ...

  6. Qml界面切换时,字体消失或乱码

    文章目录 前言 效果图 原因分析 解决方案 前言 最近开发qml 桌面应用时,在同事的 win8系统 系统上,字体会离奇消失或者乱码,在win7, win10上面就不会有这个问题,我自己下的win8也 ...

  7. android全屏与非全屏切换时Toolbar的显示,仿微信漂流瓶效果

    Toolbar已经代替了actionbar,特别是在4.4或更高版本上可以有沉浸式效果,要使用Toolbar还要配置相关的Noactionbar style样式,但是如果在全屏与非全屏切换时,如何使用 ...

  8. 掌阅群分享技术点收集(app性能优化专攻)

    保活 先从老式最基础的开始: 使用startService方式启动一个独立进程的服务,这样系统会在service意外死亡后自动重启. 使用RTC定时闹钟每5分钟检测一下(4.0以上基本无效) 启动li ...

  9. ViewPager系列之-仿掌上英雄联盟皮肤浏览效果

    封面图.png 能有一个双休的周末,对于程序员来说,也算是一件幸福的事情吧.苦逼的加了一周的班,终于可以休息放松放松了.作为一个LOL爱好者,周末最开心的事当然就是约上几个小伙伴一起开黑了.一起超神. ...

最新文章

  1. 高职高考难度大吗_成人高考与普通高考区别成人高考和高考的难度一样吗
  2. 解决intellij IEDA mapper.xml文件警告以及could not autowire的错误提示
  3. 为什么不应该重写 service 方法?
  4. Angular4+AdminLTE+Jeecg 前后端分离框架实战-张代浩-专题视频课程
  5. BNU10791:DOTA选人
  6. 【BZOJ4260】Codechef REBXOR(前i个数的最大区间异或值---01字典树+dp)
  7. vue+elementui+quill富文本框+秀米编辑器和135编辑器
  8. 为什么建设企业网站是必须的?小企业有必要做网站建设吗?
  9. TestNG 单元测试框架的使用
  10. 冉宝的每日一题-8月16日回溯法+ 动态规划压缩
  11. 微信浏览器唤起微信登录
  12. 你是外包,麻烦不要偷吃零食。。。网友:...
  13. python scapy2.3 在windows上的安装
  14. syntax error near unexpected token else
  15. 阿里云添加域名解析设置
  16. 用C语言编写程序计算对角线的和,C语言入门级代码 计算二维数组主对角线上的元素之和...
  17. 批量给pdf添加目录(最完整详细方法)
  18. 魔域服务器修改和宝宝数据,宝宝属性与真实值之间的公式计算关系
  19. Redis之多实例的操作
  20. Farmer John's math(c++)

热门文章

  1. 最新最全内隐神经表征论文合集
  2. 找靓机AppUI自动化测试延伸
  3. 速度收藏!《阿里技术参考图册》发布,公开600页技术全景图
  4. 【愚公系列】2021年12月 Java教学课程 31-继承详解
  5. python 3.6.6安装fake_人脸识别替换之fakeApp,deepfacelib
  6. 51 中断系统 外部中断0 外部中断1
  7. 四篇关于chen_zhe的美文
  8. 【每日一练SQL】oracle的merge into函数的应用updateORinsert
  9. 使用scala语言编写Spark独立应用程序实现词频统计
  10. redis实现session共享