您现在的位置是:群英 > 开发技术 > 移动开发
Textview怎样实现自下往上的跑马灯效果
Admin发表于 2022-05-17 11:49:36726 次浏览
这篇文章给大家分享的是“Textview怎样实现自下往上的跑马灯效果”,文中的讲解内容简单清晰,对大家认识和了解都有一定的帮助,对此感兴趣的朋友,接下来就跟随小编一起了解一下“Textview怎样实现自下往上的跑马灯效果”吧。


前言

自定义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;
     }
 }

效果

总结


以上就是关于“Textview怎样实现自下往上的跑马灯效果”的相关知识,感谢各位的阅读,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注群英网络,小编每天都会为大家更新不同的知识。

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。

相关信息推荐
2022-05-10 16:33:53 
摘要:给大家带来一篇关于python字符串常用方法有哪些的相关教程文章,内容涉及到Python、python教程等相关内容,更多关于python的内容希望能够帮助到大家。
2022-05-31 17:49:03 
摘要:java实现自定义注解的方法:首先新建一个java文件,并自定义一个注解;然后使用元注解定义各项;最后定义该注解的业务逻辑。
2022-08-27 17:03:52 
摘要:webpack中怎么压缩打包html资源?下面本篇文章就来给大家简单介绍一下webpack压缩打包html资源的方法,希望对大家有所帮助!
云活动
推荐内容
热门关键词
热门信息
群英网络助力开启安全的云计算之旅
立即注册,领取新人大礼包
  • 联系我们
  • 24小时售后:4006784567
  • 24小时TEL :0668-2555666
  • 售前咨询TEL:400-678-4567

  • 官方微信

    官方微信
Copyright  ©  QY  Network  Company  Ltd. All  Rights  Reserved. 2003-2019  群英网络  版权所有   茂名市群英网络有限公司
增值电信经营许可证 : B1.B2-20140078   粤ICP备09006778号
免费拨打  400-678-4567
免费拨打  400-678-4567 免费拨打 400-678-4567 或 0668-2555555
微信公众号
返回顶部
返回顶部 返回顶部