美文网首页程序员Android开发Android知识
圣光啊!小肉看我敲代码----利用SurfaceView与线程搭

圣光啊!小肉看我敲代码----利用SurfaceView与线程搭

作者: 圣光啊那个敌人值得一战 | 来源:发表于2017-02-28 20:42 被阅读0次

    利用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();
                }
            }
        }
    

    其中的计算方式大体上就是动态的改变当前的文字高度进行绘制,文字移动的频率就是线程的睡眠频率,改变后锁定并提交改变~
    大体效果如下:

    GIF.gif

    整体例子如下:
    ···
    /**

    • 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)
    
    谢谢支持~

    相关文章

      网友评论

        本文标题:圣光啊!小肉看我敲代码----利用SurfaceView与线程搭

        本文链接:https://www.haomeiwen.com/subject/kpuzwttx.html