android开发分享Android TextView跑马灯实现原理及方法实例

前言自定义view实现的跑马灯一直没有实现类似 android textview 的跑马灯首尾相接的效果,所以一直想看看android textview 的跑马灯是如何实现本文主要探秘 android

上述就是android开发分享Android TextView跑马灯实现原理及方法实例的全部内容,如果对大家有所用处且需要了解更多关于Android学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

前言

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

android开发分享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跑马灯实现的文章就介绍到这了,更多相关android textview跑马灯内容请搜索<计算机技术网(www.ctvol.com)!!>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<计算机技术网(www.ctvol.com)!!>!

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/addevelopment/1091016.html

(0)
上一篇 2022年5月10日
下一篇 2022年5月10日

精彩推荐