Android實(shí)現(xiàn)文字滾動(dòng)播放效果的示例代碼
一、項(xiàng)目介紹
1. 背景與意義
在許多資訊類、新聞?lì)愐约捌髽I(yè)展示類 Android 應(yīng)用中,文字滾動(dòng)播放(也稱為跑馬燈效果、公告欄效果)是非常常見(jiàn)的 UI 交互方式,用于持續(xù)不斷地展示公告、新聞標(biāo)題、提示信息等。在影視推薦 App、地鐵公交查詢、股市行情等場(chǎng)景中,文字滾動(dòng)不僅能夠節(jié)省屏幕空間,還能吸引用戶注意力,使信息傳遞更具張力。本項(xiàng)目通過(guò)原生 Android 技術(shù),從零開(kāi)始實(shí)現(xiàn)一套高性能、高度可定制、支持多種滾動(dòng)方向與動(dòng)畫(huà)曲線的文字滾動(dòng)播放控件,滿足各類復(fù)雜需求。
2. 功能需求
文字內(nèi)容設(shè)定:可動(dòng)態(tài)設(shè)置一段或多段文字;
滾動(dòng)模式:支持水平、垂直兩種滾動(dòng)方向;
滾動(dòng)方式:支持循環(huán)播放與單次播放,支持往返式和無(wú)縫銜接;
速度與間隔:可自定義滾動(dòng)速度與兩次滾動(dòng)之間的停留間隔;
動(dòng)畫(huà)曲線:內(nèi)置線性、加速、減速等插值器;
觸摸交互:支持用戶觸摸滑動(dòng)暫停與手動(dòng)拖動(dòng);
資源釋放:Activity/Fragment 銷毀時(shí)正確釋放動(dòng)畫(huà)與 Handler,防止內(nèi)存泄露;
可定制樣式:文字大小、顏色、字體、背景等可通過(guò) XML 屬性或代碼動(dòng)態(tài)配置;
高性能:在長(zhǎng)列表、多實(shí)例場(chǎng)景下,保持平滑的 60FPS。
3. 技術(shù)選型
語(yǔ)言:Java
最低 SDK:API 21(Android 5.0)
核心組件:
TextView或自定義View屬性動(dòng)畫(huà)(
ObjectAnimator)ValueAnimator+Canvas.drawText()(高級(jí)方案)Handler+Runnable(基礎(chǔ)方案)Scroller/OverScroller(平滑滾動(dòng))
布局容器:通常使用
FrameLayout、RelativeLayout、ConstraintLayout承載自定義控件開(kāi)發(fā)工具:Android Studio 最新穩(wěn)定版
二、相關(guān)知識(shí)詳解
1. Android 自定義 View 基礎(chǔ)
onMeasure():測(cè)量控件寬高;
onSizeChanged():尺寸變化回調(diào),初始化繪制區(qū)域;
onDraw(Canvas):繪制文字與背景;
自定義屬性:通過(guò)
res/values/attrs.xml定義,可在 XML 中使用;硬件加速:確保動(dòng)畫(huà)平滑,必要時(shí)關(guān)閉硬件加速進(jìn)行文字陰影繪制。
2. 屬性動(dòng)畫(huà)與插值器
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()控制滾動(dòng)間隔;Activity / Fragment 銷毀時(shí)要
removeCallbacks()防止內(nèi)存泄漏。
4. Scroller / OverScroller
實(shí)現(xiàn)流暢的物理滾動(dòng)效果;
scroller.startScroll()或fling();在
computeScroll()中,調(diào)用scroller.computeScrollOffset()并scrollTo(x, y);適用于需要手勢(shì)拖動(dòng)與慣性滾動(dòng)的場(chǎng)景。
5. TextView 與 Canvas.drawText()
對(duì)于簡(jiǎn)單場(chǎng)景,可直接移動(dòng)
TextView;對(duì)于更高性能與自定義效果,可在
View.onDraw()中canvas.drawText(),并通過(guò)canvas.translate()實(shí)現(xiàn)滾動(dòng)。
三、項(xiàng)目實(shí)現(xiàn)思路
確定實(shí)現(xiàn)方案
方案一(基礎(chǔ)):在布局中使用單個(gè)
TextView,通過(guò)ObjectAnimator或TranslateAnimation移動(dòng)TextView的translationX/Y。方案二(自定義View):繼承
View,在onDraw()中繪制文字并控制文字繪制位置偏移,實(shí)現(xiàn)更靈活的動(dòng)畫(huà)與樣式控制。
基礎(chǔ)流程
初始化:讀取 XML 屬性或通過(guò) setter 獲取文字內(nèi)容、字體、顏色、速度等配置;
測(cè)量與布局:在
onMeasure()計(jì)算文字寬度/高度,確定 View 大??;啟動(dòng)動(dòng)畫(huà):在
onAttachedToWindow()或startScroll()中,啟動(dòng)滾動(dòng)動(dòng)畫(huà);滾動(dòng)控制:使用
ValueAnimator或ObjectAnimator不斷更新文字的偏移量;循環(huán)與間隔:監(jiān)聽(tīng)動(dòng)畫(huà)結(jié)束(
AnimatorListener),在回調(diào)中postDelayed()再次啟動(dòng),以實(shí)現(xiàn)間隔播放;資源釋放:在
onDetachedFromWindow()中取消所有動(dòng)畫(huà)與 Handler 調(diào)用。
多方向與多模式
水平滾動(dòng):初始偏移為
viewWidth,終點(diǎn)為-textWidth;垂直滾動(dòng):初始偏移為
viewHeight,終點(diǎn)為-textHeight;往返模式:設(shè)置
repeatMode = ValueAnimator.REVERSE;無(wú)縫銜接:使用兩行文本交替滾動(dòng),一行滾出,一行緊隨其后。
觸摸暫停與拖動(dòng)
在自定義 View 中重寫
onTouchEvent(),在ACTION_DOWN時(shí)pause()動(dòng)畫(huà),ACTION_MOVE時(shí)調(diào)整偏移,ACTION_UP時(shí)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文字滾動(dòng)播放控件"
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)前滾動(dòng)偏移
// ========== 動(dòng)畫(huà) ==========
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);
// 計(jì)算文字尺寸
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) {
// 水平滾動(dòng):從右側(cè)外開(kāi)始,到左側(cè)外結(jié)束
start = getWidth();
end = -textWidth;
distance = start - end;
} else {
// 垂直滾動(dòng):從底部外開(kāi)始,到頂部外結(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ù)中通過(guò)
TypedArray讀取并初始化。
測(cè)量邏輯
onMeasure()根據(jù)滾動(dòng)方向決定控件的期望寬高;對(duì)水平滾動(dòng),寬度由父容器決定,高度由文字高度加內(nèi)邊距決定;
對(duì)垂直滾動(dòng),反之亦然。
繪制邏輯
onDraw()中,根據(jù)當(dāng)前offset繪制文字;使用
paint.measureText()和paint.getFontMetrics()計(jì)算文字寬高與基線。
動(dòng)畫(huà)邏輯
startScroll()中,計(jì)算從起始位置到結(jié)束位置的距離與時(shí)長(zhǎng);使用
ObjectAnimator對(duì)offset屬性做動(dòng)畫(huà);設(shè)置插值器、循環(huán)次數(shù)、循環(huán)模式與延時(shí);
在
onDetachedFromWindow()中取消動(dòng)畫(huà),防止泄漏。
可擴(kuò)展性
暴露
setText()、setSpeed()、pause()、resume()等方法;監(jiān)聽(tīng)用戶觸摸,支持滑動(dòng)暫停與手動(dòng)拖動(dòng);
對(duì)接 RecyclerView、ListView,實(shí)現(xiàn)列表內(nèi)多個(gè)跑馬燈。
六、項(xiàng)目總結(jié)與拓展
項(xiàng)目收獲
深入掌握自定義 View 的測(cè)量、繪制與屬性動(dòng)畫(huà);
學(xué)會(huì)在自定義控件中優(yōu)雅管理動(dòng)畫(huà)生命周期;
掌握跑馬燈效果的核心算法:偏移量計(jì)算與時(shí)長(zhǎng)轉(zhuǎn)換;
學(xué)會(huì)如何通過(guò) XML 屬性實(shí)現(xiàn)高度可配置化。
性能優(yōu)化
確保硬件加速開(kāi)啟,避免文字繪制卡頓;
對(duì)于超長(zhǎng)文字或多列文字,可使用
StaticLayout分段緩存;結(jié)合
Choreographer精確控制幀率;
高級(jí)拓展
觸摸控制:拖動(dòng)暫停、手動(dòng)快進(jìn)快退;
多行跑馬燈:支持同時(shí)滾動(dòng)多行文字,或背景漸變;
動(dòng)態(tài)數(shù)據(jù)源:與網(wǎng)絡(luò)或數(shù)據(jù)庫(kù)結(jié)合,實(shí)時(shí)更新滾動(dòng)內(nèi)容;
Jetpack Compose 實(shí)現(xiàn):基于
Canvas與Modifier.offset()的 Compose 方案;
以上就是Android實(shí)現(xiàn)文字滾動(dòng)播放效果的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Android文字滾動(dòng)播放的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用 Coroutine + Retrofit打造簡(jiǎn)單的HTTP請(qǐng)求庫(kù)
這篇文章主要介紹了Android使用 Coroutine + Retrofit打造簡(jiǎn)單的HTTP請(qǐng)求庫(kù),幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-03-03
PopupWindow自定義位置顯示的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了PopupWindow自定義位置顯示,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
關(guān)于Android輸入法彈窗bug的優(yōu)雅處理
在Android應(yīng)用中,當(dāng)跳轉(zhuǎn)到某個(gè)Activity時(shí),該Activity顯示頁(yè)面的EditText獲得焦點(diǎn),在某些機(jī)器中會(huì)觸發(fā)軟鍵盤的自動(dòng)彈出,這篇文章主要給大家介紹了關(guān)于Android輸入法彈窗bug的優(yōu)雅處理,需要的朋友可以參考下2021-10-10
Android高級(jí)UI特效仿直播點(diǎn)贊動(dòng)畫(huà)效果
這篇文章主要介紹了Android高級(jí)UI特效仿直播點(diǎn)贊動(dòng)畫(huà)效果,最近比較火的抖音快手直播視頻都有這樣的效果,下面腳本之家小編給大家?guī)?lái)android 仿直播點(diǎn)贊效果的實(shí)現(xiàn)代碼,需要的朋友參考下吧2018-03-03
android listview實(shí)現(xiàn)新聞列表展示效果
這篇文章主要為大家詳細(xì)介紹了android listview實(shí)現(xiàn)新聞列表展示效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android實(shí)現(xiàn)繪制LocationMarkerView圖的示例代碼
LocationMarker是運(yùn)動(dòng)軌跡上Start、End, 以及整公里點(diǎn)上筆者自定義繪制的一個(gè)MarkerView。這篇文章主要介紹了Android實(shí)現(xiàn)繪制LocationMarkerView圖的示例代碼,希望對(duì)大家有所幫助2023-02-02
Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)滑動(dòng)屏幕切換圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08

