Android-TextView跑马灯探秘

Android TextView中划线、下划线、跑马灯的简单使用 本人安卓初学者,小白一枚,希望以写博客的方法巩固已学的技能,讲的可能不好,望大家见谅! 不墨迹 直接上 效果图 TextView 中划线 和 下划线 xml文件中(中划线和下划线一样 有个 id 就行) <TextView android:id="@+id/txv" android:text="中划线、下划线" android:lay... 阅读详情

前言

自定义View实现的跑马灯一直没有实现类似 Android TextView 的跑马灯首尾相接的效果,所以一直想看看Android TextView 的跑马灯是如何实现

本文主要探秘 Android TextView 的跑马灯实现原理及实现自下往上效果的跑马灯

探秘

TextView#onDraw

原生 Android TextView 如何设置开启跑马灯效果,此处不再描述

View 的绘制都在 onDraw 方法中,这里直接查看 TextView#onDraw() 方法,删减一些不关心的代码

protected void onDraw(Canvas canvas) {
	// 是否需要重启启动跑马灯
	restartMarqueeIfNeeded();

    // Draw the background for this view
    super.onDraw(canvas);
        
    // 删减不关心的代码

    // 创建`mLayout`对象, 此处为`StaticLayout`
    if (mLayout == null) {
        assumeLayout();
    }

    Layout layout = mLayout;

    canvas.save();

    // 删减不关心的代码

    final int layoutDirection = getLayoutDirection();
    final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

    // 判断跑马灯设置项是否正确
	if (isMarqueeFadeEnabled()) {
		if (!mSingleLine && getLineCount() == 1 && canMarquee()
              && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
           final int width = mRight - mLeft;
           final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
           final float dx = mLayout.getLineRight(0) - (width - padding);
           canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        }

        // 判断跑马灯是否启动
        if (mMarquee != null && mMarquee.isRunning()) {
            final float dx = -mMarquee.getScroll();
            // 移动画布
            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        }
    }

    final int cursorOffsetVertical = voffsetCursor - voffsetText;

    Path highlight = getUpdatedHighlightPath();
    if (mEditor != null) {
        mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
    } else {
        // 绘制文本
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

    // 判断是否可以绘制尾部文本
    if (mMarquee != null && mMarquee.shouldDrawGhost()) {
        final float dx = mMarquee.getGhostOffset();
        // 移动画布
        canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
        // 绘制尾部文本
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

    canvas.restore();
}

Marquee

根据 onDraw() 方法分析,跑马灯效果的实现主要依赖 mMarquee 这个对象来实现,好的,看下 Marquee 吧,Marquee 代码较少,就贴上全部源码吧

private static final class Marquee {
    // TODO: Add an option to configure this
    // 缩放相关,不关心此字段
    private static final float MARQUEE_DELTA_MAX = 0.07f;
    
    // 跑马灯跑完一次后多久开始下一次
    private static final int MARQUEE_DELAY = 1200;
    
    // 绘制一次跑多长距离因子,此字段与速度相关
    private static final int MARQUEE_DP_PER_SECOND = 30;

    // 跑马灯状态常量
    private static final byte MARQUEE_STOPPED = 0x0;
    private static final byte MARQUEE_STARTING = 0x1;
    private static final byte MARQUEE_RUNNING = 0x2;

    // 对TextView进行弱引用
    private final WeakReference<TextView> mView;
    
    // 帧率相关
    private final Choreographer mChoreographer;

    // 状态
    private byte mStatus = MARQUEE_STOPPED;
    
    // 绘制一次跑多长距离
    private final float mPixelsPerMs;
    
    // 最大滚动距离
    private float mMaxScroll;
    
    // 是否可以绘制右阴影, 右侧淡入淡出效果
    private float mMaxFadeScroll;
    
    // 尾部文本什么时候开始绘制
    private float mGhostStart;
    
    // 尾部文本绘制位置偏移量
    private float mGhostOffset;
    
    // 是否可以绘制左阴影,左侧淡入淡出效果
    private float mFadeStop;
    
    // 重复限制
    private int mRepeatLimit;

    // 跑动距离
    private float mScroll;
    
    // 最后一次跑动时间,单位毫秒
    private long mLastAnimationMs;

    Marquee(TextView v) {
        final float density = v.getContext().getResources().getDisplayMetrics().density;
        // 计算每次跑多长距离
        mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
        mView = new WeakReference<TextView>(v);
        mChoreographer = Choreographer.getInstance();
    }

    // 帧率回调,用于跑马灯跑动
    private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            tick();
        }
    };

    // 帧率回调,用于跑马灯开始跑动
    private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mStatus = MARQUEE_RUNNING;
            mLastAnimationMs = mChoreographer.getFrameTime();
            tick();
        }
    };

    // 帧率回调,用于跑马灯重新跑动
    private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mStatus == MARQUEE_RUNNING) {
                if (mRepeatLimit >= 0) {
                    mRepeatLimit--;
                }
                start(mRepeatLimit);
            }
        }
    };

    // 跑马灯跑动实现
    void tick() {
        if (mStatus != MARQUEE_RUNNING) {
            return;
        }

        mChoreographer.removeFrameCallback(mTickCallback);

        final TextView textView = mView.get();
        // 判断TextView是否处于获取焦点或选中状态
        if (textView != null && (textView.isFocused() || textView.isSelected())) {
            // 获取当前时间
            long currentMs = mChoreographer.getFrameTime();
            // 计算当前时间与上次时间的差值
            long deltaMs = currentMs - mLastAnimationMs;
            mLastAnimationMs = currentMs;
            // 根据时间差计算本次跑动的距离,减轻视觉上跳动/卡顿
            float deltaPx = deltaMs * mPixelsPerMs;
            // 计算跑动距离
            mScroll += deltaPx;
            // 判断是否已经跑完
            if (mScroll > mMaxScroll) {
                mScroll = mMaxScroll;
                // 发送重新开始跑动事件
                mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
            } else {
                // 发送下一次跑动事件
                mChoreographer.postFrameCallback(mTickCallback);
            }
            // 调用此方法会触发执行`onDraw`方法
            textView.invalidate();
        }
    }

    // 停止跑马灯
    void stop() {
        mStatus = MARQUEE_STOPPED;
        mChoreographer.removeFrameCallback(mStartCallback);
        mChoreographer.removeFrameCallback(mRestartCallback);
        mChoreographer.removeFrameCallback(mTickCallback);
        resetScroll();
    }

    private void resetScroll() {
        mScroll = 0.0f;
        final TextView textView = mView.get();
        if (textView != null) textView.invalidate();
    }

    // 启动跑马灯
    void start(int repeatLimit) {
        if (repeatLimit == 0) {
            stop();
            return;
        }
        mRepeatLimit = repeatLimit;
        final TextView textView = mView.get();
        if (textView != null && textView.mLayout != null) {
            // 设置状态为在跑
            mStatus = MARQUEE_STARTING;
            // 重置跑动距离
            mScroll = 0.0f;
            // 计算TextView宽度
            final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
                - textView.getCompoundPaddingRight();
            // 获取文本第0行的宽度
            final float lineWidth = textView.mLayout.getLineWidth(0);
            // 取TextView宽度的三分之一
            final float gap = textWidth / 3.0f;
            // 计算什么时候可以开始绘制尾部文本:首部文本跑动到哪里可以绘制尾部文本
            mGhostStart = lineWidth - textWidth + gap;
            // 计算最大滚动距离:什么时候认为跑完一次
            mMaxScroll = mGhostStart + textWidth;
            // 尾部文本绘制偏移量
            mGhostOffset = lineWidth + gap;
            // 跑动到哪里时不绘制左侧阴影
            mFadeStop = lineWidth + textWidth / 6.0f;
            // 跑动到哪里时不绘制右侧阴影
            mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;

            textView.invalidate();
            // 开始跑动
            mChoreographer.postFrameCallback(mStartCallback);
        }
    }

    // 获取尾部文本绘制位置偏移量
    float getGhostOffset() {
        return mGhostOffset;
    }

    // 获取当前滚动距离
    float getScroll() {
        return mScroll;
    }

    // 获取可以右侧阴影绘制的最大距离
    float getMaxFadeScroll() {
        return mMaxFadeScroll;
    }

    // 判断是否可以绘制左侧阴影
    boolean shouldDrawLeftFade() {
        return mScroll <= mFadeStop;
    }

    // 判断是否可以绘制尾部文本
    boolean shouldDrawGhost() {
        return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
    }

    // 跑马灯是否在跑
    boolean isRunning() {
        return mStatus == MARQUEE_RUNNING;
    }

    // 跑马灯是否不跑
    boolean isStopped() {
        return mStatus == MARQUEE_STOPPED;
    }
}

好的,分析完 Marquee,跑马灯实现原理豁然明亮

  1. 在 TextView 开启跑马灯效果时调用 Marquee#start() 方法
  2. 在 Marquee#start() 方法中触发 TextView 重绘,开始计算跑动距离
  3. 在 TextView#onDraw() 方法中根据跑动距离移动画布并绘制首部文本,再根据跑动距离判断是否可以移动画布绘制尾部文本

小结

TextView 通过移动画布绘制两次文本实现跑马灯效果,根据两帧绘制的时间差计算跑动距离,怎一个"妙"了得

应用

上面分析完原生 Android TextView 跑马灯的实现原理,但是原生 Android TextView 跑马灯有几点不足:

  1. 无法设置跑动速度
  2. 无法设置重跑间隔时长
  3. 无法实现上下跑动

以上第1、2点在上面 Marquee 分析中已经有解决方案,接下来根据原生实现原理实现第3点上下跑动

MarqueeTextView

这里给出实现方案,列出主要实现逻辑,继承 AppCompatTextView,复写 onDraw() 方法,上下跑动主要是计算上下跑动的距离,然后再次重绘 TextView 上下移动画布绘制文本

/**
 * 继承AppCompatTextView,复写onDraw方法
 */
public class MarqueeTextView extends AppCompatTextView {

    private static final int DEFAULT_BG_COLOR = Color.parseColor("#FFEFEFEF");

    @IntDef({HORIZONTAL, VERTICAL})
    @Retention(RetentionPolicy.SOURCE)
    public @interface OrientationMode {
    }

    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;

    private Marquee mMarquee;
    private boolean mRestartMarquee;
    private boolean isMarquee;

    private int mOrientation;

    public MarqueeTextView(@NonNull Context context) {
        this(context, null);
    }

    public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MarqueeTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MarqueeTextView, defStyleAttr, 0);

        mOrientation = ta.getInt(R.styleable.MarqueeTextView_orientation, HORIZONTAL);

        ta.recycle();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (mOrientation == HORIZONTAL) {
            if (getWidth() > 0) {
                mRestartMarquee = true;
            }
        } else {
            if (getHeight() > 0) {
                mRestartMarquee = true;
            }
        }
    }

    private void restartMarqueeIfNeeded() {
        if (mRestartMarquee) {
            mRestartMarquee = false;
            startMarquee();
        }
    }

    public void setMarquee(boolean marquee) {
        boolean wasStart = isMarquee();

        isMarquee = marquee;

        if (wasStart != marquee) {
            if (marquee) {
                startMarquee();
            } else {
                stopMarquee();
            }
        }
    }

    public void setOrientation(@OrientationMode int orientation) {
        mOrientation = orientation;
    }

    public int getOrientation() {
        return mOrientation;
    }

    public boolean isMarquee() {
        return isMarquee;
    }

    private void stopMarquee() {
        if (mOrientation == HORIZONTAL) {
            setHorizontalFadingEdgeEnabled(false);
        } else {
            setVerticalFadingEdgeEnabled(false);
        }

        requestLayout();
        invalidate();

        if (mMarquee != null && !mMarquee.isStopped()) {
            mMarquee.stop();
        }
    }

    private void startMarquee() {
        if (canMarquee()) {

            if (mOrientation == HORIZONTAL) {
                setHorizontalFadingEdgeEnabled(true);
            } else {
                setVerticalFadingEdgeEnabled(true);
            }

            if (mMarquee == null) mMarquee = new Marquee(this);
            mMarquee.start(-1);
        }
    }

    private boolean canMarquee() {
        if (mOrientation == HORIZONTAL) {
            int viewWidth = getWidth() - getCompoundPaddingLeft() -
                getCompoundPaddingRight();
            float lineWidth = getLayout().getLineWidth(0);
            return (mMarquee == null || mMarquee.isStopped())
                && (isFocused() || isSelected() || isMarquee())
                && viewWidth > 0
                && lineWidth > viewWidth;
        } else {
            int viewHeight = getHeight() - getCompoundPaddingTop() -
                getCompoundPaddingBottom();
            float textHeight = getLayout().getHeight();
            return (mMarquee == null || mMarquee.isStopped())
                && (isFocused() || isSelected() || isMarquee())
                && viewHeight > 0
                && textHeight > viewHeight;
        }
    }

    /**
     * 仿照TextView#onDraw()方法
     */
    @Override
    protected void onDraw(Canvas canvas) {
        restartMarqueeIfNeeded();

        super.onDraw(canvas);

        // 再次绘制背景色,覆盖下面由TextView绘制的文本,视情况可以不调用`super.onDraw(canvas);`
        // 如果没有背景色则使用默认颜色
        Drawable background = getBackground();
        if (background != null) {
            background.draw(canvas);
        } else {
            canvas.drawColor(DEFAULT_BG_COLOR);
        }

        canvas.save();

        canvas.translate(0, 0);

        // 实现左右跑马灯
        if (mOrientation == HORIZONTAL) {
            if (mMarquee != null && mMarquee.isRunning()) {
                final float dx = -mMarquee.getScroll();
                canvas.translate(dx, 0.0F);
            }

            getLayout().draw(canvas, null, null, 0);

            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                final float dx = mMarquee.getGhostOffset();
                canvas.translate(dx, 0.0F);
                getLayout().draw(canvas, null, null, 0);
            }
        } else {
            // 实现上下跑马灯
            if (mMarquee != null && mMarquee.isRunning()) {
                final float dy = -mMarquee.getScroll();
                canvas.translate(0.0F, dy);
            }

            getLayout().draw(canvas, null, null, 0);

            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
                final float dy = mMarquee.getGhostOffset();
                canvas.translate(0.0F, dy);
                getLayout().draw(canvas, null, null, 0);
            }
        }

        canvas.restore();
    }
}

Marquee

private static final class Marquee {
    // 修改此字段设置重跑时间间隔 - 对应不足点2
    private static final int MARQUEE_DELAY = 1200;

    // 修改此字段设置跑动速度 - 对应不足点1
    private static final int MARQUEE_DP_PER_SECOND = 30;

    private static final byte MARQUEE_STOPPED = 0x0;
    private static final byte MARQUEE_STARTING = 0x1;
    private static final byte MARQUEE_RUNNING = 0x2;

    private static final String METHOD_GET_FRAME_TIME = "getFrameTime";

    private final WeakReference<MarqueeTextView> mView;
    private final Choreographer mChoreographer;

    private byte mStatus = MARQUEE_STOPPED;
    private final float mPixelsPerSecond;
    private float mMaxScroll;
    private float mMaxFadeScroll;
    private float mGhostStart;
    private float mGhostOffset;
    private float mFadeStop;
    private int mRepeatLimit;

    private float mScroll;
    private long mLastAnimationMs;

    Marquee(MarqueeTextView v) {
        final float density = v.getContext().getResources().getDisplayMetrics().density;
        mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
        mView = new WeakReference<>(v);
        mChoreographer = Choreographer.getInstance();
    }

    private final Choreographer.FrameCallback mTickCallback = frameTimeNanos -> tick();

    private final Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mStatus = MARQUEE_RUNNING;
            mLastAnimationMs = getFrameTime();
            tick();
        }
    };

    /**
     * `getFrameTime`是隐藏api,此处使用反射调用,高系统版本可能失效,可使用某些方案绕过此限制
     */
    @SuppressLint("PrivateApi")
    private long getFrameTime() {
        try {
            Class<? extends Choreographer> clz = mChoreographer.getClass();
            Method getFrameTime = clz.getDeclaredMethod(METHOD_GET_FRAME_TIME);
            getFrameTime.setAccessible(true);
            return (long) getFrameTime.invoke(mChoreographer);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    private final Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            if (mStatus == MARQUEE_RUNNING) {
                if (mRepeatLimit >= 0) {
                    mRepeatLimit--;
                }
                start(mRepeatLimit);
            }
        }
    };

    void tick() {
        if (mStatus != MARQUEE_RUNNING) {
            return;
        }

        mChoreographer.removeFrameCallback(mTickCallback);

        final MarqueeTextView textView = mView.get();
        if (textView != null && (textView.isFocused() || textView.isSelected() || textView.isMarquee())) {
            long currentMs = getFrameTime();
            long deltaMs = currentMs - mLastAnimationMs;
            mLastAnimationMs = currentMs;
            float deltaPx = deltaMs / 1000F * mPixelsPerSecond;
            mScroll += deltaPx;
            if (mScroll > mMaxScroll) {
                mScroll = mMaxScroll;
                mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
            } else {
                mChoreographer.postFrameCallback(mTickCallback);
            }
            textView.invalidate();
        }
    }

    void stop() {
        mStatus = MARQUEE_STOPPED;
        mChoreographer.removeFrameCallback(mStartCallback);
        mChoreographer.removeFrameCallback(mRestartCallback);
        mChoreographer.removeFrameCallback(mTickCallback);
        resetScroll();
    }

    private void resetScroll() {
        mScroll = 0.0F;
        final MarqueeTextView textView = mView.get();
        if (textView != null) textView.invalidate();
    }

    void start(int repeatLimit) {
        if (repeatLimit == 0) {
            stop();
            return;
        }
        mRepeatLimit = repeatLimit;
        final MarqueeTextView textView = mView.get();
        if (textView != null && textView.getLayout() != null) {
            mStatus = MARQUEE_STARTING;
            mScroll = 0.0F;

            // 分别计算左右和上下跑动所需的数据
            if (textView.getOrientation() == HORIZONTAL) {
                int viewWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
                    textView.getCompoundPaddingRight();
                float lineWidth = textView.getLayout().getLineWidth(0);
                float gap = viewWidth / 3.0F;
                mGhostStart = lineWidth - viewWidth + gap;
                mMaxScroll = mGhostStart + viewWidth;
                mGhostOffset = lineWidth + gap;
                mFadeStop = lineWidth + viewWidth / 6.0F;
                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
            } else {
                int viewHeight = textView.getHeight() - textView.getCompoundPaddingTop() -
                    textView.getCompoundPaddingBottom();
                float textHeight = textView.getLayout().getHeight();
                float gap = viewHeight / 3.0F;
                mGhostStart = textHeight - viewHeight + gap;
                mMaxScroll = mGhostStart + viewHeight;
                mGhostOffset = textHeight + gap;
                mFadeStop = textHeight + viewHeight / 6.0F;
                mMaxFadeScroll = mGhostStart + textHeight + textHeight;
            }

            textView.invalidate();
            mChoreographer.postFrameCallback(mStartCallback);
        }
    }

    float getGhostOffset() {
        return mGhostOffset;
    }

    float getScroll() {
        return mScroll;
    }

    float getMaxFadeScroll() {
        return mMaxFadeScroll;
    }

    boolean shouldDrawLeftFade() {
        return mScroll <= mFadeStop;
    }

    boolean shouldDrawTopFade() {
        return mScroll <= mFadeStop;
    }

    boolean shouldDrawGhost() {
        return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
    }

    boolean isRunning() {
        return mStatus == MARQUEE_RUNNING;
    }

    boolean isStopped() {
        return mStatus == MARQUEE_STOPPED;
    }
}

效果

跑马灯

happy~

android 多行textview间切换跑马灯效果 以前一直认为 textview 这个跑马灯效果很是鸡肋,必须有焦点才可以使用,用着麻烦。 但项目需求没有办法,只能尝试实现。   起初在网上找了一些资料殊途同归讲的都差不多,还有一些是瞎转载没试过就发到网上来的(ps:发自内心的12万分鄙视,没试就发上来,抄下来之后还以为自己写错了,检查了好半天。。)。 无非就是要有焦点才可看到跑马灯效果,实际应用起来麻烦透顶。 本来已经放弃用textv 阅读详情

相关推荐

Android TextView 跑马灯,解决跑马灯抖动问题

Android TextView 跑马灯实现 解决跑马灯抖动 跑马灯抖动原因 如果有很短时间的定时任务在持续调用 setText(),比如1秒重复刷新TextView, 则会导致跑马灯重新开始,从而出现跑马灯抖动问题!!! 解决办法: 重写 TextView 的 setText 方法,如下代码所示。 代码实现 package com.xxx.view; import android.content.Context; import android.graphics.Rect; import android.

mowwwcom的专栏 2362

Android TextView设置跑马灯效果

【前言】      在Textview设置的宽度有限,而需要显示的文字又比较多的情况下,往往需要给Textview设置跑马灯效果才能让用户完整地看到所有设置的文字,所以给TextView设置跑马灯效果的需求是很常见的 一、新手设置跑马灯效果 ...

逍遥阁 2万+

安卓保证TextView一直跑马灯的做法

问题 需要跑马灯的TextView和其他的View在同一个ViewGroup下, 有时候需要改动这个ViewGroup下其他View的LayoutParam,这个修改过程可能导致TextView暂时性失去焦点,表现为跑马灯效果会被重置,很难看。 解决方法 一般是采取隔离政策,把其他需要改动LayoutParam的View和跑马灯的TextView隔离开,用一个FrameLayout隔离一下。 <FrameLayout android:layout_width="0dp"

自带橡皮的铅笔的博客 471

Android TextView跑马灯设置 的 两种方法

文章目录1、在代码里 代码动态设计1)xml 文件2) 功能文件2、在 xml 文件 里配置 跑马灯3、实现一个 Activity 多个TextView 跑马灯效果1) 自定义的 MarqueeTextView 类2、 xml 文件3)、功能文件 MainActivity 所谓跑马灯 就是 文本框里面的 文字超过了 ,文本框的 宽度 ,怎轮播显示。 1、在代码里 代码动态设计 1)xml 文件...

Venus 的博客 2万+

Android TextView跑马灯效果

对于很多时候会有显示不全的问题,也就是会以省略号的形式显示,对于可以有焦点的Textview可以很好实现跑马灯效果,但是无焦点的TextView就不好操作,下面我来解决这个问题。

热衷于Android Framework和系统APP的修改经验分享 1532

Android中实现TextView跑马灯效果的两种方法

本文还有配套的精品资源,点击获取 简介:在Android应用开发中,TextView组件可以实现跑马灯效果,即在有限的空间内让文本无限滚动。本文将介绍两种实现TextView跑马灯效果的方法,包括使用XML布局属性 ellipsize 和 marqueeRepeatLimit ,以及通过自定义跑马灯动画类 CustomMarqueeAni...

weixin_42430341的博客 4093

Android TextView跑马灯不动 及属性

&lt;TextView android:text="每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()," android:layout_width="60dp" android:layout_height="wrap_content" ...

u010112268的博客 4459

MarqueeTextView

MarqueeTextView 轻量级打call工具。 可修改文字内容/颜色,背景颜色,滚动速度。 支持双击暂停滚动,滑动屏幕调整文字位置。 Project 截图:

Android TextView实现跑马灯效果

在实际的开发中,我们有时候需要滚动的显示信息,这就是我们所说的跑马灯效果。 Android中的TextView可以很容易的显示这个效果,只需要添加以下属性就可以了 android:ellipsize="marquee" android:marqueeRepeatLimit="marquee_forever" android:singleLine="true" 但是这样子有一个缺点,就是这种状

HeyNine的博客 1万+

android TextView实现跑马灯效果

主要为大家详细介绍了android TextView实现跑马灯效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

BQ25798双电源输入4节电池升降压充电与最大功率跟踪于光伏板应用

介绍一颗具有窄范围直流电压NVDC、路径管理PPMT、光伏板最大功率跟踪及反向放电OTG升降压充电控制器,带I2C接口能对内部寄存器做控制

linkedin_38804946的博客 1675

Android实现多个跑马灯效果,多个文本框TextView的跑马灯

Android实现多个跑马灯效果,多个文本框TextView的跑马灯   一、新建一个Marquee类继承TextView 由于默认的跑马灯效果是需要TextView的方法isFocused为true的时候才会跑动的,而默认时,只有一个TextView处于focused状态,解决办法就是让所有要实现跑马灯效果的TextView都处于focused状态,因此要改写TextView类。于是新建一

AMinfo的专栏 8179

CVAT自动标注避坑指南:从nuctl安装到YOLOv5模型部署的完整流程

本文详细介绍了在CVAT中部署YOLOv5自动标注功能的完整流程与避坑指南。重点解决了从nuctl工具安装、项目创建时因网络问题导致的镜像拉取失败,到最终模型函数部署与CVAT集成的关键步骤。针对国内网络环境,提供了实用的镜像替换方案,帮助用户顺利完成基于CPU的自动化标注环境搭建。

pytorch8learner的博客 229

Android TextView设置跑马灯无效?

一、想要效果 想让Android的TextView实现跑马灯效果,垂直或者横向文字滚动。 二、出现问题 设置了跑马灯相关属性后发现没有达到滚动的效果,在xml中设置属性如下 <TextView android:id="@+id/outdoor_weather_tips_tv" and...

FrancisBingo的博客 2166

硬件原理图分析

文章目录1 硬件原理图分析1.1 MCU部分1.2 NB部分1.3 电源部分 1 硬件原理图分析 1.1 MCU部分 MCU接口部分没啥需要注意,都是常规操作。 1.2 NB部分 U4是用来防静电的。 由于NB的以上几个引脚只支持1.8V,所以这里需要3.3V转1.8V的电路。只要把握住谁改变电平就好分析是怎么实现电平转换的了。 1.3 电源部分 图中的MOS是用来实现USB和电池同时插入时只能USB供电功能的。 ...

木子皿--啥都不会的菜鸟 8169

Visual Studio 2022调试Godot 4 C#项目全攻略:从环境配置到断点调试

本文详细介绍了如何配置Visual Studio 2022以高效调试Godot 4 C#项目。从环境搭建、外部编辑器设置到两种核心调试模式(附加到进程与启动配置)的实战指南,帮助开发者构建无缝的工业级工作流,解决编码乱码、断点失效等常见问题,大幅提升开发与问题排查效率。

bush的博客 391

Android当中TextView跑马灯效果不生效的原因

使用如下的设置方法跑马灯无效果 <TextView android:id="@+id/music_name_tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="marquee" android:focusable="true" android:focusableInTo

你倒是敲代码鸭. 1649

1020空心杯电机驱动设计与ESP32实战

空心杯电机是一种高动态响应的直流微特电机,其驱动本质是解决电感负载下的大di/dt启动、反电动势耦合与电流纹波抑制问题。核心原理在于建立包含内阻、电感和反电动势的等效电路模型,并据此匹配开关器件、PWM参数与采样闭环。技术价值体现在小体积下实现毫秒级响应、低噪声(>30kHz)、高精度电流保护(±50mA)及热鲁棒性。典型应用于微型航模、手持云台与精密执行器等对功率密度和动态性能敏感的嵌入式系统。本文以1020规格空心杯电机为对象,基于ESP32平台完成从MOSFET选型、32kHz硬件PWM生成到INA2

weixin_42298164的博客 580

Android TextView设置跑马灯失效

TextView失效在网上有详细的解决方案,大部分时候都能够很好的解决问题以上就是一个简单的固定宽高的TextView,显然文字的长度超过了控件的宽度ellipsize属性官方给出的解释如下:意思就是使文本中比视图宽度长的单词省略号,而不是在在中间断开。该属性的枚举值为主要是设置省略号的位置,而其中MARQUEE就是本文要学习的跑马灯效果。

安卓开发&音视频开发 1330

数通基础(基本网络通信原理、IP地址如何配置、DHCP、DNS域名系统、路由技术基础、静态路由 、TCP和UDP、vlan虚拟局域网、trunk、三层交换)

我们知道了1、电脑开机,通过dhcp获取ip地址2、电脑输入域名,www.baidu.com电脑把www.baidu.com发到自己的dns服务器3、dns服务器把域名解析成对应的ip地址4、电脑向百度服务器发送一个请求。

qq_53235556的博客 6475

全新DEIM有效涨点改进目录 | 本专栏持续更新500+篇内容 | 包含各种顶会顶刊卷积、注意力、特征融合模块、有效特征聚合提取模块,上采样模块、下采样模块,二次创新模块、独家创新等几百种创新点改进

🚀【CVPR2025DEIM改进专栏速览】🔥 专栏提供500+篇目标检测创新改进方案,涵盖卷积/注意力/特征融合等创新改进点,均源自顶会论文及实战经验。支持多领域应用(工业/医疗/遥感等),配套完整教程和答疑服务。亮点包括: 1️⃣ 持续更新SOTA模型改进方案 2️⃣ 小白友好型实验操作指南 3️⃣ 跨学科科研支持(已助非计算机专业发表论文) 4️⃣ 提供虚拟环境配置方案。 订阅可获完整项目代码及交流群服务,适合研究生科研论文改进需求。价格呈上涨趋势,早订更优惠!

Ai缝合怪、助力小伙伴高效发论文 2040

安卓开发之多个TextView跑马灯

跑马灯就是TextView容纳不下内容,需要靠播放动画的形式来显示 1、通常,很多人只需要实现一个TextView的跑马灯,只需要在将textview的属性中加入下面这段即可(具体参数代表的意思,请参考此处http://www.cnblogs.com/over140/archive/2010/08/27/1809745.html) android:layout_marginRight="20d

greenchina1992的专栏 1260

Python爬虫实战:如何通过解析JSON数据抓取抖音视频评论

本文介绍了如何使用Python爬虫抓取抖音视频评论数据。请求头伪造:通过使用随机的User-Agent,模拟真实用户的请求。代理池:使用代理IP来避免被封禁。JSON解析:直接从API接口获取JSON格式的数据,避免了动态加载内容的限制。需要注意的是,抓取网站数据时应遵守网站的爬虫协议,避免过度抓取影响网站性能或违反法律法规。希望本文能够帮助你掌握如何抓取和分析抖音视频评论数据!

2201_76125261的博客 3359

TextView: android中TextView实现单行跑马灯

textview中内容过长的话自动换行,但是调用measureText函数时发现返回值很不准确,单位也不确定,是pixel还是dip,都不准。后来想起textview中有个内容过长加省略号的属性,即ellipsize,可以较偷懒地解决这个问题,哈哈~ 用法如下: 在xml中 android:ellipsize = "end"    省略号在结尾 android:ellips

yuanlaishini2010的专栏 1598

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

上一篇: 修改linphone-sdk-android-第四篇
下一篇: Kotlin-KCP的应用-第一篇
guodongAndroid
博客等级 码龄10年 16粉丝 19原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值