124人参与 • 2025-05-06 • Android
在许多资讯类、新闻类以及企业展示类 android 应用中,文字滚动播放(也称为跑马灯效果、公告栏效果)是非常常见的 ui 交互方式,用于持续不断地展示公告、新闻标题、提示信息等。在影视推荐 app、地铁公交查询、股市行情等场景中,文字滚动不仅能够节省屏幕空间,还能吸引用户注意力,使信息传递更具张力。本项目通过原生 android 技术,从零开始实现一套高性能、高度可定制、支持多种滚动方向与动画曲线的文字滚动播放控件,满足各类复杂需求。
文字内容设定:可动态设置一段或多段文字;
滚动模式:支持水平、垂直两种滚动方向;
滚动方式:支持循环播放与单次播放,支持往返式和无缝衔接;
速度与间隔:可自定义滚动速度与两次滚动之间的停留间隔;
动画曲线:内置线性、加速、减速等插值器;
触摸交互:支持用户触摸滑动暂停与手动拖动;
资源释放:activity/fragment 销毁时正确释放动画与 handler,防止内存泄露;
可定制样式:文字大小、颜色、字体、背景等可通过 xml 属性或代码动态配置;
高性能:在长列表、多实例场景下,保持平滑的 60fps。
语言:java
最低 sdk:api 21(android 5.0)
核心组件:
textview 或自定义 view
属性动画(objectanimator)
valueanimator + canvas.drawtext()(高级方案)
handler + runnable(基础方案)
scroller / overscroller(平滑滚动)
布局容器:通常使用 framelayout、relativelayout、constraintlayout 承载自定义控件
开发工具:android studio 最新稳定版
onmeasure():测量控件宽高;
onsizechanged():尺寸变化回调,初始化绘制区域;
ondraw(canvas):绘制文字与背景;
自定义属性:通过 res/values/attrs.xml 定义,可在 xml 中使用;
硬件加速:确保动画平滑,必要时关闭硬件加速进行文字阴影绘制。
objectanimator.offloat(view, "translationx", start, end);
valueanimator.offloat(start, end),在 addupdatelistener 中更新位置;
常用插值器:linearinterpolator、accelerateinterpolator、decelerateinterpolator、acceleratedecelerateinterpolator;
自定义插值器:实现 timeinterpolator。
适合循环式轻量调度;
postdelayed() 控制滚动间隔;
activity / fragment 销毁时要 removecallbacks() 防止内存泄漏。
实现流畅的物理滚动效果;
scroller.startscroll() 或 fling();
在 computescroll() 中,调用 scroller.computescrolloffset() 并 scrollto(x, y);
适用于需要手势拖动与惯性滚动的场景。
对于简单场景,可直接移动 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()。
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="marqueetextview">
<attr name="mtv_text" format="string" />
<attr name="mtv_textcolor" format="color" />
<attr name="mtv_textsize" format="dimension" />
<attr name="mtv_speed" format="float" />
<attr name="mtv_direction">
<flag name="horizontal" value="0" />
<flag name="vertical" value="1" />
</attr>
<attr name="mtv_repeatdelay" format="integer" />
<attr name="mtv_repeatmode">
<enum name="restart" value="1" />
<enum name="reverse" value="2" />
</attr>
<attr name="mtvinterpolator" format="reference" />
<attr name="mtv_loop" format="boolean" />
</declare-styleable>
</resources><!-- res/layout/activity_main.xml -->
<framelayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<com.example.marquee.marqueetextview
android:id="@+id/marqueeview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:mtv_text="欢迎使用android文字滚动播放控件"
app:mtv_textcolor="#ff5722"
app:mtv_textsize="18sp"
app:mtv_speed="100"
app:mtv_direction="horizontal"
app:mtv_repeatdelay="500"
app:mtv_repeatmode="restart"
app:mtvinterpolator="@android:anim/linear_interpolator"
app:mtv_loop="true"/>
</framelayout>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文字滚动播放的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论