草庐IT

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

vivo互联网 2023-03-28 原文

一、背景

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

但是大家平时开发过程中肯定会遇到这样或那样的小问题,通过百度、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

有关跑马灯带你深入浅出TextView的源码世界的更多相关文章

  1. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  2. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  3. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  4. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  5. Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理) - 2

    快速导航(持续更新中…)Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理)Cesium源码解析二(metadataAvailability的含义)Cesium源码解析三(metadata元数据拓展中行列号的分块规则解析)Cesium源码解析四(Quantized-Mesh(.terrain)格式文件在CesiumJS和UE中加载情况的对比)目录1.前言2.本篇的由来3.terrain文件的加载3.1更新环境3.2更新和执行渲染命令3.3数据优化3.4结束当前帧4.总结1.前言  目前市场上三维比较火的实现方案主要有两种,b/s的方案主要是Cesium,c/s的方案主要是u

  6. 停车系统源码-基于springboot+uniapp开源项目 - 2

    Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统,支持封闭车场和路边车场,支持微信支付宝多种支付渠道,支持多种硬件,涵盖了停车场管理系统的所有基础功能。技术栈Springboot,MybatisPlus,Beetl,Mysql,Redis,RabbitMQ,UniApp功能云端功能序号模块功能描述1系统管理菜单管理配置系统菜单2系统管理组织管理管理组织机构3系统管理角色管理配置系统角色,包含数据权限和功能权限配置4系统管理用户管理管理后台用户5系统管理租户管理多租户管理6系统管理公众号配置租户公众号配置7系统管理操作日志审计日志8系统

  7. 打通源码,高效定位代码问题|云效工程师指北 - 2

    大家好,我叫胡飞虎,花名虎仔,目前负责云效旗下产品Codeup代码托管的设计与开发。代码作为企业最核心的数据资产,除了被构建、部署之外还有更大的价值。为了帮助企业和团队挖掘更多源代码价值以赋能日常代码研发、运维等工作,云效代码团队在大数据和智能化方向进行了一系列的探索和实践(例如代码搜索与推荐),本文主要介绍我们如何通过直接打通源代码来提高研发与运维效率。随着微服务架构的流行,一个业务流程需要多个微服务共同完成。一旦出现问题,运维人员在面对数量多、调用链路复杂的情况下,很难快速锁定导致问题发生的罪魁祸首:代码。为了提高排查效率,目前常见的解决方案是:链路跟踪+日志分析工具相结合。即通过链路跟踪

  8. ChatGPT教程之深入了解魔术背后的技术 - 2

    解开谜团:深入探索ChatGPT的技术奇迹。ChatGpt无处不在,无论是在播客、博客、YouTube还是社交媒体上。当我注意到这项新技术如此受欢迎时,我决定试一试,我被震惊了!有很多关于ChatGpt及其魔力的博客,但在这篇博客中,我将深入探讨其内部技术及其工作原理!ChatGpt简介根据OpenAI,ChatGpt被描述为:“我们训练了一个名为ChatGpt的模型,它以对话方式进行交互。对话格式使ChatGpt可以回答后续问题、承认错误、挑战不正确的前提并拒绝不适当的请求。ChatGPT是InstructGPT的兄弟模型,它经过训练可以按照提示中的说明进行操作并提供详细的响应。”OpenA

  9. Android Studio开发之使用内容组件Content获取通讯信息讲解及实战(附源码 包括添加手机联系人和发短信) - 2

    运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid

  10. Unity3D : 本地坐标系,世界坐标系,和TransformPoint,TransformVector,TransformDirection的区别 - 2

    目录一、世界坐标系与本地坐标系二、srcGameObject.transform.TransformPoint(Vector3 vec)三、srcGameObject.transform.TransformVector(Vector3 vec)四、srcGameObject.transform.TransformDirection(Vector3 vec)五:示例一、世界坐标系与本地坐标系    世界坐标很好理解,就是模型的transform.position,通常在无父物体的情况下,创建出来的模型默认位置就是世界坐标系的原点。    每个物体都有自身的坐标系,此坐标系就是本地坐标系。本地坐标

随机推荐