Android实现文字滚动播放效果的示例代码

一、项目介绍

1. 背景与意义

在许多资讯类、新闻类以及企业展示类 android 应用中,文字滚动播放(也称为跑马灯效果、公告栏效果)是非常常见的 ui 交互方式,用于持续不断地展示公告、新闻标题、提示信息等。在影视推荐 app、地铁公交查询、股市行情等场景中,文字滚动不仅能够节省屏幕空间,还能吸引用户注意力,使信息传递更具张力。本项目通过原生 android 技术,从零开始实现一套高性能、高度可定制、支持多种滚动方向与动画曲线的文字滚动播放控件,满足各类复杂需求。

2. 功能需求

  1. 文字内容设定:可动态设置一段或多段文字;

  2. 滚动模式:支持水平垂直两种滚动方向;

  3. 滚动方式:支持循环播放与单次播放,支持往返式无缝衔接

  4. 速度与间隔:可自定义滚动速度与两次滚动之间的停留间隔;

  5. 动画曲线:内置线性加速减速等插值器;

  6. 触摸交互:支持用户触摸滑动暂停与手动拖动;

  7. 资源释放:activity/fragment 销毁时正确释放动画与 handler,防止内存泄露;

  8. 可定制样式:文字大小、颜色、字体、背景等可通过 xml 属性或代码动态配置;

  9. 高性能:在长列表、多实例场景下,保持平滑的 60fps。

3. 技术选型

  • 语言:java

  • 最低 sdk:api 21(android 5.0)

  • 核心组件

    • textview或自定义view

    • 属性动画(objectanimator

    • valueanimator+canvas.drawtext()(高级方案)

    • handler+runnable(基础方案)

    • scroller/overscroller(平滑滚动)

  • 布局容器:通常使用framelayoutrelativelayoutconstraintlayout承载自定义控件

  • 开发工具:android studio 最新稳定版

二、相关知识详解

1. android 自定义 view 基础

  • onmeasure():测量控件宽高;

  • onsizechanged():尺寸变化回调,初始化绘制区域;

  • ondraw(canvas):绘制文字与背景;

  • 自定义属性:通过res/values/attrs.xml定义,可在 xml 中使用;

  • 硬件加速:确保动画平滑,必要时关闭硬件加速进行文字阴影绘制。

2. 属性动画与插值器

  • objectanimator.offloat(view, "translationx", start, end)

  • valueanimator.offloat(start, end),在addupdatelistener中更新位置;

  • 常用插值器:linearinterpolatoraccelerateinterpolatordecelerateinterpolatoracceleratedecelerateinterpolator

  • 自定义插值器:实现timeinterpolator

3. handler 与 runnable

  • 适合循环式轻量调度;

  • postdelayed()控制滚动间隔;

  • activity / fragment 销毁时要removecallbacks()防止内存泄漏。

4. scroller / overscroller

  • 实现流畅的物理滚动效果;

  • scroller.startscroll()fling()

  • computescroll()中,调用scroller.computescrolloffset()scrollto(x, y)

  • 适用于需要手势拖动与惯性滚动的场景。

5. textview 与 canvas.drawtext()

  • 对于简单场景,可直接移动textview

  • 对于更高性能与自定义效果,可在view.ondraw()canvas.drawtext(),并通过canvas.translate()实现滚动。

三、项目实现思路

  1. 确定实现方案

    • 方案一(基础):在布局中使用单个textview,通过objectanimatortranslateanimation移动textviewtranslationx/y

    • 方案二(自定义view):继承view,在ondraw()中绘制文字并控制文字绘制位置偏移,实现更灵活的动画与样式控制。

  2. 基础流程

    • 初始化:读取 xml 属性或通过 setter 获取文字内容、字体、颜色、速度等配置;

    • 测量与布局:在onmeasure()计算文字宽度/高度,确定 view 大小;

    • 启动动画:在onattachedtowindow()startscroll()中,启动滚动动画;

    • 滚动控制:使用valueanimatorobjectanimator不断更新文字的偏移量;

    • 循环与间隔:监听动画结束(animatorlistener),在回调中postdelayed()再次启动,以实现间隔播放;

    • 资源释放:在ondetachedfromwindow()中取消所有动画与 handler 调用。

  3. 多方向与多模式

    • 水平滚动:初始偏移为viewwidth,终点为-textwidth

    • 垂直滚动:初始偏移为viewheight,终点为-textheight

    • 往返模式:设置repeatmode = valueanimator.reverse

    • 无缝衔接:使用两行文本交替滚动,一行滚出,一行紧随其后。

  4. 触摸暂停与拖动

    • 在自定义 view 中重写ontouchevent(),在action_downpause()动画,action_move时调整偏移,action_upresume()fling()

四、完整整合版代码

4.1 attrs.xml



    
        
        
        
        
        
            
            
        
        
        
            
            
        
        
        
    

4.2 布局文件



 
    

4.3 自定义控件:marqueetextview.java

package com.example.marquee;
 
import android.animation.animator;
import android.animation.objectanimator;
import android.animation.timeinterpolator;
import android.content.context;
import android.content.res.typedarray;
import android.graphics.canvas;
import android.graphics.paint;
import android.text.textutils;
import android.util.attributeset;
import android.view.view;
import androidx.interpolator.view.animation.linearoutslowininterpolator;
import com.example.r;
 
public class marqueetextview extends view {
 
    // ========== 可配置属性 ==========
    private string text;
    private int textcolor;
    private float textsize;
    private float speed;               // px/s
    private int direction;             // 0: horizontal, 1: vertical
    private long repeatdelay;          // ms
    private int repeatmode;            // objectanimator.restart or reverse
    private boolean loop;              // 是否循环
    private timeinterpolator interpolator;
 
    // ========== 绘制相关 ==========
    private paint paint;
    private float textwidth, textheight;
    private float offset;              // 当前滚动偏移
 
    // ========== 动画 ==========
    private objectanimator animator;
 
    public marqueetextview(context context) {
        this(context, null);
    }
 
    public marqueetextview(context context, attributeset attrs) {
        this(context, attrs, 0);
    }
 
    public marqueetextview(context context, attributeset attrs, int defstyle) {
        super(context, attrs, defstyle);
        initattributes(context, attrs);
        initpaint();
    }
 
    private void initattributes(context context, attributeset attrs) {
        typedarray a = context.obtainstyledattributes(attrs, r.styleable.marqueetextview);
        text = a.getstring(r.styleable.marqueetextview_mtv_text);
        textcolor = a.getcolor(r.styleable.marqueetextview_mtv_textcolor, 0xff000000);
        textsize = a.getdimension(r.styleable.marqueetextview_mtv_textsize, 16 * getresources().getdisplaymetrics().scaleddensity);
        speed = a.getfloat(r.styleable.marqueetextview_mtv_speed, 50f);
        direction = a.getint(r.styleable.marqueetextview_mtv_direction, 0);
        repeatdelay = a.getint(r.styleable.marqueetextview_mtv_repeatdelay, 500);
        repeatmode = a.getint(r.styleable.marqueetextview_mtv_repeatmode, objectanimator.restart);
        loop = a.getboolean(r.styleable.marqueetextview_mtv_loop, true);
        int interpres = a.getresourceid(r.styleable.marqueetextview_mtvinterpolator, android.r.interpolator.linear);
        interpolator = android.view.animation.animationutils.loadinterpolator(context, interpres);
        a.recycle();
 
        if (textutils.isempty(text)) text = "";
    }
 
    private void initpaint() {
        paint = new paint(paint.anti_alias_flag);
        paint.setcolor(textcolor);
        paint.settextsize(textsize);
        paint.setstyle(paint.style.fill);
 
        // 计算文字尺寸
        textwidth = paint.measuretext(text);
        paint.fontmetrics fm = paint.getfontmetrics();
        textheight = fm.bottom - fm.top;
    }
 
    @override
    protected void onmeasure(int widthmeasurespec, int heightmeasurespec) {
        int desiredw = (int) (direction == 0 ? getsuggestedminimumwidth() : textwidth + getpaddingleft() + getpaddingright());
        int desiredh = (int) (direction == 1 ? getsuggestedminimumheight() : textheight + getpaddingtop() + getpaddingbottom());
 
        int width = resolvesize(desiredw, widthmeasurespec);
        int height = resolvesize(desiredh, heightmeasurespec);
        setmeasureddimension(width, height);
    }
 
    @override
    protected void onattachedtowindow() {
        super.onattachedtowindow();
        startscroll();
    }
 
    @override
    protected void ondetachedfromwindow() {
        super.ondetachedfromwindow();
        if (animator != null) animator.cancel();
    }
 
    private void startscroll() {
        if (animator != null && animator.isrunning()) return;
 
        float start, end, distance;
        if (direction == 0) {
            // 水平滚动:从右侧外开始,到左侧外结束
            start = getwidth();
            end = -textwidth;
            distance = start - end;
        } else {
            // 垂直滚动:从底部外开始,到顶部外结束
            start = getheight();
            end = -textheight;
            distance = start - end;
        }
        long duration = (long) (distance / speed * 1000);
 
        animator = objectanimator.offloat(this, "offset", start, end);
        animator.setinterpolator(interpolator);
        animator.setduration(duration);
        animator.setrepeatcount(loop ? objectanimator.infinite : 0);
        animator.setrepeatmode(repeatmode);
        animator.setstartdelay(repeatdelay);
        animator.addlistener(new animator.animatorlistener() {
            @override public void onanimationstart(animator animation) { }
            @override public void onanimationend(animator animation) { }
            @override public void onanimationcancel(animator animation) { }
            @override public void onanimationrepeat(animator animation) { }
        });
        animator.start();
    }
 
    public void setoffset(float value) {
        this.offset = value;
        invalidate();
    }
 
    public float getoffset() { return offset; }
 
    @override
    protected void ondraw(canvas canvas) {
        super.ondraw(canvas);
        if (direction == 0) {
            // 水平
            float y = getpaddingtop() - paint.getfontmetrics().top;
            canvas.drawtext(text, offset, y, paint);
        } else {
            // 垂直
            float x = getpaddingleft();
            canvas.drawtext(text, x, offset - paint.getfontmetrics().top, paint);
        }
    }
 
    // ==== 可添加更多 api:pause(), resume(), settext(), setspeed() 等 ====
}

五、代码解读

  1. 自定义属性

    • attrs.xml中定义了文字内容、颜色、大小、速度、方向、间隔、循环模式、插值器等属性;

    • 在控件构造函数中通过typedarray读取并初始化。

  2. 测量逻辑

    • onmeasure()根据滚动方向决定控件的期望宽高;

    • 对水平滚动,宽度由父容器决定,高度由文字高度加内边距决定;

    • 对垂直滚动,反之亦然。

  3. 绘制逻辑

    • ondraw()中,根据当前offset绘制文字;

    • 使用paint.measuretext()paint.getfontmetrics()计算文字宽高与基线。

  4. 动画逻辑

    • startscroll()中,计算从起始位置到结束位置的距离与时长;

    • 使用objectanimatoroffset属性做动画;

    • 设置插值器、循环次数、循环模式与延时;

    • ondetachedfromwindow()中取消动画,防止泄漏。

  5. 可扩展性

    • 暴露settext()setspeed()pause()resume()等方法;

    • 监听用户触摸,支持滑动暂停与手动拖动;

    • 对接 recyclerview、listview,实现列表内多个跑马灯。

六、项目总结与拓展

  1. 项目收获

    • 深入掌握自定义 view 的测量、绘制与属性动画;

    • 学会在自定义控件中优雅管理动画生命周期;

    • 掌握跑马灯效果的核心算法:偏移量计算与时长转换;

    • 学会如何通过 xml 属性实现高度可配置化。

  2. 性能优化

    • 确保硬件加速开启,避免文字绘制卡顿;

    • 对于超长文字或多列文字,可使用staticlayout分段缓存;

    • 结合choreographer精确控制帧率;

  3. 高级拓展

    • 触摸控制:拖动暂停、手动快进快退;

    • 多行跑马灯:支持同时滚动多行文字,或背景渐变;

    • 动态数据源:与网络或数据库结合,实时更新滚动内容;

    • jetpack compose 实现:基于canvasmodifier.offset()的 compose 方案;

以上就是android实现文字滚动播放效果的示例代码的详细内容,更多关于android文字滚动播放的资料请关注代码网其它相关文章!

发布于 2025-05-07 21:57:36
分享
海报
139
上一篇:WITH在MYSQL中的用法示例详解 下一篇:Springboot整合ip2region实现用户ip归属地获取
目录

    忘记密码?

    图形验证码