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
  • focusable 和 focusableInTouchMode 在不同 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 重绘都简单。代价是不能改透明度、缩放这些复杂效果,单纯滚文字够用了。

暂停和恢复 :ObjectAnimator 有 pause() 和 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。

相关推荐
拥小抱9 小时前
Compose 知识点
android
文章永久免费只为良心9 小时前
Web应用安全与防护_第三章_日志文件包含_从一道 CTFshow 题目讲透「日志投毒型 LFI → RCE」:原理、时序陷阱与全变种攻防手册
android·网络安全·渗透测试·漏洞危害·漏洞描述·日志投毒·红队指南
AIsoft_868910 小时前
学术讲座录音如何整理?录音APP可行吗
android·iphone
Patrick在香港12 小时前
一个斜杠,把毫秒变成十几秒
android·数据库·python·性能优化·正则表达式·回溯·redos
海雅达手持终端PDA14 小时前
工业手持终端的“去手机化”演进:看海雅达如何以“同轴扫摄”终结工程妥协,重塑工业PDA新范式
android·物联网·智能手机·硬件工程·能源·制造·iot
booboosui14 小时前
删掉一个住在本机的 agent:8 处要清,顺序不能反
android
hcgeng15 小时前
Android获取崩溃日志
android
aqi0015 小时前
AI编程让鸿蒙开发电子书APP更加快捷
android·华为·harmonyos·鸿蒙·harmony
GY-9315 小时前
【Android】Android Studio 中的协程
android