Android 跑马灯:从一行 XML 到自定义控件

文章目录

做歌单、新闻标题、消息列表这种 UI 的时候,经常会遇到歌名 / 标题太长塞不下的问题。

最省事的办法是让它循环滚动展示,也就是跑马灯(marquee)。

Android 上做跑马灯的路子有三条,按使用频率排大概是:

  1. XML 直接配属性(系统原生)
  2. 自己写个 TextView 子类
  3. ObjectAnimator 自己驱动

下面一个个说。


一、直接配属性

最基础的写法,四个属性搞定:

xml 复制代码
<TextView
    android:layout_width="180dp"
    android:layout_height="wrap_content"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:marqueeRepeatLimit="marquee_forever"
    android:text="这是一段非常长的歌曲名称" />

四个属性各自的作用:

  • singleLine="true" 强制单行,多行根本没法滚
  • ellipsize="marquee" 启用 marquee 截断策略
  • marqueeRepeatLimit 控制滚几次。默认只滚 3 次,很多人不知道这个默认值,
    写出来发现滚三圈就停了以为代码有问题,其实是默认值就是这样。
    设成 marquee_forever 才是无限循环
  • focusable / focusableInTouchMode 允许控件获得焦点(这个是关键的,下文讲)

这套配置在页面只有一个 TextView 的时候是能跑的,

但放进 RecyclerView 的 item 里基本都会失效------跑马灯要求 TextView 拿到焦点才动

而列表 item 的焦点归属是不稳定的,焦点可能被其他 EditText、Button 抢走。

所以光靠系统属性这条路,列表场景基本走不通。

一些细节

  • 文字宽度必须超过 TextView 的宽度,否则根本没东西可滚(会被识别成不需要 marquee)
  • marqueeRepeatLimit 的取值:可以是具体数字(比如 "5" 表示滚 5 次),
    也可以是 marquee_forever
  • focusablefocusableInTouchMode 在不同 Android 版本下行为略有差异,
    一般两个都加上比较稳

二、自定义 TextView

既然问题是"拿不到焦点",那就让 TextView 永远认为自己有焦点

整个自定义类只需要重写一个方法:

java 复制代码
public class MarqueeTextView extends AppCompatTextView {

    public MarqueeTextView(Context context) { super(context); }
    public MarqueeTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MarqueeTextView(Context c, @Nullable AttributeSet a, int d) {
        super(c, a, d);
    }

    @Override
    public boolean isFocused() {
        return true;
    }
}

为什么这么写

几个细节值得说一下:

继承 AppCompatTextView 而不是 TextView :兼容性更好,能用上 AppCompat 主题。

如果不介意主题兼容性,也可以直接继承 TextView

三个构造方法都要重写 :因为系统在 XML 加载时会调两个参数的那个,代码里 new 出来时会调一个参数的,主题里指定 style 时会调三个参数的。

少写一个就会出现"XML 里能用、代码里 new 就崩"的诡异 bug。

isFocused() 为什么稳定 :这个方法返回的是 View 自己的状态,不会和别人抢焦点资源。系统每次想"这个 TextView 是不是有焦点"的时候都问它,

它每次都说"是",所以 marquee 就会一直跑。

XML 里怎么用

<TextView> 换成这个类,属性保持原样:

xml 复制代码
<com.example.myqqmusic.MarqueeTextView
    android:layout_width="180dp"
    android:layout_height="wrap_content"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:marqueeRepeatLimit="marquee_forever"
    android:text="..." />

Adapter 一行不用动。

实际跑起来

放两张对比图看看:

  1. 使用TextView:
  2. 使用自定义View:

列表里每一项的歌名都会滚起来,刚滑进屏幕的那一项也是。

滑出去看不见的 item 实测下来系统会自动停掉渲染,不用担心后台偷偷吃 GPU。


三、ObjectAnimator 自己驱动

如果业务上需要"播放时才滚、暂停就停",前面两种方案做不到------它们都是连续滚动的,

没法在某个时刻暂停或者改变速度。这时候就要自己写动画了:

java 复制代码
private ObjectAnimator animator;

public void startMarquee(TextView tv) {
    tv.setSingleLine(true);
    tv.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
    int textWidth = tv.getMeasuredWidth();
    int viewWidth = tv.getWidth();
    int distance = viewWidth - textWidth;
    if (distance >= 0) return;   // 文字不够长就没必要滚

    animator = ObjectAnimator.ofFloat(tv, "translationX", 0, distance);
    animator.setDuration(8000);
    animator.setRepeatMode(ObjectAnimator.RESTART);
    animator.setRepeatCount(ObjectAnimator.INFINITE);
    animator.setInterpolator(new LinearInterpolator());
    animator.start();
}

public void stopMarquee() {
    if (animator != null) animator.cancel();
    tv.setTranslationX(0);
}

几个细节

为什么用 LinearInterpolator :默认的 AccelerateDecelerateInterpolator 是先加速后减速,文字滚起来会一卡一卡的,看着像 PPT。线性插值器就是匀速,最贴近系统 marquee 的感觉。

为什么是 translationX:直接平移文字最省事,比改 canvas、自己 measure 重绘都简单。代价是不能改透明度、缩放这些复杂效果,单纯滚文字够用了。

暂停和恢复ObjectAnimatorpause()resume(),可以在播放状态变化的时候控制动画:

java 复制代码
public void onPlayStateChanged(boolean isPlaying) {
    if (animator == null) return;
    if (isPlaying) animator.resume();
    else animator.pause();
}

注意一定要先 start() 过才能 pause(),没起跑的 animator 调用 pause 会被忽略。

代价

  • RecyclerView 里要自己处理可见性(哪些 item 在屏幕内才起动画)
  • 歌词那种极端长文本会卡,可以改用 HorizontalScrollView + Scroller 自己算滚动

大多数列表用不到这条,播放页歌词才用得上。


四、长文本方案:HorizontalScrollView + Scroller

ObjectAnimator 在文字非常长(比如一整首歌词)的时候会卡,因为每次文字平移都要触发整个 TextView 的重绘。

更稳的做法是把 TextView 嵌进 HorizontalScrollView,通过 Scroller 自己控制滚动位置:

java 复制代码
public class MarqueeHorizontalScrollView extends HorizontalScrollView {
    private Scroller scroller;

    public MarqueeHorizontalScrollView(Context c, AttributeSet a) {
        super(c, a);
        scroller = new Scroller(c, new LinearInterpolator());
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), 0);
            postInvalidate();
        }
    }

    public void startMarquee() {
        int textWidth = getChildAt(0).getWidth();
        int viewWidth = getWidth();
        if (textWidth <= viewWidth) return;
        scroller.startScroll(0, 0, viewWidth - textWidth, 0, 8000);
        invalidate();
    }
}

这种方式下文字不参与绘制,只滚动外层容器,性能最好。代价是代码量上来了,而且要把 TextView 嵌进 HorizontalScrollView,布局上稍微麻烦点。一般只有歌词这种极端长文本才需要。


五、总结

做 Android UI 的时候,遇到长文本塞不下,跑马灯是常规解法。

三种方案里:

  • 系统属性适合单独一个 TextView 的简单页面(版权声明、横幅提示)
  • 自定义 TextView(重写 isFocused)适合绝大多数列表场景------歌单、商品、消息预览
  • ObjectAnimator 适合需要和业务状态联动的场景------播放页歌词、暂停 / 恢复

做歌单这种 UI 的话,写个 5 行的 MarqueeTextView 就完事了,不用一开始就上 ObjectAnimator,也不用折腾 HorizontalScrollView

相关推荐
BoomHe13 小时前
Android Framework 文件应用移植到 AndroidStudio
android
传奇开心果编程14 小时前
【Jetpack Compose基础语法学与练】第8课 rememberSaveable,页面旋转/系统重建保留状态
android·学习·ui·kotlin·android jetpack
>Andre<14 小时前
UFS5.0标准中文全译·卷一:范围、术语与架构
android·linux·嵌入式硬件
事圆则缓15 小时前
Java 8 Lambda、Stream、Optional 与 Android 边界:从回调语法到运行时兼容
android·java
mmsx15 小时前
Android 地图十万要素不卡顿:空间网格 + 渐进式加载的移动端实践
android·大数据·opengl
Godikov17 小时前
Android 工业终端保活实战:前台服务 + 开机自启 + 更新自启的三重保障
android
字节暗面17 小时前
SO加固强度怎么量化?腾讯ACE与FairGuard静态分析实测
android·逆向
终端安全笔记19 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
字节暗面19 小时前
一次游戏安全SO静态分析记录:腾讯ACE与FairGuard加固强度对比
android·逆向工程·游戏加固