圣光啊!小肉看我敲代码----利用SurfaceView与线程搭配实现垂直滚动显示文字跑马灯view效果

利用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中的属性定义如下:


        
        
        
        
            
            
        
        
            
            
        
        
    

获取完了在布局里就设置好的属性后,我们再初始化些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.
  • 竖直滚动跑马灯
    */

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

 
```

VerticalMarqueeTextView_textColor : 文字颜色
VerticalMarqueeTextView_textSize : 文字大小
VerticalMarqueeTextView_isRepeat : 是否重复
VerticalMarqueeTextView_startPoint : 开始位置
VerticalMarqueeTextView_direction : 滚动方向
VerticalMarqueeTextView_speed : 滚动速度

具体使用我放到了git上,地址如下:
[LIPKKKK](https://github.com/LIPKKKK/VerticalMarqueeView)

谢谢支持~

你可能感兴趣的:(圣光啊!小肉看我敲代码----利用SurfaceView与线程搭配实现垂直滚动显示文字跑马灯view效果)