Android 自定义MarqueeView跑马灯效果,可调节首
UI要求效果图
TextView
自带的marquee
效果不能调节间距和速度,而UI的需求是可以实现首尾相接的跑马灯,以及支持超链接跳转
和HTML格式文本
。
思路
无论是 超链接跳转
,还是 HTML格式文本
的加载,TextView
自带的功能都已实现,而且效果也都不错,如果完全抛弃 TextView
,而从 view
开始自定义,那代价太大,而且兼容性以及优化一定比不得 TextView
。
所以第一想法就是在 TextView
的基础上实现。
思路设计图首尾相接,可控制速度,那不如就直接写两个
TextView
,无限循环。
当第一个
TextView
移动出屏幕后,调整位置到第二个TextView
的后面,如此,无限轮换,造成视觉上的无限循环效果。
实现
1. 继承 FrameLayout
,初始化时,直接添加两个 TextView
public class MarqueeView extends FrameLayout {
private TextView mTextView;
private TextView mGhostTextView;
public MarqueeView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
initLayout();
}
private void initLayout() {
mTextView = createTextView();
mGhostTextView = createTextView();
addView(mTextView);
addView(mGhostTextView);
}
private TextView createTextView() {
TextView textView = new TextView(getContext());
textView.setPadding(0, 0, 0, 0);
textView.setSingleLine();
textView.setTextColor(textColor);
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
textView.setLayoutParams(layoutParams);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setMovementMethod(LinkMovementMethod.getInstance());
return textView;
}
}
2. 重写 onDraw
方法,并计算两个 TextView
位置。
因为计算两个
TextView
位置的方法是类似一个死循环的,需要一直运行,所以我就写了一个ValueAnimator
来控制,方便实现暂停和开始。
private void initAnim() {
valueAnimator = ValueAnimator.ofFloat(0, measureText);
valueAnimator.addUpdateListener(animatorUpdateListener);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
}
ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffset -= speed; //移动速度控制
mGhostOffset -= speed;/
if (mOffset + measureText < 0) { //如果TextView1移出屏幕,就换位置到TextView2后面
mOffset = mGhostOffset + measureText + spacing;
}
if (mGhostOffset + measureText < 0) {//如果TextView2移出屏幕,就换位置到TextView1后面
mGhostOffset = mOffset + measureText + spacing;
}
invalidate();
}
};
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mTextView == null || mGhostTextView == null) {
return;
}
mTextView.setX(mOffset); //根据偏移量移动位置
mGhostTextView.setX(mGhostOffset);
}
public void startAnim() {
if (valueAnimator == null) {
initAnim();
}
valueAnimator.start();
}
public void stopAnim() {
if (valueAnimator != null) {
valueAnimator.clone();
}
}
运行时效果图如下,TextView
没有绘制完整
原因也很简单,因为
TextView
在onMeasure
测量自身宽度时,就算是WRAP_CONTENT
也最多就只能达到父组件FrameLayout
的宽度,在执行onDraw
时,TextView
宽度不够,所以就会导致文字绘制不完全。
解决方式:重写 TextView
的 onMeasure
方法
注意:我是重写的是父组件
FrameLayout
的onMeasure
,在FrameLayout
测绘时再循环遍历子view
进行重新测绘。
这个效果跟重写TextView
的onMeasure
是一样的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
measureText, MeasureSpec.EXACTLY);
view.measure(childWidthMeasureSpec, view.getMeasuredHeight());
}
}
3. 用 HorizontalScrollView
当父控件
当我写完上面那些之后,突然想起 HorizontalScrollView
本身已是一个可滑动组件,而 HorizontalScrollView
也已经支持子 view
宽度超过其本身。
遂,放弃 FrameLayout
改用 HorizontalScrollView
public class MarqueeView extends HorizontalScrollView {
private TextView mTextView;
private TextView mGhostTextView;
......
//onMeasure方法也不用再重写
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void initLayout() {
//HorizontalScrollView跟ScrollView一样只支持一个子view,所以就再嵌套了一个RelativeLayout
RelativeLayout relativeLayout = new RelativeLayout(getContext());
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
relativeLayout.setLayoutParams(layoutParams);
addView(relativeLayout);
mTextView = createTextView();
mGhostTextView = createTextView();
relativeLayout.addView(mTextView);
relativeLayout.addView(mGhostTextView);
}
private TextView createTextView() {
......
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
}
最终效果图
最终效果图GitHub:https://github.com/jiaoyaning/MarqueeView