46人参与 • 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 举报,一经查实将立刻删除。
发表评论