文字的跑马灯效果在移动端开发中是一个比较常见的需求场景,一般在单行的空间下显示比较重要的文案,若文案比较长单行不足以全部展示,但是产品规划又不允许把多余的文字简单的隐藏或者用...来代替,这时候使用跑马灯的形式可以一次把文案全部展现,同时也能很好的吸引用户的注意力。
其实跑马灯对于一个有一定经验的Android开发者来说,应该算是比较常见而容易的东西了。长期没有用、最近又遇到类似的需求,就顺便又重新了解了一下跑马灯,写一点文字算是一个简单的记录吧。简单来说,Android原生TextView的跑马灯效果使用起来非常简单,但也存在着一些坑。
比如,想要实现下面这样的一个列表,每一项都有一个跑马灯TextView,用大部分开发者常用的传统方式,可能就实现不了,但实际上你不需要自定义控件,使用原生TextView完全可以实现这样的小国:
想要让TextView支持跑马灯效果,设置起来相当简单,只需要在布局文件里给TextView添加几个属性就可以了:
<TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="marquee" android:singleLine="true" android:marqueeRepeatLimit="marquee_forever" android:textColor="@color/black" android:textSize="16sp" />
这里面只有ellipsize和singleLine两个属性是必须的,不设置这2个属性是不能实现跑马灯的。而marqueeRepeatLimit则是设置了跑马灯的次数,自身有默认值所以可以不进行设置,查看TextView的源码的话可以看到默认值是3,而marquee_forever则代表正无穷。
正常来说,这样的设置已经足够了, 然后在代码里对这个TextView进行setText的时候,文字长度一定要够长,长到单行显示不完,那么顺利的话,你就已经可以看到跑马灯了。然而,事情往往不会那么顺利,原生的TextView要想顺利跑马灯,是有一些先决条件的,我们可以去看看源码,在TextView的源码里,是有一个名为startMarquee的方法的:
private void startMarquee() { // Do not ellipsize EditText if (getKeyListener() != null) return; if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) { return; } if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && getLineCount() == 1 && canMarquee()) { if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) { mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE; final Layout tmp = mLayout; mLayout = mSavedMarqueeModeLayout; mSavedMarqueeModeLayout = tmp; setHorizontalFadingEdgeEnabled(true); requestLayout(); invalidate(); } if (mMarquee == null) mMarquee = new Marquee(this); mMarquee.start(mMarqueeRepeatLimit); } }
只有这一大串判断都通过了,才会走最后的mMarquee.start()方法。前面2个判断,第一个条件,只要当前TextView不是EditText则getKeyListener()的结果基本上都是null,不用太过关心,第二个条件则是在判断是否有必要进行跑马灯,在这个判断里,如果展示全部文字所需要的宽度跟TextView的宽度十分接近那么也不会有跑马灯效果,那么只要文字够多够长且单行显示,那么这个条件也就不必关心了。
那么就只剩下第三个判断了,这个判断中条件比较多,对于mMarquee == null || mMarquee.isStopped()这个判断没有什么好说的,getLineCount() == 1这个就是所谓的单行限制了,而canMarquee()基本上还是在对横向上的宽度空间进行判断,只有剩下的isFocused() || isSelected()才是比较关键的地方,也就是说,在其他条件都OK的情况下,isFocused()和isSelected()必须至少满足一个,才可以进行跑马灯。
isFocused()顾名思义,是在判断当前TextView是否有焦点,一个View想要有焦点,首先它自身需要满足一个条件,就是它可以有焦点,在布局文件里我们可以添加这样两行属性:
android:focusable="true" android:focusableInTouchMode="true"
然后通过View的requestFocus()方法就能获取到焦点了。
然而焦点这个东西是比较复杂的,很多时候,一个拥有较多控件的页面布局,你很难确保需要跑马灯的TextView能顺利获取到焦点,而且有的控件天生就容易获取焦点(如EditText)。另外,比如你在一个列表(ListView或RecyclerView)里,每个列表项都需要有一个TextView有跑马灯效果,那么单凭设置焦点的方式是不可行的:一个页面哪来10几个焦点给你们每个TextView都分一个呢!
而isSelected()这个方法其实我们也并不陌生,一般来说,我们会给一个控件写一个xml的drawable文件,里面会指明默认情况下、selected状态下控件的背景,然后通过设置setSelected的方法改变控件的isSelected状态,从而实现背景的切换。与获取焦点的方法相比,还是去满足isSelected()条件更容易一些,因为一个页面里每个控件都可以去改变selected状态而不会产生任何冲突,而且正好TextView也重写了setSelected方法:
@Override public void setSelected(boolean selected) { boolean wasSelected = isSelected(); super.setSelected(selected); if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (selected) { startMarquee(); } else { stopMarquee(); } } }
我们十分“惊喜”的看到,TextView的setSelected方法,居然直接就调用了startMarquee方法!好吧,我也不知道为什么Android官方这里是这么设计的,也许是意识到焦点的问题不好解决(其实很多时候是无解的),所以故意在这里留下了一道方便之门?
所以,对比之下,使用TextView的setSelected方法,要比去获取焦点的方式更稳妥靠谱一些。而且使用焦点获取法,会遇到更多的坑,动不动就要去自定义TextView来重写一些跟焦点有关的方法,甚至有些时候你还得去自定义父控件,随着页面复杂度的提高和需求的变化,这个坑是永远填不完的。使用setSeleted方法虽然也不能说是完美,但确实坑要少很多。
最后,不管是使用焦点的方法还是选中的方法,总有一些坑无法避免,而且也无法满足更多精细化的需求,所以自定义一个跑马灯控件,其实是非常有必要的,这样一个控件,日后我会把它的源码放出来的。
评论