Android實(shí)現(xiàn)文字滾動播放效果的示例代碼
一、項目介紹
1. 背景與意義
在許多資訊類、新聞類以及企業(yè)展示類 Android 應(yīng)用中,文字滾動播放(也稱為跑馬燈效果、公告欄效果)是非常常見的 UI 交互方式,用于持續(xù)不斷地展示公告、新聞標(biāo)題、提示信息等。在影視推薦 App、地鐵公交查詢、股市行情等場景中,文字滾動不僅能夠節(jié)省屏幕空間,還能吸引用戶注意力,使信息傳遞更具張力。本項目通過原生 Android 技術(shù),從零開始實(shí)現(xiàn)一套高性能、高度可定制、支持多種滾動方向與動畫曲線的文字滾動播放控件,滿足各類復(fù)雜需求。
2. 功能需求
文字內(nèi)容設(shè)定:可動態(tài)設(shè)置一段或多段文字;
滾動模式:支持水平、垂直兩種滾動方向;
滾動方式:支持循環(huán)播放與單次播放,支持往返式和無縫銜接;
速度與間隔:可自定義滾動速度與兩次滾動之間的停留間隔;
動畫曲線:內(nèi)置線性、加速、減速等插值器;
觸摸交互:支持用戶觸摸滑動暫停與手動拖動;
資源釋放:Activity/Fragment 銷毀時正確釋放動畫與 Handler,防止內(nèi)存泄露;
可定制樣式:文字大小、顏色、字體、背景等可通過 XML 屬性或代碼動態(tài)配置;
高性能:在長列表、多實(shí)例場景下,保持平滑的 60FPS。
3. 技術(shù)選型
語言:Java
最低 SDK:API 21(Android 5.0)
核心組件:
TextView
或自定義View
屬性動畫(
ObjectAnimator
)ValueAnimator
+Canvas.drawText()
(高級方案)Handler
+Runnable
(基礎(chǔ)方案)Scroller
/OverScroller
(平滑滾動)
布局容器:通常使用
FrameLayout
、RelativeLayout
、ConstraintLayout
承載自定義控件開發(fā)工具:Android Studio 最新穩(wěn)定版
二、相關(guān)知識詳解
1. Android 自定義 View 基礎(chǔ)
onMeasure():測量控件寬高;
onSizeChanged():尺寸變化回調(diào),初始化繪制區(qū)域;
onDraw(Canvas):繪制文字與背景;
自定義屬性:通過
res/values/attrs.xml
定義,可在 XML 中使用;硬件加速:確保動畫平滑,必要時關(guān)閉硬件加速進(jìn)行文字陰影繪制。
2. 屬性動畫與插值器
ObjectAnimator.ofFloat(view, "translationX", start, end)
;ValueAnimator.ofFloat(start, end)
,在addUpdateListener
中更新位置;常用插值器:
LinearInterpolator
、AccelerateInterpolator
、DecelerateInterpolator
、AccelerateDecelerateInterpolator
;自定義插值器:實(shí)現(xiàn)
TimeInterpolator
。
3. Handler 與 Runnable
適合循環(huán)式輕量調(diào)度;
postDelayed()
控制滾動間隔;Activity / Fragment 銷毀時要
removeCallbacks()
防止內(nèi)存泄漏。
4. Scroller / OverScroller
實(shí)現(xiàn)流暢的物理滾動效果;
scroller.startScroll()
或fling()
;在
computeScroll()
中,調(diào)用scroller.computeScrollOffset()
并scrollTo(x, y)
;適用于需要手勢拖動與慣性滾動的場景。
5. TextView 與 Canvas.drawText()
對于簡單場景,可直接移動
TextView
;對于更高性能與自定義效果,可在
View.onDraw()
中canvas.drawText()
,并通過canvas.translate()
實(shí)現(xiàn)滾動。
三、項目實(shí)現(xiàn)思路
確定實(shí)現(xiàn)方案
方案一(基礎(chǔ)):在布局中使用單個
TextView
,通過ObjectAnimator
或TranslateAnimation
移動TextView
的translationX/Y
。方案二(自定義View):繼承
View
,在onDraw()
中繪制文字并控制文字繪制位置偏移,實(shí)現(xiàn)更靈活的動畫與樣式控制。
基礎(chǔ)流程
初始化:讀取 XML 屬性或通過 setter 獲取文字內(nèi)容、字體、顏色、速度等配置;
測量與布局:在
onMeasure()
計算文字寬度/高度,確定 View 大?。?/p>啟動動畫:在
onAttachedToWindow()
或startScroll()
中,啟動滾動動畫;滾動控制:使用
ValueAnimator
或ObjectAnimator
不斷更新文字的偏移量;循環(huán)與間隔:監(jiān)聽動畫結(jié)束(
AnimatorListener
),在回調(diào)中postDelayed()
再次啟動,以實(shí)現(xiàn)間隔播放;資源釋放:在
onDetachedFromWindow()
中取消所有動畫與 Handler 調(diào)用。
多方向與多模式
水平滾動:初始偏移為
viewWidth
,終點(diǎn)為-textWidth
;垂直滾動:初始偏移為
viewHeight
,終點(diǎn)為-textHeight
;往返模式:設(shè)置
repeatMode = ValueAnimator.REVERSE
;無縫銜接:使用兩行文本交替滾動,一行滾出,一行緊隨其后。
觸摸暫停與拖動
在自定義 View 中重寫
onTouchEvent()
,在ACTION_DOWN
時pause()
動畫,ACTION_MOVE
時調(diào)整偏移,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; // 是否循環(huán) private TimeInterpolator interpolator; // ========== 繪制相關(guān) ========== private Paint paint; private float textWidth, textHeight; private float offset; // 當(dāng)前滾動偏移 // ========== 動畫 ========== 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) { // 水平滾動:從右側(cè)外開始,到左側(cè)外結(jié)束 start = getWidth(); end = -textWidth; distance = start - end; } else { // 垂直滾動:從底部外開始,到頂部外結(jié)束 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
中定義了文字內(nèi)容、顏色、大小、速度、方向、間隔、循環(huán)模式、插值器等屬性;在控件構(gòu)造函數(shù)中通過
TypedArray
讀取并初始化。
測量邏輯
onMeasure()
根據(jù)滾動方向決定控件的期望寬高;對水平滾動,寬度由父容器決定,高度由文字高度加內(nèi)邊距決定;
對垂直滾動,反之亦然。
繪制邏輯
onDraw()
中,根據(jù)當(dāng)前offset
繪制文字;使用
paint.measureText()
和paint.getFontMetrics()
計算文字寬高與基線。
動畫邏輯
startScroll()
中,計算從起始位置到結(jié)束位置的距離與時長;使用
ObjectAnimator
對offset
屬性做動畫;設(shè)置插值器、循環(huán)次數(shù)、循環(huán)模式與延時;
在
onDetachedFromWindow()
中取消動畫,防止泄漏。
可擴(kuò)展性
暴露
setText()
、setSpeed()
、pause()
、resume()
等方法;監(jiān)聽用戶觸摸,支持滑動暫停與手動拖動;
對接 RecyclerView、ListView,實(shí)現(xiàn)列表內(nèi)多個跑馬燈。
六、項目總結(jié)與拓展
項目收獲
深入掌握自定義 View 的測量、繪制與屬性動畫;
學(xué)會在自定義控件中優(yōu)雅管理動畫生命周期;
掌握跑馬燈效果的核心算法:偏移量計算與時長轉(zhuǎn)換;
學(xué)會如何通過 XML 屬性實(shí)現(xiàn)高度可配置化。
性能優(yōu)化
確保硬件加速開啟,避免文字繪制卡頓;
對于超長文字或多列文字,可使用
StaticLayout
分段緩存;結(jié)合
Choreographer
精確控制幀率;
高級拓展
觸摸控制:拖動暫停、手動快進(jìn)快退;
多行跑馬燈:支持同時滾動多行文字,或背景漸變;
動態(tài)數(shù)據(jù)源:與網(wǎng)絡(luò)或數(shù)據(jù)庫結(jié)合,實(shí)時更新滾動內(nèi)容;
Jetpack Compose 實(shí)現(xiàn):基于
Canvas
與Modifier.offset()
的 Compose 方案;
以上就是Android實(shí)現(xiàn)文字滾動播放效果的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Android文字滾動播放的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Android Studio 開發(fā)自己的SDK教程
很多時候我們要將自己開發(fā)一個類庫打包成jar包以供他調(diào)用,這個jar包也叫你自己的SDK或者叫l(wèi)ibrary。android studio生成jar包的方法與eclipse有所不同。在studio中l(wèi)ibrary其實(shí)是module的概念。2017-10-10Android中g(shù)ravity、layout_gravity、padding、margin的區(qū)別小結(jié)
這篇文章主要介紹了Android中g(shù)ravity、layout_gravity、padding、margin的區(qū)別小結(jié),需要的朋友可以參考下2014-08-08使用Win10+Android+夜神安卓模擬器,搭建ReactNative開發(fā)環(huán)境
今天小編就為大家分享一篇關(guān)于使用Win10+Android+夜神安卓模擬器,搭建ReactNative開發(fā)環(huán)境,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10Android GPS獲取當(dāng)前經(jīng)緯度坐標(biāo)
這篇文章主要為大家詳細(xì)介紹了Android GPS獲取當(dāng)前經(jīng)緯度坐標(biāo),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05Android 開機(jī)應(yīng)用掃描相關(guān)總結(jié)
本篇文章只是作為指南引導(dǎo)去看PkMS,不會貼大段代碼進(jìn)行分析,更多是基于方法分析實(shí)現(xiàn)的邏輯,另外就是代碼是基于Android 11,與Android 10之前代碼有比較大的差別。2021-05-05Android開發(fā)筆記之: 數(shù)據(jù)存儲方式詳解
本篇文章是對Android中數(shù)據(jù)存儲方式進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05