今天同事遇到一个缺陷,跑马灯不生效,迟迟无法解决,于是帮忙看了下,最终顺利解决了,做个记录。
首先原来的代码是这样的:
JAVA代码实现(Kotlin):
holder.itemView.tv_title.text = title
XML中实现:
<TextView
android:id="@+id/tv_title"
android:layout_width="@dimen/dip96"
android:layout_height="@dimen/dip17"
android:ellipsize="marquee"
android:focusableInTouchMode="true"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="@color/color_333333"
android:textSize="@dimen/dip12"
tools:text="系统版本信息" />
都很正常。搜索晚上大多数的解决方案,无法是添加singleLine等字段,这里面其实都已经设置了。百度的所有方案都试过了,都是无效的,靠人不如靠己,就只能自己尝试调试源码解决了。幸好手里面有一部神机pixel4,和源码完全对得上,可以直接调试。
原因分析:
我这里就不一步一步的分析原因了,直接定位到原因点,是否展示跑马灯效果,是textView中这段代码控制的:
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private void startMarquee() {
// Do not ellipsize EditText
if (getKeyListener() != null) return;
if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
return;
}
if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
&& getLineCount() == 1 && canMarquee()) {
if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
final Layout tmp = mLayout;
mLayout = mSavedMarqueeModeLayout;
mSavedMarqueeModeLayout = tmp;
setHorizontalFadingEdgeEnabled(true);
requestLayout();
invalidate();
}
if (mMarquee == null) mMarquee = new Marquee(this);
mMarquee.start(mMarqueeRepeatLimit);
}
}
前面我们能看到正常的判断,是否一行,是否选中,是否获取焦点等等,这些都没有问题。问题点就在canMarquee()方法里面。
private boolean canMarquee() {
int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
return width > 0 && (mLayout.getLineWidth(0) > width
|| (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null
&& mSavedMarqueeModeLayout.getLineWidth(0) > width));
}
这里有一个很重要的问题,我们的绘制流程一般是measure->layout->draw的流程,而startMarquee的调用时机默认是在makeNewLayout中,也就是measure的流程中。但是mRight和mLeft的赋值却是在layout的流程当中,所以这里,首次的时候,最终得到的width的值一定是<=0的,这样的话,canMarquee返回值为false,所以跑马灯没有生效。
在源码当中,只有界面绘制出来,然后再去获取焦点的话,此时跑马灯效果才生效,这也是正常跑马灯效果展示的流程。但是如果xml中就声明了获取焦点,这时候有时也是不生效的,因为默认就具有焦点,调用canMarquee方法的时候,left和right很有可能还是0。
解决方案1:
既然这样,那我们就post一个runnable,再去获取焦点就好了。如下:
holder.itemView.tv_title.text = name
Handler().post{
holder.itemView.tv_title.requestFocus()
}
但是这样还是有问题,如果界面上有多个TextView的话,多个textView发生焦点抢占,这时候去执行
f ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
&& getLineCount() == 1 && canMarquee())
判断的时候,因为返回isFoucused返回false,所以跑马灯还是不生效的。或者严格点说,多个textView,只有最后获取到焦点的那个才会生效。
解决方案2:
看源码,除了isFocused(),还有isSelected(),那我们能不能使用isSelected来解决这个问题呢?当然可以,代码稍微一改:
holder.itemView.tv_title.text = name
Handler().post{
holder.itemView.tv_title.isSelected = true
}
这样就完美了,多个textView也可以同时实现跑马灯的效果了。
说到这里,还有一个要注意的点,源码当中,只有select状态的切换,才会触发执行跑马灯效果,所以务必把textView的isSelected的属性,设置为false,切记。
源码如下:
@Override
public void setSelected(boolean selected) {
boolean wasSelected = isSelected();
super.setSelected(selected);
//这里,只有发生状态的切换,才会触发startMarquee方法
if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (selected) {
startMarquee();
} else {
stopMarquee();
}
}
}