Android TextView跑马灯实现原理及方法实例

目录
  • 前言
  • 探秘
    • TextView#onDraw
    • Marquee
    • 小结
  • 应用
    • MarqueeTextView
    • Marquee
    • 效果
  • 总结

前言

自定义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,跑马灯实现原理豁然明亮

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

小结

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

应用

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

  • 无法设置跑动速度
  • 无法设置重跑间隔时长
  • 无法实现上下跑动

以上第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;
     }
 }

效果

总结

到此这篇关于Android TextView跑马灯实现的文章就介绍到这了,更多相关Android TextView跑马灯内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android使用TextView跑马灯效果

    老规矩,先上图看效果. 说明 TextView的跑马灯效果也就是指当你只想让TextView单行显示,可是文本内容却又超过一行时,自动从左往右慢慢滑动显示的效果就叫跑马灯效果. 其实,TextView实现跑马灯效果很简单,因为官方已经实现了,你只需要通过设置几个属性即可.而且,相关的资料其实网上也有一大堆了,之所以还写这篇博客出来是因为,网上好多人的博客都是只贴代码的啊,好一点的就是附带几张图片,可是这是动画效果啊,不动起来,谁知道跑马灯效果到底长什么样,到底是不是自己想要的效果啊(不会只有题主

  • Android TextView实现跑马灯效果的方法

    本文为大家分享一个非常简单但又很常用的控件,跑马灯状态的TextView.当要显示的文本长度太长,又不想换行时用它来显示文本,一来可以完全的显示出文本,二来效果也挺酷,实现起来超级简单,所以,何乐不为.先看下效果图: 代码实现 TextView自带了跑马灯功能,只要把它的ellipsize属性设置为marquee就可以了.但有个前提,就是TextView要处于被选中状态才能有效果,看到这,我们就很自然的自定义一个控件,写出以下代码: public class MarqueeTextView ex

  • Android自定义TextView跑马灯效果

    Android自带的跑马灯效果不太好控制,还必须要满足条件才能有效果,而且速度不受控制.前面我的博客中有一篇就是用Android自带的跑马灯效果的,但是基于不同的使用效果,这里在网上找到了一个更好的方法.沿用了作者的一些方法,但是添加了更好的扩展功能,和大家一起分享.这里面有控制往左往右两个方向的实现. 1.首先是简单的布局main.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android&quo

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

     条件: 1.android:ellipsize="marquee" 2.TextView必须单行显示,即内容必须超出TextView大小 3.TextView要获得焦点才能滚动 mTVText.setText("超过文本长度的数据"); mTVText.setSingleLine(true);设置单行显示 mTVText.setEllipsize(TruncateAt.MARQUEE);设置跑马灯显示效果 TextView.setHorizontallyScrol

  • Android基于TextView属性android:ellipsize实现跑马灯效果的方法

    本文实例讲述了Android基于TextView属性android:ellipsize实现跑马灯效果的方法.分享给大家供大家参考,具体如下: Android系统中TextView实现跑马灯效果,必须具备以下几个条件: 1.android:ellipsize="marquee" 2.TextView必须单行显示,即内容必须超出TextView大小 3.TextView要获得焦点才能滚动 XML代码: android:ellipsize="marquee", andro

  • Android自定义textview实现竖直滚动跑马灯效果

    本文实例为大家分享了Android自定义textview实现跑马灯效果的具体代码,供大家参考,具体内容如下 xml布局 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.c

  • Android基于TextView不获取焦点实现跑马灯效果

    本文实例讲述了Android基于TextView不获取焦点实现跑马灯效果.分享给大家供大家参考,具体如下: 1. 写一个类继承TextView package com.example.tt; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.widget.TextView; public class ScrollingText

  • android使用TextView实现跑马灯效果

    本文实例为大家分享了android使用TextView实现跑马灯效果的具体代码,供大家参考,具体内容如下 先上效果图:此为静态图,实际动态中文字匀速向左滑动. 实现步骤: 第一步:创建好布局页面 <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.c

  • Android 实现不依赖焦点和选中的TextView跑马灯

    前言 之前有写一篇TextView跑马灯的效果,后来实际项目中有发现新的问题,比如还是无法自动跑,文本超过了显示区域就截取的问题,今天换了一种思路来实现,更简单更好用. 正文 代码实现: public class MarqueeTextView extends TextView { /** 是否停止滚动 */ private boolean mStopMarquee; private String mText; private float mCoordinateX; private float

  • Android中使用TextView实现文字跑马灯效果

    通常情况下我们想实现文字的走马灯效果需要在xml文件中这样设置 <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:ellipsize="marquee" android:focusable="true" android:

随机推荐