一、背景

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

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