Android原生TextView的跑马灯效果实现

KaelLi 2021年8月5日19:45:48
评论
6,6211

文字的跑马灯效果在移动端开发中是一个比较常见的需求场景,一般在单行的空间下显示比较重要的文案,若文案比较长单行不足以全部展示,但是产品规划又不允许把多余的文字简单的隐藏或者用...来代替,这时候使用跑马灯的形式可以一次把文案全部展现,同时也能很好的吸引用户的注意力。

其实跑马灯对于一个有一定经验的Android开发者来说,应该算是比较常见而容易的东西了。长期没有用、最近又遇到类似的需求,就顺便又重新了解了一下跑马灯,写一点文字算是一个简单的记录吧。简单来说,Android原生TextView的跑马灯效果使用起来非常简单,但也存在着一些坑。

比如,想要实现下面这样的一个列表,每一项都有一个跑马灯TextView,用大部分开发者常用的传统方式,可能就实现不了,但实际上你不需要自定义控件,使用原生TextView完全可以实现这样的小国:

Android原生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方法虽然也不能说是完美,但确实坑要少很多。

最后,不管是使用焦点的方法还是选中的方法,总有一些坑无法避免,而且也无法满足更多精细化的需求,所以自定义一个跑马灯控件,其实是非常有必要的,这样一个控件,日后我会把它的源码放出来的。

KaelLi
  • 本文由 发表于 2021年8月5日19:45:48
  • 转载请务必保留本文链接:https://www.kaelli.com/48.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: