不管三七二十一,我们先上车
if (isMarqueeFadeEnabled()) {
if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}
继续深挖:
private final boolean isMarqueeFadeEnabled() {
return mEllipsize == TextUtils.TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
}
两个并列条件,一个是我们设置的ellipsize == marquee, 另一条是一个值不等于,我们看哪里会对这个mMarqueeFadeMode变量进行赋值,这是赋值的一个代码段,在源码中被两个地方调用
if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
setHorizontalFadingEdgeEnabled(true);
mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
} else {
setHorizontalFadingEdgeEnabled(false);
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
}
那么这个判断条件是什么呢?
/**
* Returns a configuration for the specified context. The configuration depends on
* various parameters of the context, like the dimension of the display or the
* density of the display.
*
* @param context The application context used to initialize the view configuration.
*/
public static ViewConfiguration get(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int density = (int) (100.0f * metrics.density);
ViewConfiguration configuration = sConfigurations.get(density);
if (configuration == null) {
configuration = new ViewConfiguration(context);
sConfigurations.put(density, configuration);
}
return configuration;
}
static final SparseArray<ViewConfiguration> sConfigurations =
new SparseArray<ViewConfiguration>(2);
根据sConfigurations的定义,这个数组的值,只能通过上边的put放置,所以configuration还是通过configuration = new ViewConfiguration(context);
这句代码生成的,那么这个构造方法都干了什么呢?
/**
* Creates a new configuration for the specified context. The configuration depends on
* various parameters of the context, like the dimension of the display or the density
* of the display.
*
* @param context The application context used to initialize this view configuration.
*
* @see #get(android.content.Context)
* @see android.util.DisplayMetrics
*/
private ViewConfiguration(Context context) {
final Resources res = context.getResources();
final DisplayMetrics metrics = res.getDisplayMetrics();
final Configuration config = res.getConfiguration();
final float density = metrics.density;
final float sizeAndDensity;
if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
sizeAndDensity = density * 1.5f;
} else {
sizeAndDensity = density;
}
mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
// Size of the screen in bytes, in ARGB_8888 format
final WindowManager win = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
final Display display = win.getDefaultDisplay();
final Point size = new Point();
display.getRealSize(size);
mMaximumDrawingCacheSize = 4 * size.x * size.y;
mOverscrollDistance = (int) (sizeAndDensity * OVERSCROLL_DISTANCE + 0.5f);
mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
if (!sHasPermanentMenuKeySet) {
final int configVal = res.getInteger(
com.android.internal.R.integer.config_overrideHasPermanentMenuKey);
switch (configVal) {
default:
case HAS_PERMANENT_MENU_KEY_AUTODETECT: {
IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
try {
sHasPermanentMenuKey = !wm.hasNavigationBar();
sHasPermanentMenuKeySet = true;
} catch (RemoteException ex) {
sHasPermanentMenuKey = false;
}
}
break;
case HAS_PERMANENT_MENU_KEY_TRUE:
sHasPermanentMenuKey = true;
sHasPermanentMenuKeySet = true;
break;
case HAS_PERMANENT_MENU_KEY_FALSE:
sHasPermanentMenuKey = false;
sHasPermanentMenuKeySet = true;
break;
}
}
mFadingMarqueeEnabled = res.getBoolean(
com.android.internal.R.bool.config_ui_enableFadingMarquee);
mTouchSlop = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
mPagingTouchSlop = mTouchSlop * 2;
mDoubleTapTouchSlop = mTouchSlop;
mMinimumFlingVelocity = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewMinFlingVelocity);
mMaximumFlingVelocity = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewMaxFlingVelocity);
mGlobalActionsKeyTimeout = res.getInteger(
com.android.internal.R.integer.config_globalActionsKeyTimeout);
}
初始化的数据还挺多,根据下边的代码,我们主要关注mFadingMarqueeEnabled:
/**
* @hide
* @return Whether or not marquee should use fading edges.
*/
public boolean isFadingMarqueeEnabled() {
return mFadingMarqueeEnabled;
}
从上边的构造方法中发现如下代码,皮皮虾,我们继续开车,继续找其根源
mFadingMarqueeEnabled = res.getBoolean(
com.android.internal.R.bool.config_ui_enableFadingMarquee);
而当我们去查找这个属性的时候看到了SDK\platforms\android-25\data\res\values\config.xml这个文件里有这个属性
<!-- Enables or disables fading edges when marquee is enabled in TextView.
Off by default, since the framebuffer readback used to implement the
fading edges is prohibitively expensive on most GPUs. -->
<bool name="config_ui_enableFadingMarquee">false</bool>
在TextView中启用选框时,启用或禁用渐变边。
默认情况下关闭,因为framebuffer回读用于实现
在大多数GPU上,渐变边缘的开销是非常大的
所以这个值默认就是false的,从这里向上看,我们发现当这个值默认false的时候,这条语句mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS
就会执行,从而导致isMarqueeFadeEnabled()返回的是false, 所以我们也就无法使得TextView进行跑马灯效果了,那我们该怎么办,从源码中可以看到有另一个代码段:
/**
* @deprecated Use {@link android.view.ViewConfiguration#get(android.content.Context)} instead.
*/
@Deprecated
public ViewConfiguration() {
mEdgeSlop = EDGE_SLOP;
mFadingEdgeLength = FADING_EDGE_LENGTH;
mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
mScrollbarSize = SCROLL_BAR_SIZE;
mTouchSlop = TOUCH_SLOP;
mDoubleTapTouchSlop = DOUBLE_TAP_TOUCH_SLOP;
mPagingTouchSlop = PAGING_TOUCH_SLOP;
mDoubleTapSlop = DOUBLE_TAP_SLOP;
mWindowTouchSlop = WINDOW_TOUCH_SLOP;
//noinspection deprecation
mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
mOverscrollDistance = OVERSCROLL_DISTANCE;
mOverflingDistance = OVERFLING_DISTANCE;
mFadingMarqueeEnabled = true;
mGlobalActionsKeyTimeout = GLOBAL_ACTIONS_KEY_TIMEOUT;
}
但是这个方法被废弃了,而且我们也不可能尝试去修改代码段,我们可以尝试直接修改mMarqueeFadeMode的值,但是这个值的定义如下,是一个private,我们是不可能对其进行重新赋值的,所以
/**
* On some devices the fading edges add a performance penalty if used
* extensively in the same layout. This mode indicates how the marquee
* is currently being shown, if applicable. (mEllipsize will == MARQUEE)
*/
private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
看来车上错了,我们重新发车
/**
* The width passed in is now the desired layout width,
* not the full view width with padding.
* {@hide}
*/
protected void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
stopMarquee();
.........
.........
.........
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!compressText(ellipsisWidth)) {
final int height = mLayoutParams.height;
// If the size of the view does not depend on the size of the text, try to
// start the marquee immediately
if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
startMarquee();
} else {
// Defer the start of the marquee until we know our width (see setFrame())
mRestartMarquee = true;
}
}
}
// CursorControllers need a non-null mLayout
if (mEditor != null) mEditor.prepareCursorControllers();
}
从上边我们可以看到如果开始跑马灯的前提是设置ellipsize = marquee 而且文本长度应该进行滚动,然后我们看到如果mLayoutParams.height的height满足height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT,但是我们需要看到的是底下会置重新滚动的标志位true,所以我们姑且认为这里会尝试去滚动。
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);
}
}
阅读上边代码,我们直接看底下的if判断条件:
(mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) && getLineCount() == 1 && canMarquee()
- 没有开始滚动或者上次滚动已经停止
- 得到焦点或者被选中
- 行数为1
- 可以滚动
那么我们的singleline会在哪里被设置呢?
private void applySingleLine(boolean singleLine, boolean applyTransformation,
boolean changeMaxLines) {
mSingleLine = singleLine;
if (singleLine) {
setLines(1);
setHorizontallyScrolling(true);
if (applyTransformation) {
setTransformationMethod(SingleLineTransformationMethod.getInstance());
}
} else {
if (changeMaxLines) {
setMaxLines(Integer.MAX_VALUE);
}
setHorizontallyScrolling(false);
if (applyTransformation) {
setTransformationMethod(null);
}
}
}
代码中设置的singleline可以让文字跑起来,但是maxline不能
/**
* If true, sets the properties of this field (number of lines, horizontally scrolling,
* transformation method) to be for a single-line input; if false, restores these to the default
* conditions.
*
* Note that the default conditions are not necessarily those that were in effect prior this
* method, and you may want to reset these properties to your custom values.
*
* @attr ref android.R.styleable#TextView_singleLine
*/
@android.view.RemotableViewMethod
public void setSingleLine(boolean singleLine) {
// Could be used, but may break backward compatibility.
// if (mSingleLine == singleLine) return;
setInputTypeSingleLine(singleLine);
applySingleLine(singleLine, true, true);
}
// We need to update the single line mode if it has changed or we
// were previously in password mode.
if (mSingleLine != singleLine || forceUpdate) {
// Change single line mode, but only change the transformation if
// we are not in password mode.
applySingleLine(singleLine, !isPassword, true);
}
public TextView(
Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
case com.android.internal.R.styleable.TextView_maxLines:
setMaxLines(a.getInt(attr, -1));
break;
case com.android.internal.R.styleable.TextView_lines:
setLines(a.getInt(attr, -1));
break;
case com.android.internal.R.styleable.TextView_minLines:
setMinLines(a.getInt(attr, -1));
break;
case com.android.internal.R.styleable.TextView_scrollHorizontally:
if (a.getBoolean(attr, false)) {
setHorizontallyScrolling(true);
}
break;
case com.android.internal.R.styleable.TextView_singleLine:
singleLine = a.getBoolean(attr, singleLine);
break;
case com.android.internal.R.styleable.TextView_ellipsize:
ellipsize = a.getInt(attr, ellipsize);
break;
case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
break;
case com.android.internal.R.styleable.TextView_includeFontPadding:
if (!a.getBoolean(attr, true)) {
setIncludeFontPadding(false);
}
break;
case com.android.internal.R.styleable.TextView_cursorVisible:
if (!a.getBoolean(attr, true)) {
setCursorVisible(false);
}
break;
}
}
a.recycle();
...
// This call will save the initial left/right drawables
setCompoundDrawablesWithIntrinsicBounds(
drawableLeft, drawableTop, drawableRight, drawableBottom);
setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
setCompoundDrawablePadding(drawablePadding);
// Same as setSingleLine(), but make sure the transformation method and the maximum number
// of lines of height are unchanged for multi-line TextViews.
setInputTypeSingleLine(singleLine);
applySingleLine(singleLine, singleLine, singleLine);
if (singleLine && getKeyListener() == null && ellipsize < 0) {
ellipsize = 3; // END
}
switch (ellipsize) {
case 1:
setEllipsize(TextUtils.TruncateAt.START);
break;
case 2:
setEllipsize(TextUtils.TruncateAt.MIDDLE);
break;
case 3:
setEllipsize(TextUtils.TruncateAt.END);
break;
case 4:
if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
setHorizontalFadingEdgeEnabled(true);
mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
} else {
setHorizontalFadingEdgeEnabled(false);
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
}
setEllipsize(TextUtils.TruncateAt.MARQUEE);
break;
}
}
上边代码挑选出了有价值的部分,我们可以看到当我们在xml中设置了两个属性的时候所改变的值:
singleline = true : 会设置singleLine为true
maxlines = 1:setMaxLines(a.getInt(attr, -1));
/** * Makes the TextView at most this many lines tall. * * Setting this value overrides any other (maximum) height setting. * * @attr ref android.R.styleable#TextView_maxLines */ @android.view.RemotableViewMethod public void setMaxLines(int maxlines) { mMaximum = maxlines; mMaxMode = LINES; requestLayout(); invalidate(); }
但是Android Studio居然提示我们使用maxline这个属性替代singleline这个属性,是不是很滑稽
最后结论
我们如果想让文本跑马灯,那么我们需要设置如下属性:
android:singleLine="true"
android:marqueeRepeatLimit="marquee_forever"
android:ellipsize="marquee"
android:focusableInTouchMode="true"
android:focusable="true"