利用SurfaceView实现垂直滚动显示文字跑马灯view效果
作者:圣光啊那个敌人值得一战
前一阵在做的项目有一个循环滚动显示通知内容的需求,当时赶时间,就简单的套到了ScrollView里然后计算控件高度让它滚动显示,但是问题明显是很多的,因为数据更新的方式比较奇葩(服务端通知客户端),所以经常会有数据刷新重新加载view造成高度计算错误的情况出现,而且这个项目是运行在公司自己生产的设备上当做考勤机来使用,所以正常情况下会一直运行10几天。。。所以在压力测试的时候这个问题相当的突出。
所以喽。。。这个问题不改我会让经理打死的。本来就想着在网上找个现成的库用一下,有轮子不用白不用啊,但是搜了搜,都只有单行滚动切换的,就算在其基础上改,不但受限于别人的思路,也会让效果大打折扣。所以在工位上思索了下(我强行不改这个需求的风险有多大?恩,蛮高的),决定自己实现。
本来决定思路的时候是想着继承个view然后开线程循环更新文字位置显示来着,但是感觉好像大概会让绘制过于频繁(其实还好,就是觉得),这时候想起来了以前绘制更新大量图片的时候用到的SurfaceView与线程的搭配蛮舒服的,所以就拍脑袋决定,就这个了!
要用SurfaceView来实现这个需求,我们得看一下一个回调接口,SurfaceHolder.Callback,其注释描述如下:
/**
* A client may implement this interface to receive information about
* changes to the surface. When used with a {@link SurfaceView}, the
* Surface being held is only available between calls to
* {@link #surfaceCreated(SurfaceHolder)} and
* {@link #surfaceDestroyed(SurfaceHolder)}. The Callback is set with
* {@link SurfaceHolder#addCallback SurfaceHolder.addCallback} method.
*/
public interface Callback {
/**
* This is called immediately after the surface is first created.
* Implementations of this should start up whatever rendering code
* they desire. Note that only one thread can ever draw into
* a {@link Surface}, so you should not draw into the Surface here
* if your normal rendering will be in another thread.
*
* @param holder The SurfaceHolder whose surface is being created.
*/
public void surfaceCreated(SurfaceHolder holder);
/**
* This is called immediately after any structural changes (format or
* size) have been made to the surface. You should at this point update
* the imagery in the surface. This method is always called at least
* once, after {@link #surfaceCreated}.
*
* @param holder The SurfaceHolder whose surface has changed.
* @param format The new PixelFormat of the surface.
* @param width The new width of the surface.
* @param height The new height of the surface.
*/
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height);
/**
* This is called immediately before a surface is being destroyed. After
* returning from this call, you should no longer try to access this
* surface. If you have a rendering thread that directly accesses
* the surface, you must ensure that thread is no longer touching the
* Surface before returning from this function.
*
* @param holder The SurfaceHolder whose surface is being destroyed.
*/
public void surfaceDestroyed(SurfaceHolder holder);
}
来大家看,这个接口里面需要实现三个方法,而且名字起得都很直白亲民,创建方法,改变方法,销毁方法,这就意味着我们能够在线程改变SurfaceView的holder内容时根据这三个方法的实现来管理SurfaceView,这样就能在创建SurfaceView的时候得到holder,改变的时候检查线程情况,销毁的时候处理线程,比如下面这样:
@Override
public void surfaceCreated(SurfaceHolder holder) {
this.holder = holder;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mThread != null)
mThread.isRun = true;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mThread != null)
mThread.isRun = false;
}
上面的mThread就是用来绘制并提交文字位置实现滚动效果的线程了,而开始绘制前,我们肯定不能瞎画啊,所以得先初始化一下滚动的效果参数等
public VerticalMarqueeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
if (isInEditMode()) {
//防止编辑预览界面报错
return;
}
init(attrs, defStyleAttr);
}
private float mTextSize = 100; //字体大小
private int mTextColor = Color.RED; //字体的颜色
private boolean mIsRepeat;//是否重复滚动
private int mStartPoint;// 开始滚动的位置 0是从上面开始 1是从下面开始
private int mDirection;//滚动方向 0 向上滚动 1向下滚动
private int mSpeed;//滚动速度
private void init(AttributeSet attrs, int defStyleAttr) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.VerticalMarqueeTextView, defStyleAttr, 0);
mTextColor = a.getColor(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_textColor, Color.RED);
mTextSize = a.getDimension(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_textSize, 48);
mIsRepeat = a.getBoolean(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_isRepeat, false);
mStartPoint = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_startPoint, 0);
mDirection = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_direction, 0);
mSpeed = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_speed, 20);
if (mSpeed < 20) {
mSpeed = 20;
}
a.recycle();
point = new Point(0, 0);
holder = this.getHolder();
holder.addCallback(this);
mTextPaint = new TextPaint();
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextAlign(Paint.Align.LEFT);
setZOrderOnTop(true);//使surfaceview放到最顶层
getHolder().setFormat(PixelFormat.TRANSLUCENT);//使窗口支持透明度
}
attr中的属性定义如下:
<declare-styleable name="VerticalMarqueeTextView">
<attr name="VerticalMarqueeTextView_textColor" format="color" />
<attr name="VerticalMarqueeTextView_textSize" format="dimension" />
<attr name="VerticalMarqueeTextView_isRepeat" format="boolean" />
<attr name="VerticalMarqueeTextView_startPoint" format="integer" >
<enum name="start" value="0" />
<enum name="end" value="1" />
</attr>
<attr name="VerticalMarqueeTextView_direction" format="integer" >
<enum name="up" value="0" />
<enum name="down" value="1" />
</attr>
<attr name="VerticalMarqueeTextView_speed" format="integer" />
</declare-styleable>
获取完了在布局里就设置好的属性后,我们再初始化些point类啊,TextPaint类,给holder设置回调啊就差不多了,初始工作这就算完成,哎~戈薇刚才是不是在心里骂了句扯淡?对,我们还没有初始化位置信息,话说我为什么不把位置信息也一并放入构造函数里初始化呢?以为我的需求是个通知啊各位,它是会经常变得,所以,我得在每次文本内容改变的时候计算,比如这样:
public void setText(String msg) {
if (!TextUtils.isEmpty(msg)) {
measurementsText(msg);
}
}
protected void measurementsText(String msg) {
margueeString = msg;
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
mTextPaint.setStrokeWidth(0.5f);
mTextPaint.setFakeBoldText(true);
textWidth = (int) mTextPaint.measureText(margueeString);//因为有这句话,所以得等控件绘制完在进行通知显示,对,就是用handler
int height = getHeight() - getPaddingTop() - getPaddingBottom();
if (mStartPoint == 0)
currentY = 50;
else
currentY = height;
}
话说,各位看见我上面加的注释了没?啥?我还加注释了?哼,大拳拳捶你胸口!都不认真看!
恩,把被锤吐血的同学拉下去,我们继续。各位看,在上面我们获取完了初始的位置后,就真的是差不多了,现在只需要开个线程不停循环计算然后绘制并提交改变就行了,首先,我们再初始化一下(主要这个方法写杂了,没法归类啊)
/**
* 开始滚动
*
* @param isStop 是否停止显示
* @param sec 停止显示时间
*/
public void startScroll(boolean isStop, int sec) {
if (mThread != null) {
return;
}
this.isStop = isStop;
this.sec = sec * 1000;
/*
* 设置绘制多行文本所需参数
*
* @param string 文本
* @param textPaint 文本笔
* @param canvas canvas
* @param point 点
* @param width 宽度
* @param align layout的对齐方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种。
* @param spacingmult 相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
* @param spacingadd 在基础行距上添加多少
* @param includepad 参数未知(不知道啥,反正填false)
* @param height 绘制高度
*/
staticLayout = new StaticLayout(margueeString, mTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.5f, 0, false);
//获取所有字的累加高度
textHeight = staticLayout.getHeight();
isFirstDraw = true;
mThread = new MarqueeViewThread(holder);//创建一个绘图线程
mThread.isRun = true;
mThread.start();
}
在这里不得不说一下StaticLayout这个类,大家知道,一般显示文字都只是显示一行或者定好TextView的宽度好让字多的时候换行,但是我们这里是没有用到TextView的,所以文字换行这个事情就显得很麻烦了,但是好在Android已经为我们提供好了这个叫做StaticLayout的类,它的注释我就不给大家看了(主要我没看懂),主要是用这个类方便换行,它会根据高度适配绘制多行文本,讲道理在坐的各位,可以的。
说完参数和初始位置后,就到了我们的重点了各位(敲黑板!),那就是本次的重头戏,线程循环绘制文本了撒,例子如下:
/**
* 是否继续滚动
*/
private boolean isGo = true;
/**
* 线程
*/
class MarqueeViewThread extends Thread {
private final SurfaceHolder holder;
public boolean isRun;//是否在运行
public MarqueeViewThread(SurfaceHolder holder) {
this.holder = holder;
isRun = true;
}
public void onDraw() {
try {
synchronized (holder) {
if (TextUtils.isEmpty(margueeString)) {
Thread.sleep(1000);//睡眠时间为1秒
return;
}
if (isGo) {
final Canvas canvas = holder.lockCanvas();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int contentHeight = getHeight() - paddingTop - paddingBottom;
if (mDirection == 0) {//向上滚动
if (currentY <= -textHeight) {
currentY = contentHeight;
if (!mIsRepeat) {//如果是不重复滚动
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
return;
}
} else {
currentY -= sepY;
}
currentY -= sepY;
} else {// 向下滚动
if (currentY >= textHeight + sepY + 10) {
currentY = 0;
if (!mIsRepeat) {//如果是不重复滚动
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
return;
}
} else {
currentY += sepY;
}
}
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//绘制透明色
textCenter(canvas, currentY);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
if (isFirstDraw) {
mHandler.sendEmptyMessageDelayed(STOP_ROLL, 50);//暂停显示5秒
isFirstDraw = false;
}
}
Thread.sleep(mSpeed);//睡眠时间为移动的频率~~
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (isRun) {
onDraw();
}
}
}
其中的计算方式大体上就是动态的改变当前的文字高度进行绘制,文字移动的频率就是线程的睡眠频率,改变后锁定并提交改变~
大体效果如下:
整体例子如下:
···
/**
- Created by lip on 2016/12/23.
- <p>
- 竖直滚动跑马灯
*/
public class VerticalMarqueeView extends SurfaceView implements SurfaceHolder.Callback {
public Context mContext;
private float mTextSize = 100; //字体大小
private int mTextColor = Color.RED; //字体的颜色
private boolean mIsRepeat;//是否重复滚动
private int mStartPoint;// 开始滚动的位置 0是从上面开始 1是从下面开始
private int mDirection;//滚动方向 0 向上滚动 1向下滚动
private int mSpeed;//滚动速度
private SurfaceHolder holder;
private TextPaint mTextPaint;
private MarqueeViewThread mThread;
private String margueeString;
private int textWidth = 0, textHeight = 0;
public int currentY = 0;// 当前y的位置
public double sepY = 1;//每一步滚动的距离
private Point point;//点,没啥用,懒得弄了
private StaticLayout staticLayout;//绘制多行文本所需类
private boolean isFirstDraw = true;//是否为某条文本的第一次绘制~~
private boolean isStop = false;
private int sec = 5000;
public VerticalMarqueeView(Context context) {
this(context, null);
}
public VerticalMarqueeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VerticalMarqueeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
if (isInEditMode()) {
//防止编辑预览界面报错
return;
}
init(attrs, defStyleAttr);
}
private void init(AttributeSet attrs, int defStyleAttr) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.VerticalMarqueeTextView, defStyleAttr, 0);
mTextColor = a.getColor(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_textColor, Color.RED);
mTextSize = a.getDimension(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_textSize, 48);
mIsRepeat = a.getBoolean(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_isRepeat, false);
mStartPoint = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_startPoint, 0);
mDirection = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_direction, 0);
mSpeed = a.getInt(R.styleable.VerticalMarqueeTextView_VerticalMarqueeTextView_speed, 20);
if (mSpeed < 5) {
mSpeed = 5;
}
a.recycle();
point = new Point(0, 0);
holder = this.getHolder();
holder.addCallback(this);
mTextPaint = new TextPaint();
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextAlign(Paint.Align.LEFT);
setZOrderOnTop(true);//使surfaceview放到最顶层
getHolder().setFormat(PixelFormat.TRANSLUCENT);//使窗口支持透明度
}
public void setText(String msg) {
if (!TextUtils.isEmpty(msg)) {
measurementsText(msg);
}
}
protected void measurementsText(String msg) {
margueeString = msg;
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
mTextPaint.setStrokeWidth(0.5f);
mTextPaint.setFakeBoldText(true);
textWidth = (int) mTextPaint.measureText(margueeString);
int height = getHeight() - getPaddingTop() - getPaddingBottom();
if (mStartPoint == 0)
currentY = 50;
else
currentY = height;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
this.holder = holder;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mThread != null)
mThread.isRun = true;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mThread != null)
mThread.isRun = false;
}
/**
* 线程是否在运行
*
* @return 结果
*/
public boolean isThreadRunning() {
return mThread != null && mThread.isRun && !mThread.isInterrupted();
}
/**
* 开始滚动
*
* @param isStop 是否停止显示
* @param sec 停止显示时间
*/
public void startScroll(boolean isStop, int sec) {
if (mThread != null) {
return;
}
this.isStop = isStop;
this.sec = sec * 1000;
/*
* 设置绘制多行文本所需参数
*
* @param string 文本
* @param textPaint 文本笔
* @param canvas canvas
* @param point 点
* @param width 宽度
* @param align layout的对齐方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种。
* @param spacingmult 相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
* @param spacingadd 在基础行距上添加多少
* @param includepad 参数未知(不知道啥,反正填false)
* @param height 绘制高度
*/
staticLayout = new StaticLayout(margueeString, mTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.5f, 0, false);
//获取所有字的累加高度
textHeight = staticLayout.getHeight();
isFirstDraw = true;
mThread = new MarqueeViewThread(holder);//创建一个绘图线程
mThread.isRun = true;
mThread.start();
}
/**
* 停止滚动
*/
public void stopScroll() {
if (mThread != null) {
mThread.isRun = false;
}
mThread = null;
}
/**
* 暂停播放
*/
public void pauseScroll() {
if (mThread != null) {
mThread.isRun = false;
mThread = null;
}
}
/**
* 恢复播放
*/
public void restartRoll() {
mThread = new MarqueeViewThread(holder);
mThread.isRun = true;
mThread.start();
}
/**
* 请空内容
*/
public void clearText() {
if (mThread != null && mThread.isRun) {
margueeString = "";
}
}
/**
* 是否继续滚动
*/
private boolean isGo = true;
/**
* 线程
*/
class MarqueeViewThread extends Thread {
private final SurfaceHolder holder;
public boolean isRun;//是否在运行
public MarqueeViewThread(SurfaceHolder holder) {
this.holder = holder;
isRun = true;
}
public void onDraw() {
try {
synchronized (holder) {
if (TextUtils.isEmpty(margueeString)) {
Thread.sleep(1000);//睡眠时间为1秒
return;
}
if (isGo) {
final Canvas canvas = holder.lockCanvas();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int contentHeight = getHeight() - paddingTop - paddingBottom;
if (mDirection == 0) {//向上滚动
if (currentY <= -textHeight) {
currentY = contentHeight;
if (!mIsRepeat) {//如果是不重复滚动
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
return;
}
} else {
currentY -= sepY;
}
currentY -= sepY;
} else {// 向下滚动
if (currentY >= textHeight + sepY + 10) {
currentY = 0;
if (!mIsRepeat) {//如果是不重复滚动
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
return;
}
} else {
currentY += sepY;
}
}
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//绘制透明色
textCenter(canvas, currentY);
holder.unlockCanvasAndPost(canvas);//结束锁定画图,并提交改变。
if (isFirstDraw) {
mHandler.sendEmptyMessageDelayed(STOP_ROLL, 50);//暂停显示5秒
isFirstDraw = false;
}
}
Thread.sleep(mSpeed);//睡眠时间为移动的频率~~
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (isRun) {
onDraw();
}
}
}
/**
* 绘制多行文本
*
* @param canvas canvas
* @param height 绘制高度
*/
private void textCenter(Canvas canvas, int height) {
canvas.save();
canvas.translate(0, height);
staticLayout.draw(canvas);
canvas.restore();
}
public static final int ROLL_OVER = 100;//一条播放完毕
public static final int STOP_ROLL = 200;//停止滚动
public static final int START_ROLL = 300;//开始滚动
public static final int STOP_THREAT = 400;//停止线程a
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case ROLL_OVER:
stopScroll();
if (mOnMargueeListener != null) {
mOnMargueeListener.onRollOver();
}
break;
case STOP_ROLL:
isGo = false;
mHandler.sendEmptyMessageDelayed(START_ROLL, sec);
break;
case START_ROLL:
isGo = true;
break;
case STOP_THREAT:
stopScroll();
break;
}
}
};
/**
* dip转换为px
*
* @param context
* @param dpValue
* @return
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public void reset() {
int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int contentHeight = getHeight() - getPaddingTop() - getPaddingBottom();
if (mStartPoint == 0)
currentY = 0;
else
currentY = contentHeight;
}
/**
* 滚动回调
*/
public interface OnMargueeListener {
void onRollOver();//滚动完毕
}
OnMargueeListener mOnMargueeListener;
public void setOnMargueeListener(OnMargueeListener mOnMargueeListener) {
this.mOnMargueeListener = mOnMargueeListener;
}
}
VerticalMarqueeView
maven
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
Add the dependency
dependencies { compile 'com.github.LIPKKKK:VerticalMarqueeView:v1.0.3' }
how to use
<com.lip.verticalmarqueeviewdemo.view.VerticalMarqueeView
android:id="@+id/lip_VerticalView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6"
app:VerticalMarqueeTextView_textColor = "#000"
app:VerticalMarqueeTextView_textSize = "20sp"
app:VerticalMarqueeTextView_isRepeat = "true"
app:VerticalMarqueeTextView_startPoint = "0"
app:VerticalMarqueeTextView_direction = "1"
app:VerticalMarqueeTextView_speed = "20" />
```
VerticalMarqueeTextView_textColor : 文字颜色
VerticalMarqueeTextView_textSize : 文字大小
VerticalMarqueeTextView_isRepeat : 是否重复
VerticalMarqueeTextView_startPoint : 开始位置
VerticalMarqueeTextView_direction : 滚动方向
VerticalMarqueeTextView_speed : 滚动速度
具体使用我放到了git上,地址如下:
[LIPKKKK](https://github.com/LIPKKKK/VerticalMarqueeView)
谢谢支持~