一个文本跑马灯的“学”案

不管三七二十一,我们先上车

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 sConfigurations =
            new SparseArray(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这个文件里有这个属性


<bool name="config_ui_enableFadingMarquee">falsebool>
在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,我们是不可能对其进行重新赋值的,所以img

/**
 * 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. 没有开始滚动或者上次滚动已经停止
  2. 得到焦点或者被选中
  3. 行数为1
  4. 可以滚动

那么我们的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中设置了两个属性的时候所改变的值:

  1. singleline = true : 会设置singleLine为true

  2. 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"

你可能感兴趣的:(Android,随便说说)