上述就是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跑马灯内容请搜索<计算机技术网(www.ctvol.com)!!>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<计算机技术网(www.ctvol.com)!!>!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/addevelopment/1091016.html