乐趣区

关于android:跑马灯带你深入浅出TextView的源码世界

一、背景

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

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

退出移动版