需要背景:
一个在 launcher 一直循环滚动的跑马效果
0. 循环滚动时,标题与标题的间隔能够自定义(原生的貌似不可以)
1. 当切屏或离开页面时,跑马灯停止更新
2. 虽然离开了跑马灯页面,但标题内容可能会被更新,因此回到其页面时,更新标题内容并判断是否需要滚动
3. 当标题过短,不用循环滚动显示时,标题居中
思路:
初始化
判断是否需要滚动,通过标题的宽度与控件宽度比较,分为两种情况
TYPE_NO_SCROLL
、TYPE_SCROLL
标题循环滚动时,如果自定义标题与标题直接的间隔呢,
mTitleWithSpace
,就是添加了间隔的标题,可以通过定义mSpaceCount
来定义间隔计算标题向左侧滑动的最大距离
mMaxScroll
private void initMarqueeText() {
if (!TextUtils.isEmpty(mTitleText)) {
initPaint();
//重置相关属性
mWidth = getWidth();
mHeight = getHeight() - getPaddingTop() - getPaddingBottom();
mTextWidth = measureTextWidth(String.valueOf(mTitleText));
//文字滚动判断
if (mTextWidth > mWidth) {
mTitleType = TYPE_SCROLL;
mTitleWithSpace = mTitleText;
for (int i = 0; i < mSpaceCount; i++) {
mTitleWithSpace += " ";
}
mDrawingText = mTitleWithSpace;
mTitleCharList = mDrawingText.toCharArray();
mMaxScroll = mTextWidth + mSpaceCount * oneSpaceWidth();
mTextPaint.setTextAlign(Paint.Align.LEFT);
startIndex = 0;
} else {
//不滚动
mDrawingText = mTitleText;
mTitleType = TYPE_NO_SCROLL;
}
mTextDefalutXShaft = 0;
}
}
onDraw
根据不同的类型,draw 不同类型,其中
mCenterNoScroll
表示当标题能全部在控件上显示,而不用滚动时,标题是否居中核心是如何循环滚动,当右侧可以有多余的空间时,先判断这空间可以放多少个文字,然后会去拿标题数据的文字
private void drawMarqueeText(Canvas canvas) {
...(省略)
switch (mTitleType) {
case TYPE_NO_SCROLL:
LogUtils.i(TAG, "跑马灯标题不滚动");
if (mDrawingText != null && !mDrawingText.equals("")) {
if (mCenterNoScroll) {
mTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mDrawingText, mWidth / 2, mHeight / 2, mTextPaint);
} else {
mTextPaint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(mDrawingText, 0, mHeight / 2, mTextPaint);
}
}
break;
case TYPE_SCROLL:
if (!TextUtils.isEmpty(mDrawingText)) {
if (isShown()) {
canvas.drawText(mDrawingText, mTextDefalutXShaft, mHeight / 2, mTextPaint);//从坐标0开始绘制
mTextDefalutXShaft = mTextDefalutXShaft - mSpeed;
}
//当达到条件后,暂停一下,然后从0开始绘制
if (mMaxScroll < Math.abs(mTextDefalutXShaft)) {
mDrawingText = mTitleWithSpace;
mTextDefalutXShaft = 0;
startIndex = 0;
postInvalidateDelayed(mPauseTime);
} else {
// 控件总宽度 - 可见文字宽度(文字宽度-文字向x轴原点左边滑动距离)= 右边剩余无文字的宽度
//右侧的剩余空间可以填充的字数
int rightTitleCount = (int) ((mWidth - (mTextWidth + mTextDefalutXShaft)) / oneSpaceWidth());
if (rightTitleCount < 0) {
postInvalidateDelayed(30);
return;
}
for (int i = startIndex; i < rightTitleCount - mSpaceCount; i++) {
if (i < mTitleCharList.length) {//拿titleText中的文字
mDrawingText = mDrawingText + mTitleCharList[i];
startIndex++;
}
}
postInvalidateDelayed(30);
}
} else {
LogUtils.d(TAG, "drawMarqueeText mMarqueeText是空的");
}
break;
}
}
暂停滚动
//暂停滚动
public void stopScroll() {
isRunning = false;
}
此时如果跑马灯在滚动时,走到onDraw ,生效
private void drawMarqueeText(Canvas canvas) {
if (!isRunning) {
LogUtils.i(TAG, "drawMarqueeText: pause");
if (isShown()) {
canvas.drawText(mDrawingText, 0, mHeight / 2, mTextPaint);//从坐标0开始绘制
}
return;
}
...(省略)
}
继续、内容更新
//设置滚动的文字
public void setScrollText(String text) {
if (TextUtils.isEmpty(text) || text.equals(mTitleText)) {
return;
}
mTitleText = text;
boolean isScroll = mTitleType == TYPE_SCROLL;
if (mTitleType != TYPE_IDLE) {//idle 状态 onSizeChanged 去初始化
initMarqueeText();
}
if (isRunning && !isScroll) {//不是需要暂停并且没有滚动的情况下,调用
postInvalidate();
}
}
//继续滚动
public void startScroll() {
if (!isRunning) {
isRunning = true;
postInvalidate();
}
}
总结
这个自定义跑马灯最核心的,是如何循环滚动,计算 draw 的开始的位置与文字;
而标题内容更新、继续滚动等情况需求考虑是否重复加载。
如果想看更多代码细节,可以看 Demo