Android实现文字滚动播放效果的示例代码
一、项目介绍
1. 背景与意义
在许多资讯类、新闻类以及企业展示类 android 应用中,文字滚动播放(也称为跑马灯效果、公告栏效果)是非常常见的 ui 交互方式,用于持续不断地展示公告、新闻标题、提示信息等。在影视推荐 app、地铁公交查询、股市行情等场景中,文字滚动不仅能够节省屏幕空间,还能吸引用户注意力,使信息传递更具张力。本项目通过原生 android 技术,从零开始实现一套高性能、高度可定制、支持多种滚动方向与动画曲线的文字滚动播放控件,满足各类复杂需求。
2. 功能需求
文字内容设定:可动态设置一段或多段文字;
滚动模式:支持水平、垂直两种滚动方向;
滚动方式:支持循环播放与单次播放,支持往返式和无缝衔接;
速度与间隔:可自定义滚动速度与两次滚动之间的停留间隔;
动画曲线:内置线性、加速、减速等插值器;
触摸交互:支持用户触摸滑动暂停与手动拖动;
资源释放:activity/fragment 销毁时正确释放动画与 handler,防止内存泄露;
可定制样式:文字大小、颜色、字体、背景等可通过 xml 属性或代码动态配置;
高性能:在长列表、多实例场景下,保持平滑的 60fps。
3. 技术选型
语言:java
最低 sdk:api 21(android 5.0)
核心组件:
textview
或自定义view
属性动画(
objectanimator
)valueanimator
+canvas.drawtext()
(高级方案)handler
+runnable
(基础方案)scroller
/overscroller
(平滑滚动)
布局容器:通常使用
framelayout
、relativelayout
、constraintlayout
承载自定义控件开发工具: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
中更新位置;常用插值器:
linearinterpolator
、accelerateinterpolator
、decelerateinterpolator
、acceleratedecelerateinterpolator
;自定义插值器:实现
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()
实现滚动。
三、项目实现思路
确定实现方案
方案一(基础):在布局中使用单个
textview
,通过objectanimator
或translateanimation
移动textview
的translationx/y
。方案二(自定义view):继承
view
,在ondraw()
中绘制文字并控制文字绘制位置偏移,实现更灵活的动画与样式控制。
基础流程
初始化:读取 xml 属性或通过 setter 获取文字内容、字体、颜色、速度等配置;
测量与布局:在
onmeasure()
计算文字宽度/高度,确定 view 大小;启动动画:在
onattachedtowindow()
或startscroll()
中,启动滚动动画;滚动控制:使用
valueanimator
或objectanimator
不断更新文字的偏移量;循环与间隔:监听动画结束(
animatorlistener
),在回调中postdelayed()
再次启动,以实现间隔播放;资源释放:在
ondetachedfromwindow()
中取消所有动画与 handler 调用。
多方向与多模式
水平滚动:初始偏移为
viewwidth
,终点为-textwidth
;垂直滚动:初始偏移为
viewheight
,终点为-textheight
;往返模式:设置
repeatmode = valueanimator.reverse
;无缝衔接:使用两行文本交替滚动,一行滚出,一行紧随其后。
触摸暂停与拖动
在自定义 view 中重写
ontouchevent()
,在action_down
时pause()
动画,action_move
时调整偏移,action_up
时resume()
或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() 等 ==== }
五、代码解读
自定义属性
在
attrs.xml
中定义了文字内容、颜色、大小、速度、方向、间隔、循环模式、插值器等属性;在控件构造函数中通过
typedarray
读取并初始化。
测量逻辑
onmeasure()
根据滚动方向决定控件的期望宽高;对水平滚动,宽度由父容器决定,高度由文字高度加内边距决定;
对垂直滚动,反之亦然。
绘制逻辑
ondraw()
中,根据当前offset
绘制文字;使用
paint.measuretext()
和paint.getfontmetrics()
计算文字宽高与基线。
动画逻辑
startscroll()
中,计算从起始位置到结束位置的距离与时长;使用
objectanimator
对offset
属性做动画;设置插值器、循环次数、循环模式与延时;
在
ondetachedfromwindow()
中取消动画,防止泄漏。
可扩展性
暴露
settext()
、setspeed()
、pause()
、resume()
等方法;监听用户触摸,支持滑动暂停与手动拖动;
对接 recyclerview、listview,实现列表内多个跑马灯。
六、项目总结与拓展
项目收获
深入掌握自定义 view 的测量、绘制与属性动画;
学会在自定义控件中优雅管理动画生命周期;
掌握跑马灯效果的核心算法:偏移量计算与时长转换;
学会如何通过 xml 属性实现高度可配置化。
性能优化
确保硬件加速开启,避免文字绘制卡顿;
对于超长文字或多列文字,可使用
staticlayout
分段缓存;结合
choreographer
精确控制帧率;
高级拓展
触摸控制:拖动暂停、手动快进快退;
多行跑马灯:支持同时滚动多行文字,或背景渐变;
动态数据源:与网络或数据库结合,实时更新滚动内容;
jetpack compose 实现:基于
canvas
与modifier.offset()
的 compose 方案;
以上就是android实现文字滚动播放效果的示例代码的详细内容,更多关于android文字滚动播放的资料请关注代码网其它相关文章!