开发者

Android实现文字滚动播放效果的示例代码

开发者 https://www.devze.com 2025-05-10 11:31 出处:网络 作者: Katie。
目录一、项目介绍1. 背景与意义2. 功能需求3. 技术选型二、相关知识详解1. android 自定义 View 基础2. 属性动画与插值器3. Handler 与 Runnable4. Scroller / OverScroller5. TextView 与 Canvas.drawText()三、项目
目录
  • 一、项目介绍
    • 1. 背景与意义
    • 2. 功能需求
    • 3. 技术选型
  • 二、相关知识详解
    • 1. android 自定义 View 基础
    • 2. 属性动画与插值器
    • 3. Handler 与 Runnable
    • 4. Scroller / OverScroller
    • 5. TextView 与 Canvas.drawText()
  • 三、项目实现思路
    • 四、完整整合版代码
      • 4.1 attrs.XML
      • 4.2 布局文件
      • 4.3 自定义控件:MarqueeTextView.Java
    • 五、代码解读
      • 六、项目总结与拓展

        一、项目介绍

        1. 背景与意义

        在许多资讯类、新闻类以及企业展示类 Android 应用中,文字滚动播放(也称为跑马灯效果、公告栏效果)是非常常见的 UI 交互方式,用于持续不断地展示公告、新闻标题、提示信息等。在影视推荐 App、地铁公交查询、股市行情等场景中,文字滚动不仅能够节省屏幕空间,还能吸引用户注意力,使信息传递更具张力。本项目通过原生 Android 技术,从零开始实现一套高性能、高度可定制、支持多种滚动方向与动画曲线的文字滚动播放控件,满足各类复杂需求。

        2. 功能需求

        1. 文字内容设定:可动态设置一段或多段文字;

        2. 滚动模式:支持水平垂直两种滚动方向;

        3. 滚动方式:支持循环播放与单次播放,支持往返式无缝衔接

        4. 速度与间隔:可自定义滚动速度与两次滚动之间的停留间隔;

        5. 动画曲线:内置线性加速减速等插值器;

        6. 触摸交互:支持用户触摸滑动暂停与手动拖动;

        7. 资源释放:Activity/Fragment 销毁时正确释放动画与 Handler,防止内存泄露;

        8. 可定制样式:文字大小、颜色、字体、js背景等可通过 XML 属性或代码动态配置;

        9. 高性能:在长列表、多实例场景下,保持平滑的 60FPS。

        3. 技术选型

        • 语言:Java

        • 最低 SDK:API 21(Android 5.0)

        • 核心组件

          • TextView 或自定义 View

          • 属性动画(ObjectAnimator

          • ValueAnimator + Canvas.drawText()(高级方案)

          • Handler + Runnable(基础方案)

          • Scroller / OverScroller(平滑滚动)

        • 布局容器:通常使用 FrameLayoutRelativeLayoutConstraintLayout 承载自定义控件

        • 开发工具: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 中更新位置;

        • 常用插值器:LinearInterpolatorAccelerateInterpolatorDecelerateInterpolatorAccelerateDecelerateInterpolator

        • 自定义插值器:实现 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() 实现滚动。

        三、项目实现思路

        1. 确定实现方案

          • 方案一(基础):在布局中使用单个 TextView,通过 ObjectAnimator 或 TranslateAnimation 移动 TextView 的 translationX/Y

          • 方案二(自定义View):继承 View,在 onDraw() 中绘制文字并控制文字绘制位置偏移,实现更灵活的动画与样式控制。

        2. 基础流程

          • 初始化:读取 XML 属性或通过 setter 获取文字内容、字体、颜色、速度等配置;

          • 测量与布局:在 onMeasure() 计算文字宽度/高度,确定 View 大小;

          • 启动动画:在 onAttachedToWindow() 或 startScroll() 中,启动滚动动画;

          • 滚动控制:使用 ValueAnimator 或 ObjectAnimator 不断更新文字的偏移量;

          • 循环与间隔:监听动画结束(AnimatorListener),在回调中 postDelayed() 再次启动,以实现间隔播放;

          • 资源释放:在 onDetachedFromWindow() 中取消所有动画与 Handler 调用。

        3. 多方向与多模式

          • 水平滚动:初始偏移为 viewWidth,终点为 -te编程客栈xtWidth

          • 垂直滚动:初始偏移为 viewHeight,终点为 -textHeight

          • 往返模式:设置 repeatMode = ValueAnimator.REVERSE

          • 无缝衔接:使用两行文本交替滚动,一行滚出,一行紧随其后。

        4. 触摸暂停与拖动

          • 在自定义 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" />
             python       <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);
                loandroidop = 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() 等 ====
        }

        五、代码解读

        1. 自定义属性

          • 在 attrs.xml 中定义了文字内容、颜色、大小、速度、方向、间隔、循环模式、插值器等属性;

          • 在控件构造函数中通过 TypedArray 读取并初始化。

        2. 测量逻辑

          • onMeasure() 根据滚动方向决定控件的期望宽高;

          • 对水平滚动,宽度由父容器决定,高度由文字高度加内边距决定;

          • 对垂直滚动,反之亦然。

        3. 绘制逻辑

          • onDraw() 中,根据当前 offset 绘制文字;

          • 使用 paint.measureText() 和 paint.getFontMetrics() 计算文字宽高与基线。

        4. 动画逻辑

          • startScroll() 中,计算从起始位置到结束位置的距离与时长;

          • 使用 ObjectAnimator 对 offset 属性做动画;

          • 设置插值器、循环次数、循环模式与延时;

          • 在 onDetachedFromWindow() 中取消动画,防止泄漏。

        5. 可扩展性

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

          • 监听用户触摸,支持滑动暂停与手动拖动;

          • 对接 RecyclerView、ListView,实现列表内多个跑马灯。

        六、项目总结与拓展

        1. 项目收获

          • 深入掌握自定义 View 的测量、绘制与属性动画;

          • 学会在自定义控件中优雅管理动画生命周期;

          • 掌握跑马灯效果的核心算法:偏移量计算与时长转换;

          • 学会如何通过 XML 属性实现高度可配置化。

        2. 性能优化

          • 确保硬件加速开启,避免文字绘制卡顿;

          • 对于超长文字或多列文字,可使用 StaticLayout 分段缓存;

          • 结合 Choreographer 精确控制帧率;

        3. 高级拓展

          • 触摸控制javascript拖动暂停、手动快进快退;

          • 多行跑马灯:支持同时滚动多行文字,或背景渐变;

          • 动态数据源:与网络或数据库结合,实时更新滚动内容;

          • Jetpack Compose 实现:基于 Canvas 与 Modifier.offset() 的 Compose 方案;

        以上就是Android实现文字滚动播放效果的示例代码的详细内容,更多关于Android文字滚动播放的资料请关注编程客栈(www.devze.com)其它相关文章!

        0

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        关注公众号