利用SurfaceView實(shí)現(xiàn)垂直滾動(dòng)顯示文字跑馬燈view效果
作者:圣光啊那個(gè)敵人值得一戰(zhàn)
前一陣在做的項(xiàng)目有一個(gè)循環(huán)滾動(dòng)顯示通知內(nèi)容的需求,當(dāng)時(shí)趕時(shí)間,就簡單的套到了ScrollView里然后計(jì)算控件高度讓它滾動(dòng)顯示,但是問題明顯是很多的,因?yàn)閿?shù)據(jù)更新的方式比較奇葩(服務(wù)端通知客戶端),所以經(jīng)常會(huì)有數(shù)據(jù)刷新重新加載view造成高度計(jì)算錯(cuò)誤的情況出現(xiàn),而且這個(gè)項(xiàng)目是運(yùn)行在公司自己生產(chǎn)的設(shè)備上當(dāng)做考勤機(jī)來使用,所以正常情況下會(huì)一直運(yùn)行10幾天。。。所以在壓力測試的時(shí)候這個(gè)問題相當(dāng)?shù)耐怀觥?/p>
所以嘍。。。這個(gè)問題不改我會(huì)讓經(jīng)理打死的。本來就想著在網(wǎng)上找個(gè)現(xiàn)成的庫用一下,有輪子不用白不用啊,但是搜了搜,都只有單行滾動(dòng)切換的,就算在其基礎(chǔ)上改,不但受限于別人的思路,也會(huì)讓效果大打折扣。所以在工位上思索了下(我強(qiáng)行不改這個(gè)需求的風(fēng)險(xiǎn)有多大?恩,蠻高的),決定自己實(shí)現(xiàn)。
本來決定思路的時(shí)候是想著繼承個(gè)view然后開線程循環(huán)更新文字位置顯示來著,但是感覺好像大概會(huì)讓繪制過于頻繁(其實(shí)還好,就是覺得),這時(shí)候想起來了以前繪制更新大量圖片的時(shí)候用到的SurfaceView與線程的搭配蠻舒服的,所以就拍腦袋決定,就這個(gè)了!
要用SurfaceView來實(shí)現(xiàn)這個(gè)需求,我們得看一下一個(gè)回調(diào)接口,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);
}
來大家看,這個(gè)接口里面需要實(shí)現(xiàn)三個(gè)方法,而且名字起得都很直白親民,創(chuàng)建方法,改變方法,銷毀方法,這就意味著我們能夠在線程改變SurfaceView的holder內(nèi)容時(shí)根據(jù)這三個(gè)方法的實(shí)現(xiàn)來管理SurfaceView,這樣就能在創(chuàng)建SurfaceView的時(shí)候得到holder,改變的時(shí)候檢查線程情況,銷毀的時(shí)候處理線程,比如下面這樣:
@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就是用來繪制并提交文字位置實(shí)現(xiàn)滾動(dòng)效果的線程了,而開始繪制前,我們肯定不能瞎畫啊,所以得先初始化一下滾動(dòng)的效果參數(shù)等
public VerticalMarqueeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
if (isInEditMode()) {
//防止編輯預(yù)覽界面報(bào)錯(cuò)
return;
}
init(attrs, defStyleAttr);
}
private float mTextSize = 100; //字體大小
private int mTextColor = Color.RED; //字體的顏色
private boolean mIsRepeat;//是否重復(fù)滾動(dòng)
private int mStartPoint;// 開始滾動(dòng)的位置 0是從上面開始 1是從下面開始
private int mDirection;//滾動(dòng)方向 0 向上滾動(dòng) 1向下滾動(dòng)
private int mSpeed;//滾動(dòng)速度
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>
獲取完了在布局里就設(shè)置好的屬性后,我們再初始化些point類啊,TextPaint類,給holder設(shè)置回調(diào)啊就差不多了,初始工作這就算完成,哎~戈薇剛才是不是在心里罵了句扯淡?對,我們還沒有初始化位置信息,話說我為什么不把位置信息也一并放入構(gòu)造函數(shù)里初始化呢?以為我的需求是個(gè)通知啊各位,它是會(huì)經(jīng)常變得,所以,我得在每次文本內(nèi)容改變的時(shí)候計(jì)算,比如這樣:
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);//因?yàn)橛羞@句話,所以得等控件繪制完在進(jìn)行通知顯示,對,就是用handler
int height = getHeight() - getPaddingTop() - getPaddingBottom();
if (mStartPoint == 0)
currentY = 50;
else
currentY = height;
}
話說,各位看見我上面加的注釋了沒?啥?我還加注釋了?哼,大拳拳捶你胸口!都不認(rèn)真看!
恩,把被錘吐血的同學(xué)拉下去,我們繼續(xù)。各位看,在上面我們獲取完了初始的位置后,就真的是差不多了,現(xiàn)在只需要開個(gè)線程不停循環(huán)計(jì)算然后繪制并提交改變就行了,首先,我們再初始化一下(主要這個(gè)方法寫雜了,沒法歸類啊)
/**
* 開始滾動(dòng)
*
* @param isStop 是否停止顯示
* @param sec 停止顯示時(shí)間
*/
public void startScroll(boolean isStop, int sec) {
if (mThread != null) {
return;
}
this.isStop = isStop;
this.sec = sec * 1000;
/*
* 設(shè)置繪制多行文本所需參數(shù)
*
* @param string 文本
* @param textPaint 文本筆
* @param canvas canvas
* @param point 點(diǎn)
* @param width 寬度
* @param align layout的對齊方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三種。
* @param spacingmult 相對行間距,相對字體大小,1.5f表示行間距為1.5倍的字體高度。
* @param spacingadd 在基礎(chǔ)行距上添加多少
* @param includepad 參數(shù)未知(不知道啥,反正填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);//創(chuàng)建一個(gè)繪圖線程
mThread.isRun = true;
mThread.start();
}
在這里不得不說一下StaticLayout這個(gè)類,大家知道,一般顯示文字都只是顯示一行或者定好TextView的寬度好讓字多的時(shí)候換行,但是我們這里是沒有用到TextView的,所以文字換行這個(gè)事情就顯得很麻煩了,但是好在Android已經(jīng)為我們提供好了這個(gè)叫做StaticLayout的類,它的注釋我就不給大家看了(主要我沒看懂),主要是用這個(gè)類方便換行,它會(huì)根據(jù)高度適配繪制多行文本,講道理在坐的各位,可以的。
說完參數(shù)和初始位置后,就到了我們的重點(diǎn)了各位(敲黑板!),那就是本次的重頭戲,線程循環(huán)繪制文本了撒,例子如下:
/**
* 是否繼續(xù)滾動(dòng)
*/
private boolean isGo = true;
/**
* 線程
*/
class MarqueeViewThread extends Thread {
private final SurfaceHolder holder;
public boolean isRun;//是否在運(yùn)行
public MarqueeViewThread(SurfaceHolder holder) {
this.holder = holder;
isRun = true;
}
public void onDraw() {
try {
synchronized (holder) {
if (TextUtils.isEmpty(margueeString)) {
Thread.sleep(1000);//睡眠時(shí)間為1秒
return;
}
if (isGo) {
final Canvas canvas = holder.lockCanvas();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int contentHeight = getHeight() - paddingTop - paddingBottom;
if (mDirection == 0) {//向上滾動(dòng)
if (currentY <= -textHeight) {
currentY = contentHeight;
if (!mIsRepeat) {//如果是不重復(fù)滾動(dòng)
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
return;
}
} else {
currentY -= sepY;
}
currentY -= sepY;
} else {// 向下滾動(dòng)
if (currentY >= textHeight + sepY + 10) {
currentY = 0;
if (!mIsRepeat) {//如果是不重復(fù)滾動(dòng)
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
return;
}
} else {
currentY += sepY;
}
}
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//繪制透明色
textCenter(canvas, currentY);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
if (isFirstDraw) {
mHandler.sendEmptyMessageDelayed(STOP_ROLL, 50);//暫停顯示5秒
isFirstDraw = false;
}
}
Thread.sleep(mSpeed);//睡眠時(shí)間為移動(dòng)的頻率~~
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (isRun) {
onDraw();
}
}
}
其中的計(jì)算方式大體上就是動(dòng)態(tài)的改變當(dāng)前的文字高度進(jìn)行繪制,文字移動(dòng)的頻率就是線程的睡眠頻率,改變后鎖定并提交改變~
大體效果如下:

整體例子如下:
···
/**
- Created by lip on 2016/12/23.
- <p>
- 豎直滾動(dòng)跑馬燈
*/
public class VerticalMarqueeView extends SurfaceView implements SurfaceHolder.Callback {
public Context mContext;
private float mTextSize = 100; //字體大小
private int mTextColor = Color.RED; //字體的顏色
private boolean mIsRepeat;//是否重復(fù)滾動(dòng)
private int mStartPoint;// 開始滾動(dòng)的位置 0是從上面開始 1是從下面開始
private int mDirection;//滾動(dòng)方向 0 向上滾動(dòng) 1向下滾動(dòng)
private int mSpeed;//滾動(dòng)速度
private SurfaceHolder holder;
private TextPaint mTextPaint;
private MarqueeViewThread mThread;
private String margueeString;
private int textWidth = 0, textHeight = 0;
public int currentY = 0;// 當(dāng)前y的位置
public double sepY = 1;//每一步滾動(dòng)的距離
private Point point;//點(diǎn),沒啥用,懶得弄了
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()) {
//防止編輯預(yù)覽界面報(bào)錯(cuò)
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;
}
/**
* 線程是否在運(yùn)行
*
* @return 結(jié)果
*/
public boolean isThreadRunning() {
return mThread != null && mThread.isRun && !mThread.isInterrupted();
}
/**
* 開始滾動(dòng)
*
* @param isStop 是否停止顯示
* @param sec 停止顯示時(shí)間
*/
public void startScroll(boolean isStop, int sec) {
if (mThread != null) {
return;
}
this.isStop = isStop;
this.sec = sec * 1000;
/*
* 設(shè)置繪制多行文本所需參數(shù)
*
* @param string 文本
* @param textPaint 文本筆
* @param canvas canvas
* @param point 點(diǎn)
* @param width 寬度
* @param align layout的對齊方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三種。
* @param spacingmult 相對行間距,相對字體大小,1.5f表示行間距為1.5倍的字體高度。
* @param spacingadd 在基礎(chǔ)行距上添加多少
* @param includepad 參數(shù)未知(不知道啥,反正填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);//創(chuàng)建一個(gè)繪圖線程
mThread.isRun = true;
mThread.start();
}
/**
* 停止?jié)L動(dòng)
*/
public void stopScroll() {
if (mThread != null) {
mThread.isRun = false;
}
mThread = null;
}
/**
* 暫停播放
*/
public void pauseScroll() {
if (mThread != null) {
mThread.isRun = false;
mThread = null;
}
}
/**
* 恢復(fù)播放
*/
public void restartRoll() {
mThread = new MarqueeViewThread(holder);
mThread.isRun = true;
mThread.start();
}
/**
* 請空內(nèi)容
*/
public void clearText() {
if (mThread != null && mThread.isRun) {
margueeString = "";
}
}
/**
* 是否繼續(xù)滾動(dòng)
*/
private boolean isGo = true;
/**
* 線程
*/
class MarqueeViewThread extends Thread {
private final SurfaceHolder holder;
public boolean isRun;//是否在運(yùn)行
public MarqueeViewThread(SurfaceHolder holder) {
this.holder = holder;
isRun = true;
}
public void onDraw() {
try {
synchronized (holder) {
if (TextUtils.isEmpty(margueeString)) {
Thread.sleep(1000);//睡眠時(shí)間為1秒
return;
}
if (isGo) {
final Canvas canvas = holder.lockCanvas();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int contentHeight = getHeight() - paddingTop - paddingBottom;
if (mDirection == 0) {//向上滾動(dòng)
if (currentY <= -textHeight) {
currentY = contentHeight;
if (!mIsRepeat) {//如果是不重復(fù)滾動(dòng)
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
return;
}
} else {
currentY -= sepY;
}
currentY -= sepY;
} else {// 向下滾動(dòng)
if (currentY >= textHeight + sepY + 10) {
currentY = 0;
if (!mIsRepeat) {//如果是不重復(fù)滾動(dòng)
mHandler.sendEmptyMessage(ROLL_OVER);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
return;
}
} else {
currentY += sepY;
}
}
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//繪制透明色
textCenter(canvas, currentY);
holder.unlockCanvasAndPost(canvas);//結(jié)束鎖定畫圖,并提交改變。
if (isFirstDraw) {
mHandler.sendEmptyMessageDelayed(STOP_ROLL, 50);//暫停顯示5秒
isFirstDraw = false;
}
}
Thread.sleep(mSpeed);//睡眠時(shí)間為移動(dòng)的頻率~~
}
}
} 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;//停止?jié)L動(dòng)
public static final int START_ROLL = 300;//開始滾動(dòng)
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轉(zhuǎn)換為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;
}
/**
* 滾動(dòng)回調(diào)
*/
public interface OnMargueeListener {
void onRollOver();//滾動(dòng)完畢
}
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 : 是否重復(fù)
VerticalMarqueeTextView_startPoint : 開始位置
VerticalMarqueeTextView_direction : 滾動(dòng)方向
VerticalMarqueeTextView_speed : 滾動(dòng)速度
具體使用我放到了git上,地址如下:
[LIPKKKK](https://github.com/LIPKKKK/VerticalMarqueeView)
謝謝支持~