ScrollTextView
新博客已经迁移至:Dkaishu的博客 ,http://dkaishu.com/
很多 App 中都有滚动展示文字需求,特别适合用来展示简短的通知和广告等内容,不多说,先简单看下效果,
具体的 UI 可以自己定义,我已经其开原到 github :https://github.com/Dkaishu/ScrollTextView,并可以在 gradle 中引用到工程,只需一行代码。如何使用见上面链接。推荐结合源码阅读下面内容。
实现思路
滚动文字的实现思路很多种,我觉得简单易实现的的大致可分为两种:一是,类似常见 Banner 的思路,实现 banner,将TextView 添加到ViewGroup中,这种方式扩展性强,如果滚动显示的不仅仅是文字,还有其他View时,可考虑,淘宝首页的滚动条应该是这样实现的;二、自定义View继承 TextView 或 View 等,这种方式代码量会小很多,性能好很多,使用也简单很多,当需求单一时,优先考虑。此 ScrollTextView 是基于第二种思路实现,下面是几个关键点。
支持的属性
因为应用场景单一,所以不必考虑过多属性,核心的几个:文字颜色大小、背景、滚动速度、展示时间等;其他:不同条目的点击事件监听,当前显示文字的监听等。这样看来,我们直接继承 View 就完全可以满足需求了。
/**
* 默认文字颜色
*/
private static final int DEFAULT_TEXT_COLOR = Color.BLACK;
/**
* 默认文字大小(单位sp)
*/
private static final int DEFAULT_TEXT_SIZE = 16;
/**
* 单行模式
*/
private static final boolean SINGLE_LINE = true;
/**
* 单行显示时,默认带省略号(单行模式下才有效)
*/
private static final boolean ELLIPSIS = true;
/**
* 默认文字滚动时间(滚动控制速度)
*/
private static final long DEFAULT_SCROLL_TIME = 500;
/**
* 默认文字切换间隔时间
*/
private static final long DEFAULT_SPAN_TIME = 3000;
/**
* 文字滚动时间,默认500ms
*/
private long scrollTime = DEFAULT_SCROLL_TIME;
/**
* 文字切换间隔时间,默认3000ms
*/
private long spanTime = DEFAULT_SPAN_TIME;
/**
* 是否单行模式
*/
private boolean isSingleLine;
/**
* 单行显示下是否自带省略号
*/
private boolean isEllipsis;
自定义 View
构造方法:
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ScrollTextLayout, defStyleAttr, 0);
int textColor = DEFAULT_TEXT_COLOR;
float textSize = sp2px(context, DEFAULT_TEXT_SIZE);
if (typedArray != null) {
textColor = typedArray.getColor(R.styleable.ScrollTextLayout_textColor, textColor);
textSize = typedArray.getDimension(R.styleable.ScrollTextLayout_textSize, textSize);
isSingleLine = typedArray.getBoolean(R.styleable.ScrollTextLayout_singleLine, SINGLE_LINE);
isEllipsis = typedArray.getBoolean(R.styleable.ScrollTextLayout_ellipsis, ELLIPSIS);
typedArray.recycle();
}
mPaint = new Paint();
mPaint.setColor(textColor);
mPaint.setTextSize(textSize);
mPaint.setAntiAlias(true);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
mTextHeight = fontMetrics.bottom - fontMetrics.top;
mTextOffsetY = -fontMetrics.top;
mIndexMap = new HashMap<>();
mTextInfos = new LinkedList<>();
mEllipsisTextInfos = new ArrayList<>();
setOnClickListener(this);
}
重写 onMeasure 与 dispatchDraw
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
int textMaxWidth = 0;
if (mContents != null && mContents.size() > 0) {
textMaxWidth = textTypeSetting(measuredWidth - getPaddingLeft() - getPaddingRight(), mContents);
mCurrentTextInfos = mTextInfos.poll();
mTextInfos.offer(mCurrentTextInfos);
}
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
measuredWidth = textMaxWidth + getPaddingLeft() + getPaddingRight();
}
if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
measuredHeight = (int) (mTextHeight + getPaddingBottom() + getPaddingTop());
}
setMeasuredDimension(measuredWidth, measuredHeight);
startTextScroll();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mCurrentTextInfos != null && mCurrentTextInfos.size() > 0) {
for (TextInfo textInfo : mCurrentTextInfos) {
canvas.drawText(textInfo.text,
textInfo.x + getPaddingLeft(),
textInfo.y + getPaddingTop() + mTop,
mPaint);
}
}
if (mTextInfos.size() > 1) {
List<TextInfo> nextTextInfos = mTextInfos.peek();
if (nextTextInfos != null && nextTextInfos.size() > 0) {
for (TextInfo textInfo : nextTextInfos) {
canvas.drawText(textInfo.text, textInfo.x + getPaddingLeft(), textInfo.y + getPaddingTop() + mTop
+ mTextHeight + getPaddingTop() + getPaddingBottom(), mPaint);
}
}
}
}
hander.postDelayed 控制时间
Handler mHandler = new Handler();
Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (mTextInfos.size() > 1) {
mValueAnimator = ValueAnimator.ofFloat(0.0f, -1.0f);
mValueAnimator.setDuration(scrollTime);
final OnScrollListener sl = mIndexMap.get(mCurrentTextInfos).getScrollListener();
mValueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
if (sl != null) sl.onScrollStart(mCurrentTextInfos);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mTop = 0;
mCurrentTextInfos = mTextInfos.poll();
mTextInfos.offer(mCurrentTextInfos);
if (sl != null) sl.onScrollEnd(mCurrentTextInfos);
startTextScroll();
}
@Override
public void onAnimationCancel(Animator animation) {
mTop = 0;
}
});
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
mTop = (int) (value * (mTextHeight + getPaddingTop() + getPaddingBottom()));
invalidate();
}
});
mValueAnimator.start();
}
}
};
难点:文字排版
private int textTypeSetting(float maxParentWidth, List<String> list) {
// 清空数据及初始化数据
mTextInfos.clear();
mIndexMap.clear();
mEllipsisTextInfos.clear();
mEllipsisTextWidth = 0f;
// 初始化省略号
if (isSingleLine && isEllipsis) {
String ellipsisText = "...";
for (int i = 0; i < ellipsisText.length(); i++) {
char ch = ellipsisText.charAt(i);
float[] widths = new float[1];
String srt = String.valueOf(ch);
mPaint.getTextWidths(srt, widths);
TextInfo textInfo = new TextInfo();
textInfo.text = srt;
textInfo.x = mEllipsisTextWidth;
textInfo.y = mTextOffsetY;
mEllipsisTextInfos.add(textInfo);
mEllipsisTextWidth += widths[0];
}
}
// 文字排版
float maxWidth = 0;
// 文字排版最大宽度
float tempMaxWidth = 0f;
int index = 0;
for (String text : list) {
if (isNullOrEmpty(text)) {
continue;
}
// 排版文字当前的宽度
float textWidth = 0;
// 文字信息集合
List<TextInfo> textInfos = new ArrayList<TextInfo>();
if (isSingleLine) {
// 临时文字信息集合
List<TextInfo> tempTextInfos = new ArrayList<TextInfo>();
// 单行排不下
boolean isLess = false;
// 省略号的起始位置
float ellipsisStartX = 0;
for (int j = 0; j < text.length(); j++) {
char ch = text.charAt(j);
float[] widths = new float[1];
String srt = String.valueOf(ch);
mPaint.getTextWidths(srt, widths);
TextInfo textInfo = new TextInfo();
textInfo.text = srt;
textInfo.x = textWidth;
textInfo.y = mTextOffsetY;
textWidth += widths[0];
if (textWidth <= maxParentWidth - mEllipsisTextWidth) // 当排版的宽度小于等于最大宽度去除省略号长度时
{
textInfos.add(textInfo);
ellipsisStartX = textWidth;
} else if (textWidth <= maxParentWidth) // 当排版宽度小于最大宽度时
{
tempTextInfos.add(textInfo);
} else
// 最大宽度排版不下
{
isLess = true;
break;
}
}
if (isLess) {
tempMaxWidth = maxParentWidth;
for (TextInfo ellipsisTextInfo : mEllipsisTextInfos) {
TextInfo textInfo = new TextInfo();
textInfo.text = ellipsisTextInfo.text;
textInfo.x = (ellipsisTextInfo.x + ellipsisStartX);
textInfo.y = ellipsisTextInfo.y;
textInfos.add(textInfo);
}
} else {
tempMaxWidth = textWidth;
textInfos.addAll(tempTextInfos);
}
if (tempMaxWidth > maxWidth) {
maxWidth = tempMaxWidth;
}
mTextInfos.offer(textInfos);
if (mScrollClickListeners != null && mScrollClickListeners.size() > index) {
mIndexMap.put(textInfos, new ListenersInfo(mScrollClickListeners.get(index), null));
}
if (mScrollListeners != null && mScrollListeners.size() > index) {
mIndexMap.get(textInfos).setScrollListener(mScrollListeners.get(index));
}
index++;
} else {
for (int j = 0; j < text.length(); j++) {
char ch = text.charAt(j);
float[] widths = new float[1];
String srt = String.valueOf(ch);
mPaint.getTextWidths(srt, widths);
TextInfo textInfo = new TextInfo();
textInfo.text = srt;
textInfo.x = textWidth;
textInfo.y = mTextOffsetY;
textWidth += widths[0];
if (textWidth > maxParentWidth) // 当排版宽度小于最大宽度时
{
tempMaxWidth = maxParentWidth;
mTextInfos.offer(textInfos);
if (mScrollClickListeners != null && mScrollClickListeners.size() > index) {
mIndexMap.put(textInfos, new ListenersInfo(mScrollClickListeners.get(index), null));
}
if (mScrollListeners != null && mScrollListeners.size() > index) {
mIndexMap.get(textInfos).setScrollListener(mScrollListeners.get(index));
}
textInfos = new ArrayList<TextInfo>();
textInfo.x = 0;
textInfo.y = mTextOffsetY;
textWidth = widths[0];
}
textInfos.add(textInfo);
}
if (textWidth > tempMaxWidth) {
tempMaxWidth = textWidth;
}
mTextInfos.offer(textInfos);
if (tempMaxWidth > maxWidth) {
maxWidth = tempMaxWidth;
}
if (mScrollClickListeners != null && mScrollClickListeners.size() > index) {
mIndexMap.put(textInfos, new ListenersInfo(mScrollClickListeners.get(index), null));
}
if (mScrollListeners != null && mScrollListeners.size() > index) {
mIndexMap.get(textInfos).setScrollListener(mScrollListeners.get(index));
}
index++;
}
}
return (int) maxWidth;
}
滚动文字的监听
/**
* 描述:内容滚动动画开始与结束的监听事件
*/
public interface OnScrollListener {
/**
* @param passedTextInfos 动画开始前显示的文字
*/
void onScrollStart(List<TextInfo> passedTextInfos);
/**
* @param incommingTextInfos
*/
void onScrollEnd(List<TextInfo> incommingTextInfos);
}
设置数据源
/**
* @param list 滚动内容集合
* @param scrollClickListeners 滚动内容的点击监听集合 @nullable
* @param scrollListeners 滚动动画开始与停止监听集合 @nullable
*/
public void setTextContent(List<String> list, List<OnScrollClickListener> scrollClickListeners
, List<OnScrollListener> scrollListeners) {
this.mContents = list;
this.mScrollClickListeners = scrollClickListeners;
this.mScrollListeners = scrollListeners;
requestLayout();
invalidate();
}