写点什么

跑马灯带你深入浅出 TextView 的源码世界

  • 2022 年 3 月 22 日
  • 本文字数:5249 字

    阅读完需:约 17 分钟

一、背景


想必大家平时也没那么多时间是单独看源码,又或者只是单纯的看源码遇到问题还是不知道怎么从源码的角度解决。


但是大家平时开发过程中肯定会遇到这样或那样的小问题,通过百度、Google 搜索都无果,想尝试分析源码又不知道从什么地方开始分析起,导致最终放弃。


本篇文章就是通过一个小问题着手,从思路到实施一步步教大家面对一个问题时怎么从源码的角度去分析解决问题。


1.1 问题背景


在 Android6.0 及以上系统版本中,点击“添加购物车”按钮 TextView 跑马灯动画会出现跳动(动画重置,滚动从头重新开始)如下图所示:



1.2 前期准备


下好源码的 AndroidStuido 、生成一个 Android 模拟器、有问题的 demo 工程。


protected void onCreate(Bundle savedInstanceState) {       super.onCreate(savedInstanceState);       setContentView(R.layout.activity_main);       findViewById(R.id.show_tv).setSelected(true);       final TextView changeTv = findViewById(R.id.change_tv);       changeTv.setText(getString(R.string.shopping_count, mNum));       findViewById(R.id.click_tv).setOnClickListener(new View.OnClickListener() {           @Override           public void onClick(View v) {               mNum++;               changeTv.setText(getString(R.string.shopping_count, mNum));           }       });   }
复制代码


<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity">     <com.workshop.textview.MyTextView        android:id="@+id/show_tv"        android:layout_width="match_parent"        android:layout_height="40dp"        android:layout_alignParentTop="true"        android:layout_marginTop="30dp"        android:ellipsize="marquee"        android:focusable="true"        android:focusableInTouchMode="true"        android:marqueeRepeatLimit="marquee_forever"        android:padding="5dp"        android:scrollHorizontally="true"        android:textColor="@android:color/holo_blue_bright"        android:singleLine="true"        android:text="!!!广告!!!vivo S7手机将不惧距离与光线的限制,带来全场景化自拍体验,刷新了5G时代的自拍旗舰标准"        android:textSize="24sp" />      <TextView        android:id="@+id/change_tv"        android:layout_width="wrap_content"        android:layout_height="50dp"        android:layout_centerHorizontal="true"        android:layout_centerVertical="true"        android:text="@string/shopping_count"        android:textColor="@android:color/holo_orange_dark"        android:textSize="28sp" />     <TextView        android:id="@+id/click_tv"        android:layout_width="wrap_content"        android:layout_height="40dp"        android:layout_alignParentBottom="true"        android:layout_centerHorizontal="true"        android:layout_marginBottom="30dp"        android:background="@android:color/darker_gray"        android:padding="5dp"        android:singleLine="true"        android:text="添加购物车"        android:textColor="@android:color/background_dark"        android:textSize="24sp"        android:textStyle="bold" /> </RelativeLayout>
复制代码


二、思路


先说下解决问题的思路,个人也认为思路是本片文章比较重要的一个点。


  • 先去 Google 和百度上查找 textview 跑马灯的原理并最好能找到相关关键代码,如果没有找到保底也要找到一个分析的切入点。


  • 画出流程图整理出整体的跑马灯框架(如果只是想解决问题其实框架不用太细,不过这里为了把事情说清楚,会将原理说的更深一点)。


  • 找到影响跑马灯动画变化的关键因素,对影响变量变化的原因做一个适当的猜想。


  • 用 debug 手段验证自己的猜想。


  • 第四步和第五步持续的循环,最终找到自己的答案。


三、源码分析


3.1 跑马灯整体流程分析


我也跟大部分人一样,先 Google 一把,站在巨人的肩膀上,看看前人能不能给我一些思路,步骤如下;


1)打开 Google 搜索 “Android TextView 跑马灯 原理” ;


2)随便打开几个,这个时候我也不准备细看别人的分析,最好能找到框架图,找不到就找到关键代码实现也是好的;


3)果然没找到现成的框架图,但是找到一篇文章里提及了 startMarquee()方法。看到这名字就知道靠谱,因为和我们 xml 里面定义的定义的跑马灯参数是一致的。 android:ellipsize="marquee" ;


4)在 AndroidStdio 里搜索 TextView, 打开类接口图找到 startMarquee()方法,这里为了分析方便,我把方法贴到下面。



简单分析一下这个代码;


做了一些是否跑马灯的条件判断。以第 9,10 行为例,只有当前设置的 line 为 1,并且 ellipsize 属性是 marquee 才进行初始化操作。我们知道只有在 xml 里设置 singleline ="true"同时设置 ellipsize=“marquee”才能启动跑马灯,刚好和 9,10 行吻合。之后在 23 行执行 start 操作 start 的具体内容会在后面分析。


5)确定找到的地方是正确的后,我们先不去研究细节,继续了解整个框架的实现。


找一下这个方法用的地方,发现并不算多,有些地方都可以直接排除掉,这样就可以画出下面这个主流程图。



  • 在 onDraw()里面的第一个方法就会根据属性判断是或否调用 startMarquee()方法。


  • 在 statMarquee()方法里会初始化一个 Textview 的内部类 Marquee()。


  • 初始化 mMarquee 后就调用.start()方法。


  • 这个方法里会根据传进来 TextView 对象,也就是它自己的一些属性值,初始化一些跑马灯所需要的数据值,以供父类使用。


  • 初始化值后调用 TextView 的 invalidate()方法。


  • 之后会触发 onDraw()方法,onDraw()方法里会根据 mMarquee 的属性值进行移动画布。


3.2 Marquee


第一节只是分析了大体的流程,但是我们看到 TextView 只是一个使用方,跑马灯真正的业务实现是在一个叫做 Marquee 的内部类里,还记得上面我们留了一个坑吗,在 startMarquee 里会调用 mMarquee.start 方法,这个时候就已经调到内部类里面的方法了,我们来看看 start 方法里都做了什么。



2)第 10 行设置偏移变量为 0.0f(1)第 9 行设置 mStatus 为 MARQUEE_STARTING,表示这是第一次滑动。


3)第 11 行设置文字的实际的宽度复制给 textWidth,其实也很简单,就是整个 TextView 控件的宽度减去左边和右边的 padding 区域。


4)第 14 行设置滑动的的间距 gap,从这里可以看出 Android 默认跑马灯的滑动间距是文字长度的三分之一。


5)第 16 行设置最大滑动距离 mMaxScroll,其实也就是字的宽度加上 gap。


6)第 21 行设置好所有初始变量后调用 textView.invalidate();触发 textview 的 ondraw 方法。这个也是我们平时最常用的触发 view 刷新的刷新的方法,这个是在主线程刷新所有只要用 invalidate 就可以了。


7)第 22 行设置 Choreographer 监听事件,用于后续继续控制动画。


简单的画一个 TextView 和 TextView.Marquee 和 Choreographer 的关系图。



TextView: 绘制跑马灯的实体,主要在 ondraw 里面初始化内部类 TextView.Marquee。


TextView.Marquee:用来管理跑马灯的偏向值 onScroll,同时不停的调用 invalidate 方法触发 TextVIew 的 ondraw 方法,用来绘制显示文案。


Choreographer:系统的一个帧回调方法,每一帧都会提供回调给 Marquee 用于触发 view 的刷新,保证动画的平滑性,后面会详细说下 Choreographer。


3.3 Choreographer


Choreographer 是一个系统的方法,我们先来看下它在 Google 官方的定义是什么;


Coordinates the timing of animations, input and drawing.......Applications typically interact with the choreographer indirectly using higher level abstractions in the animation framework or the view hierarchy. Here are some examples of things you can do using the higher-level APIs.


翻译过来就是:这个类是一个监听系统的垂直帧信号,在每一帧都会回调。它是一个底层 api,如果你是在做 Animation 之类的事情,请使用更高级的 api。


理解一下:就是一般不建议你用,我猜想可能是因为它回调过于频繁,可能会影响性能。它的回调次数也跟当前手机屏幕的刷新率有关,对于一个 60 刷新率的系统来说 这个 postFrameCallback 会在 1000/60 = 16.6 毫秒回调一次,如果是 120 刷新率的话就是 1000/120 = 8.3 毫秒就回调一次。所以在综上所述,这个类的回调不能做耗时的工作。


简单看下 choreographer 的实现原理,里面会监听一个叫做 DisplayEventReceiver 的系统 Receiver,这个 Receiver 会跟底层的 SurfaceFlinger 的 Connection 连接,SurfaceFlinger 会实时发 sync 信号,通过 onVsync 回调上来。



重点我们来看看 Marquee 在 postFrameCallback 里做了哪些事情;在 Choreographer 里面会调用一个叫做 Tick 的方法,就是用来计算偏向值的,我们对这个方法来深入分析下。



1)前 3 行定义了 mPixelsPerMs 看着是不是很熟悉,其实就是定义了滑动的速度,30dp 对应的 px 值/1000ms。也就是 android 跑马灯默认的滑动速度是 30dp 每秒。


2)第 16 行,通过回调的当前时间 currentMs 和上一次回调的时间 mLastAnimationMs 算出差值 deltaMs 这里的单位是 ms。


3)第 18 行,通过 deltaMs 和 mPixelsPerMs 算出当前时间差所要移动的位移,复制给 mScroll。


4)第 20 行,如果位移大于最大值,就等于最大值。


5)第 26 行,调用了 invalidate 刷新 TextView。


既然前面初始化了 mMarquee 并且刷新了 Textview,接下来 TextView 的 ondraw 肯定是要用到 mMarquess 里面的数据进行绘制,ondraw 的方法比较长,这里我们找到了两处使用 mMarquee 的地方,分别是;




分别对两个地方都打上断点,发现只走了代码段二,那么我们重点来看看代码二里面做了什么(在通过代码已经搞不清路径的情况下,通过 debug 是最好的方式)。可以看到代码二里面是根据 getScroll()值,对画布做了水平移动,不停的回调移动,也就形成了动画。


总结一下,算出时间差值(currentMs - mLastAnimationMs),再用这个时间差值乘以 30dp 复制给 mScroll. 也就是每秒移动 30dp,最后再主动触发 TextView 的刷新。通过 postFrameCallback 不仅解决了持续触发跑马灯动画的问题,还保证了动画了流畅性。


我们给第二部分做一个结论:TextView 通过:marquee → Choreographer → mScroll 最终在 ondraw 里面绘制 TextView 的位置。


知道原理后我们接下来回到问题的本身,分析问题。


四、问题分析


通过第二节的原理分析后,在结合视频里面现象,我们知道动画发生了重置了,必然是 mScroll 发生了变化。


4.1 谁引发 mScroller 重置


再结合整个现象,可以猜测在点击"添加购物车"按钮后,某段代码重置了 getScroll()值,也就是 Marquee 的成员变量 mScroll。


有了猜测,顺着这个思路,我们来找找哪些地方把 mScroll 置为零了。通过 debug 向上追到头,发现是有人触发了 TextView 的 onMeasure 方法。



4.2 谁触发的 onMeasure


1)在 view 初始化的时候会走一遍完整的生命周期,如下图所示;



2)在调用 requestLayout()的方法,会触发 onMeasure。


并且当子 view 触发 requestLayout 的时候,会触发整个视图树的重绘,这个时候 ViewGroup 除了要完成自己的 measure 过程,还会遍历调用所有子元素的 measure 方法。以 framelayout 为例;



在第 35 行会遍历并触发所有子 view 的 measure 方法。基于以上的 2 个事实我们提出以下一个假设。



子 view A 调用了 requestLayout 方法,viewgroup 发生了重绘,触发了子 view B 的 onMeasure()方法 。


那么目标就很明确了,视频里另外一个显示数字增加的子 view 和它唯一做的一件事 setText。


4.3 怎么触发 onMeasure 的


前面的猜想就是我们可能是在 setText 里面触发了 requestLayout 方法,那么想验证就简单了:


  • 在 setText 的入口方法打上断点 ;


  • 在所有调用 requestLayout 的地方都打上断点。


果然不出所料,沿着 setText 方法 debug 下去有调用 requestLayout 方法,这个时候尝试画出流程图。



去掉所有其他逻辑,我们发现它会判断当前布局方式是 wrap_content 去执行不一样的逻辑。看了下“购物车”按钮就是 wrap_content 属性,所以会走 requestLayout,继而会触发跑马灯的重绘。


五、问题解决


通过问题分析的结论,那么解决方案就显而易见了,把“购物车”按钮的属性改成非 wrap_content 再次尝试,果然跑马灯就不会再次重绘了,修改代码如下:



六、总结


经过此次分析我们来以迷宫为例子总结一下收获:


对于源码现象的分析需要依赖自己对 Android 知识的熟练掌握,并精准的猜想作为前提。Android 知识更像是走迷宫的指南针。


debug 可以作为排除一些错误的支线,直接找到正确的主线,更像是在迷宫里加上几个锚点,进行试错。


多画流程图可以加深自己的框架的理解,流程图更像是迷宫的地图,帮助你少走弯路。


作者:vivo 官网商城开发团队-HouYutao

发布于: 刚刚阅读数: 2
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
跑马灯带你深入浅出TextView的源码世界_android_vivo互联网技术_InfoQ写作平台