使用VerticalRollingTextView实现单行文字垂直滚动

VerticalRollingTextView是一个竖直方向无限循环滚动显示单行文本的控件。非常轻量级,直接继承View实现,使用Paint绘制文本,不依赖任何第三方。

开源项目地址:
https://github.com/shubowen/VerticalRollingTextView

运行效果图:
   

一、项目使用

(1).添加项目依赖。
dependencies {
   com.xiaosu:VerticalRollingTextView:1.2.0 
}

(2).声明布局文件。

在xml中可以添加如下自定义属性。

    
    
    
    
    
    
    
    

(3).添加数据集合。
List textArray = ...;
verticalRollingView.setDataSetAdapter(new DataSetAdapter(textArray) {
    @Override
    protected String text(String s) {
        return s;
    }
});

(4).操作控件。
// 开始滚动
verticalRollingView.run();
// 停止滚动
verticalRollingView.stop();
// 设置点击监听
verticalRollingView.setOnItemClickListener(new VerticalRollingTextView.OnItemClickListener() {
    @Override
    public void onItemClick(VerticalRollingTextView view, int index) {
        // your code
    }
});

二、源码分析

实现原理:
VerticalRollingTextView直接继承自View类,重写onDraw()方法,在onDraw()中调用Paint绘制文字。文字距离父容器顶部有一个偏移量,内部使用了一个自定义动画InternalAnimation,在动画执行过程中,不断的减小偏移量并重写绘制,达到文字向上滚动的效果。

下面是核心代码的实现。

首先是类的初始化。在构造方法中,初始化画笔,解析xml中添加的自定义属性,计算出文字的top到ascent的距离为后面使用。
public VerticalRollingTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    
    // 初始化画笔,绘制文字使用
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setColor(Color.BLACK);
    mPaint.setTypeface(Typeface.DEFAULT);

    // 解析自定义属性
    parseAttrs(context, attrs);

    // 计算出top到ascent的距离,后面在onDraw()方法中调用
    Paint.FontMetricsInt metricsInt = mPaint.getFontMetricsInt();
    mTextTopToAscentOffset = metricsInt.ascent - metricsInt.top;

    mAnimation.setDuration(mDuration);
}
关于FontMetricsInt类中的top和ascent属性,可参考下图。
使用VerticalRollingTextView实现单行文字垂直滚动_第1张图片

调用run()方法会开启动画。调用postDelayed()方法一直循环执行mRollingTask任务,在每一次任务中,调用View的startAnimation()方法启动InternalAnimation动画。
// 开启动画
public void run() {
    if (isRunning) {
        return;
    }

    isRunning = true;
    post(mRollingTask);
}

private InternalAnimation mAnimation = new InternalAnimation();

// 循环的任务
Runnable mRollingTask = new Runnable() {
    @Override
    public void run() {
        mAnimationEnded = false;
        startAnimation(mAnimation);
        postDelayed(this, mAnimInterval);
    }
};

InternalAnimation是一个自定义动画类,继承自Animation类,实现了applyTransformation()方法。在applyTransformation()中,使用参数interpolatedTime在动画执行中由0.0递增到1.0的特性,通过计算不断的减小mCurrentOffsetY的值,并调用postInvalidate()方法触发View类的onDraw()方法调用。当mCurrentOffsetY减小到endValue时,动画结束,调用animationEnd()方法。

class InternalAnimation extends Animation {
        float startValue;
        float endValue;

        // 动画执行过程中,interpolatedTime由0.0递增到1.0
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            if (mAnimationEnded)
                return;

            // 不断的修改mCurrentOffsetY的值,mCurrentOffsetY取值范围:startValue~endValue
            mCurrentOffsetY = evaluate(interpolatedTime, startValue, endValue);
            // mCurrentOffsetY的值由startValue减小到endValue时,动画结束
            if (mCurrentOffsetY == endValue) {
                animationEnd();
            }
            // 调用onDraw()方法绘制
            postInvalidate();
        }

        // 为成员变量startValue和endValue赋值
        public void updateValue(float startValue, float endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }
    }

    // float估值器
    float evaluate(float fraction, float startValue, float endValue) {
        return startValue + fraction * (endValue - startValue);
    }

滚动文字的绘制在onDraw()方法中完成。在onDraw()中有一个初始化操作,计算偏移量,并把值添加到InternalAnimation中,这段代码只执行一次。在每一轮的动画中,onDraw()方法通过索引mCurrentIndex和mNextIndex获取需要显示的文字,调用canvas的drawText()方法将文字绘制出来。
// 绘制文本
@Override
protected void onDraw(Canvas canvas) {
    if (mDataSetAdapter == null || mDataSetAdapter.isEmpty()) {
        return;
    }

    // 获取当前需要显示的文字
    String text1 = mDataSetAdapter.getText(mCurrentIndex);
    // 获取下一个需要显示的文字
    String text2 = mDataSetAdapter.getText(mNextIndex);

    // 只需要进行一次测量
    if (mOrgOffsetY == -1) {
        mPaint.getTextBounds(text1, 0, text1.length(), bounds);
        // mOffset:文字底部到父视图顶部的距离
        mOffset = (getHeight() + bounds.height()) * 0.5f;
        // mOrgOffsetY:文字在垂直方向居中时,向上滚动的偏移量
        mOrgOffsetY = mCurrentOffsetY = mOffset - mTextTopToAscentOffset;
        // 初始化InternalAnimation中的成员变量
        mAnimation.updateValue(mOrgOffsetY, -2 * mTextTopToAscentOffset);
    }

    // 不断的绘制文本。x:始终为0不变;y:不断的减小
    canvas.drawText(text1, 0, mCurrentOffsetY, mPaint);
    canvas.drawText(text2, 0, mCurrentOffsetY + mOffset + mTextTopToAscentOffset, mPaint);
}

一轮动画执行完毕,animationEnd()方法被调用。在该方法中,增加mCurrentIndex和mNextIndex的值以获取下一次轮播的文字,并重置索引mCurrentOffsetY。
// 动画执行完成
public void animationEnd() {
    // 索引加一,取下一条数据
    mCurrentIndex++;
    // 因为文字是一直循环滚动下去的,这里使用取余数确保不会越界
    mCurrentIndex = mCurrentIndex < mDataSetAdapter.getItemCount() ? mCurrentIndex
            : mCurrentIndex % mDataSetAdapter.getItemCount();
    //计算下一个待显示文字角标
    confirmNextIndex();
    // 该轮动画完成,接下来要开始新一轮动画,重置mCurrentOffsetY变量
    mCurrentOffsetY = mOrgOffsetY;
    mAnimationEnded = true;
}

// 计算当前索引的下一条数据
private void confirmNextIndex() {
    // mNextIndex是在当前索引mCurrentIndex之后
    mNextIndex = mCurrentIndex + 1;
    // 如果mCurrentIndex已经是最后一条,mNextIndex再从0开始
    mNextIndex = mNextIndex < mDataSetAdapter.getItemCount() ? mNextIndex : 0;
}

你可能感兴趣的:(开源项目,开源项目,文字滚动)