欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android實(shí)現(xiàn)文字滾動播放效果的示例代碼

 更新時間:2025年05月06日 09:08:58   作者:Katie。  
在許多資訊類、新聞類以及企業(yè)展示類 Android 應(yīng)用中,文字滾動播放是非常常見的 UI 交互方式,文字滾動不僅能夠節(jié)省屏幕空間,還能吸引用戶注意力,所以本項目通過原生 Android 技術(shù),從零開始實(shí)現(xiàn)一套支持多種滾動方向與動畫曲線的文字滾動播放控件,需要的朋友可以參考下

一、項目介紹

1. 背景與意義

在許多資訊類、新聞類以及企業(yè)展示類 Android 應(yīng)用中,文字滾動播放(也稱為跑馬燈效果、公告欄效果)是非常常見的 UI 交互方式,用于持續(xù)不斷地展示公告、新聞標(biāo)題、提示信息等。在影視推薦 App、地鐵公交查詢、股市行情等場景中,文字滾動不僅能夠節(jié)省屏幕空間,還能吸引用戶注意力,使信息傳遞更具張力。本項目通過原生 Android 技術(shù),從零開始實(shí)現(xiàn)一套高性能、高度可定制、支持多種滾動方向與動畫曲線的文字滾動播放控件,滿足各類復(fù)雜需求。

2. 功能需求

  1. 文字內(nèi)容設(shè)定:可動態(tài)設(shè)置一段或多段文字;

  2. 滾動模式:支持水平垂直兩種滾動方向;

  3. 滾動方式:支持循環(huán)播放與單次播放,支持往返式無縫銜接;

  4. 速度與間隔:可自定義滾動速度與兩次滾動之間的停留間隔;

  5. 動畫曲線:內(nèi)置線性、加速、減速等插值器;

  6. 觸摸交互:支持用戶觸摸滑動暫停與手動拖動;

  7. 資源釋放:Activity/Fragment 銷毀時正確釋放動畫與 Handler,防止內(nèi)存泄露;

  8. 可定制樣式:文字大小、顏色、字體、背景等可通過 XML 屬性或代碼動態(tài)配置;

  9. 高性能:在長列表、多實(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)思路

  1. 確定實(shí)現(xiàn)方案

    • 方案一(基礎(chǔ)):在布局中使用單個 TextView,通過 ObjectAnimator 或 TranslateAnimation 移動 TextView 的 translationX/Y。

    • 方案二(自定義View):繼承 View,在 onDraw() 中繪制文字并控制文字繪制位置偏移,實(shí)現(xiàn)更靈活的動畫與樣式控制。

  2. 基礎(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)用。

  3. 多方向與多模式

    • 水平滾動:初始偏移為 viewWidth,終點(diǎn)為 -textWidth;

    • 垂直滾動:初始偏移為 viewHeight,終點(diǎn)為 -textHeight;

    • 往返模式:設(shè)置 repeatMode = ValueAnimator.REVERSE

    • 無縫銜接:使用兩行文本交替滾動,一行滾出,一行緊隨其后。

  4. 觸摸暫停與拖動

    • 在自定義 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() 等 ====
}

五、代碼解讀

  1. 自定義屬性

    • 在 attrs.xml 中定義了文字內(nèi)容、顏色、大小、速度、方向、間隔、循環(huán)模式、插值器等屬性;

    • 在控件構(gòu)造函數(shù)中通過 TypedArray 讀取并初始化。

  2. 測量邏輯

    • onMeasure() 根據(jù)滾動方向決定控件的期望寬高;

    • 對水平滾動,寬度由父容器決定,高度由文字高度加內(nèi)邊距決定;

    • 對垂直滾動,反之亦然。

  3. 繪制邏輯

    • onDraw() 中,根據(jù)當(dāng)前 offset 繪制文字;

    • 使用 paint.measureText() 和 paint.getFontMetrics() 計算文字寬高與基線。

  4. 動畫邏輯

    • startScroll() 中,計算從起始位置到結(jié)束位置的距離與時長;

    • 使用 ObjectAnimator 對 offset 屬性做動畫;

    • 設(shè)置插值器、循環(huán)次數(shù)、循環(huán)模式與延時;

    • 在 onDetachedFromWindow() 中取消動畫,防止泄漏。

  5. 可擴(kuò)展性

    • 暴露 setText()、setSpeed()pause()、resume() 等方法;

    • 監(jiān)聽用戶觸摸,支持滑動暫停與手動拖動;

    • 對接 RecyclerView、ListView,實(shí)現(xiàn)列表內(nèi)多個跑馬燈。

六、項目總結(jié)與拓展

  1. 項目收獲

    • 深入掌握自定義 View 的測量、繪制與屬性動畫;

    • 學(xué)會在自定義控件中優(yōu)雅管理動畫生命周期;

    • 掌握跑馬燈效果的核心算法:偏移量計算與時長轉(zhuǎn)換;

    • 學(xué)會如何通過 XML 屬性實(shí)現(xiàn)高度可配置化。

  2. 性能優(yōu)化

    • 確保硬件加速開啟,避免文字繪制卡頓;

    • 對于超長文字或多列文字,可使用 StaticLayout 分段緩存;

    • 結(jié)合 Choreographer 精確控制幀率;

  3. 高級拓展

    • 觸摸控制:拖動暫停、手動快進(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)文章

最新評論