一、项目介绍
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
<!-- 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>4.2 布局文件
<!-- 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>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文字滚动播放的资料请关注代码网其它相关文章!
发表评论