当前位置: 代码迷 >> Android >> 浅谈TextView Ellipsize效果与Marquee跑马灯无效果有关问题
  详细解决方案

浅谈TextView Ellipsize效果与Marquee跑马灯无效果有关问题

热度:93   发布时间:2016-04-28 02:37:52.0
浅谈TextView Ellipsize效果与Marquee跑马灯无效果问题

说到TextView 效果,相信大家一定熟悉跑马灯。

先来看看 Ellipsize是什么,Ellipsize 从开发技术上翻译为省略效果。故名思议,就是当文本无法显示全部时,用什么效果来显示未显示的部分。

 

一,What is Ellipsize  and  How to use ?

首先我们在Android XML中需要这样定义

    <TextView        android:id="@+id/textView"        android:layout_width="wrap_content"        android:layout_height="30dp"        android:text="@string/hello_world"  //全部文字为:Hello world!这是一个跑马灯专门用来测试的,所以字数一定要多,不过肯定超过15字了。                android:ellipsize="end"        android:singleLine="true"                        android:textSize="20sp"        />

 显示效果:

可以看到在这段文字后面有三个"..."来表示这段话没有显示完毕,后面还有一部分。所以问题来了这个效果怎么来的呢?

 android:ellipsize="end" android:singleLine="true"

 起作用的就是这两行代码。

android:singleLine="true"用来指定当前TextView为单行显示,意思就是无论字数多少,都要一行来显示,这个也是显示Ellipsize效果的必须条件。

至于为什么需要android:singleLine="true"后面讲解源码会说到。我们再来看android:ellipsize这个属性。

它是由TextView中的一个枚举定义的,我们看看这个数据结构:

  public enum TruncateAt {        START,        MIDDLE,        END,        MARQUEE,        /**         * @hide         */        END_SMALL    }

 

 从枚举名TruncateAt的意思:在什么地方截断。顾名思义就是在TextView的内容中哪个位置显示截断效果。下面几个图分别代表每个变量的效果:

TruncateAt.MARQUEE就是跑马灯效果。但是本该是一行文字从坐往右的滚动,但是实际上只添加

android:ellipsize="marquee"android:singleLine="true"

 

这两个属性,看到就是上图那个样子。仔细一点你会发现那个其实不是普通的效果:"来"字后半部有渐进透明,后面会讲解跑马灯效果不显示的原因。

其实在实际Android XML中还有一个属性android:ellipsize="none",这个意思是不指定省略效果:

我们可以看到这段话多出了"测试"两个字,但是"试"被截断了。其实android:ellipsize的默认值为end。因为在TextView的构造函数中有这样一行代码:

   if (singleLine && getKeyListener() == null && ellipsize < 0) {                ellipsize = 3; // END        }

 

意思是即使你只设定了 android:singleLine="true"。那么也会显示android:ellipsize="end"时的效果。

 

 二,为什么跑马灯无效果?

对于这个问题,相比大家都知道,当我们写了如下代码时:

  <TextView        android:id="@+id/textView"        android:layout_width="wrap_content"        android:layout_height="30dp"        android:text="@string/hello_world"        android:ellipsize="marquee"        android:singleLine="true"        android:textSize="20sp"        />

 

却发现应用运行时跑马灯效果却没有。妈蛋,这是为毛。这个时候相比大家都能百度到解法。下面有两个网上最流行的解法

1.重写TextView

2.需要调用TextViews一大堆的函数。

起初我看到这个非常不爽,还能不能好好的玩耍了,妈蛋,android:ellipsize="start|middle|end"都能一行代码显示,为毛这个不行。想要知道问题原因很简单。

知道了why,你就知道了how。

 

谈到跑马灯,里面还是有点复杂的的。Android SDK将TextView 的跑马灯封装起来了。

TextView继承至View。所以它中元素的绘制都在onDraw()中。在TextView中有段代码:

 

 final int layoutDirection = getLayoutDirection();        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);        if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&                mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {            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);            }        }

 

其中final float dx = -mMarquee.getScroll();就是TextView的位移量,而canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);这行代码通过位移画布

来做到跑马灯效果。但是这个mMarquee.getScroll()到底是什么呢?

查阅源码便可发现TextView 中有一个内部类Marquee

 

private static final class Marquee {        // TODO: Add an option to configure this        private static final float MARQUEE_DELTA_MAX = 0.07f;        private static final int MARQUEE_DELAY = 1200;        private static final int MARQUEE_RESTART_DELAY = 1200;        private static final int MARQUEE_DP_PER_SECOND = 30;        private static final byte MARQUEE_STOPPED = 0x0;        private static final byte MARQUEE_STARTING = 0x1;        private static final byte MARQUEE_RUNNING = 0x2;        private final WeakReference<TextView> mView;        private final Choreographer mChoreographer;        private byte mStatus = MARQUEE_STOPPED;        private final float mPixelsPerSecond;        private float mMaxScroll;        private float mMaxFadeScroll;        private float mGhostStart;        private float mGhostOffset;        private float mFadeStop;        private int mRepeatLimit;        private float mScroll;        private long mLastAnimationMs;        Marquee(TextView v) {            final float density = v.getContext().getResources().getDisplayMetrics().density;            mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;            mView = new WeakReference<TextView>(v);            mChoreographer = Choreographer.getInstance();        }        private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                tick();            }        };        private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                mStatus = MARQUEE_RUNNING;                mLastAnimationMs = mChoreographer.getFrameTime();                tick();            }        };        private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                if (mStatus == MARQUEE_RUNNING) {                    if (mRepeatLimit >= 0) {                        mRepeatLimit--;                    }                    start(mRepeatLimit);                }            }        };        void tick() {            if (mStatus != MARQUEE_RUNNING) {                return;            }            mChoreographer.removeFrameCallback(mTickCallback);            final TextView textView = mView.get();            if (textView != null && (textView.isFocused() || textView.isSelected())) {                long currentMs = mChoreographer.getFrameTime();                long deltaMs = currentMs - mLastAnimationMs;                mLastAnimationMs = currentMs;                float deltaPx = deltaMs / 1000f * mPixelsPerSecond;                mScroll += deltaPx;                if (mScroll > mMaxScroll) {                    mScroll = mMaxScroll;                    mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);                } else {                    mChoreographer.postFrameCallback(mTickCallback);                }                textView.invalidate();            }        }        void stop() {            mStatus = MARQUEE_STOPPED;            mChoreographer.removeFrameCallback(mStartCallback);            mChoreographer.removeFrameCallback(mRestartCallback);            mChoreographer.removeFrameCallback(mTickCallback);            resetScroll();        }        private void resetScroll() {            mScroll = 0.0f;            final TextView textView = mView.get();            if (textView != null) textView.invalidate();        }        void start(int repeatLimit) {            if (repeatLimit == 0) {                stop();                return;            }            mRepeatLimit = repeatLimit;            final TextView textView = mView.get();            if (textView != null && textView.mLayout != null) {                mStatus = MARQUEE_STARTING;                mScroll = 0.0f;                final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -                        textView.getCompoundPaddingRight();                final float lineWidth = textView.mLayout.getLineWidth(0);                final float gap = textWidth / 3.0f;                mGhostStart = lineWidth - textWidth + gap;                mMaxScroll = mGhostStart + textWidth;                mGhostOffset = lineWidth + gap;                mFadeStop = lineWidth + textWidth / 6.0f;                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;                textView.invalidate();                mChoreographer.postFrameCallback(mStartCallback);            }        }        float getGhostOffset() {            return mGhostOffset;        }        float getScroll() {            return mScroll;        }        float getMaxFadeScroll() {            return mMaxFadeScroll;        }        boolean shouldDrawLeftFade() {            return mScroll <= mFadeStop;        }        boolean shouldDrawGhost() {            return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;        }        boolean isRunning() {            return mStatus == MARQUEE_RUNNING;        }        boolean isStopped() {            return mStatus == MARQUEE_STOPPED;        }    }

其中getScroll值返回的是 mScroll。所以我们要找到这个值在哪被改变的。所以我们又很容易找mScroll现在start()中进行初始化:但是未正向或者负向改变mScroll的值,只是进行其他变量值的赋值。

但是在start()函数最后面有行代码:mChoreographer.postFrameCallback(mStartCallback);mChoreographer又是什么?如果再扯上Choreographer,那就说很多了。

 下面引用一篇文章的介绍:http://www.360doc.com/content/14/0827/10/10366845_405038717.shtml

所有的图像显示输出都是由时钟驱动的,这个驱动信号称为VSYNC。这个名词来源于模拟电视时代,在那个年代,因为带宽的限制,每一帧图像都有分成两次传输,先扫描偶数行(也称偶场)传输,再回到头部扫描奇数行(奇场),扫描之前,发送一个VSYNC同步信号,用于标识这个这是一场的开始。场频,也就是VSYNC 频率决定了帧率(场频/2). 在现在的数字传输中,已经没有了场的概念,但VSYNC这一概念得于保持下来,代表了图像的刷新频率,意味着收到VSYNC信号后,我们必须将新的一帧进行显示。VSYNC一般由硬件产生,也可以由软件产生(如果够准确的话),Android 中VSYNC来着于HWComposer,接收者没错,就是Choreographer。Choreographer英文意思是编舞者,跳舞很讲究节奏不是吗,必须要踩准点。Choreographer 就是用来帮助Android的动画,输入,还是显示刷新按照固定节奏来完成工作的。

 即Marquee使用Choreographer来进行每一桢的绘制。mChoreographer.postFrameCallback(mStartCallback)有个回调:

        private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {            @Override            public void doFrame(long frameTimeNanos) {                mStatus = MARQUEE_RUNNING;                mLastAnimationMs = mChoreographer.getFrameTime();                tick();            }        };

 

我们看到mStatus值 由start()中设定的MARQUEE_STARTING改为:MARQUEE_RUNNING。说明这个时候开始执行这个动作。其调用了tick()函数:

 void tick() {            if (mStatus != MARQUEE_RUNNING) {                return;            }            mChoreographer.removeFrameCallback(mTickCallback);            final TextView textView = mView.get();            if (textView != null && (textView.isFocused() || textView.isSelected())) {                long currentMs = mChoreographer.getFrameTime();                long deltaMs = currentMs - mLastAnimationMs;                mLastAnimationMs = currentMs;                float deltaPx = deltaMs / 1000f * mPixelsPerSecond;                mScroll += deltaPx;                if (mScroll > mMaxScroll) {                    mScroll = mMaxScroll;                    mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);                } else {                    mChoreographer.postFrameCallback(mTickCallback);                }                textView.invalidate();            }        }

 

定位到这个函数,我们一下子明了。这个方法通过mChoreographer.getFrameTime()得到当前帧时间,然后和上一次的时间帧做减法。得到偏移量。然后进行转换,得到跑马灯应该

位置的偏移量mScroll。但是mScroll的改变也是有前提的:textView != null && (textView.isFocused() || textView.isSelected()

textView.isFocused()与textView.isSelected()使用的是短路或(||)。就说明两个方法的返回值有一定的独立性。这两个方法均为View中的方法,TextView均未重写,

 

再看:

 

textView.isFocused():

  /**     * Returns true if this view has focus     *     * @return True if this view has focus, false otherwise.     */    @ViewDebug.ExportedProperty(category = "focus")    public boolean isFocused() {        return (mPrivateFlags & PFLAG_FOCUSED) != 0;    }

 

 

 

源码中:

static final int PFLAG_FOCUSED = 0x00000002;

 

PFLAG_FOCUSED是个常量,不能被改变,而能改变的只有mPrivateFlags了。mPrivateFlags在TextView中是个变量,所以只要mPrivateFlags不等于0.则
isFocused()便返回true。

查看isSelected()方法源码:

  /**     * Indicates the selection state of this view.     *     * @return true if the view is selected, false otherwise     */    @ViewDebug.ExportedProperty    public boolean isSelected() {        return (mPrivateFlags & PFLAG_SELECTED) != 0;    }

 

    static final int PFLAG_SELECTED                    = 0x00000004;

 

PFLAG_SELECTED也是个常量,所以这个返回值也取决于mPrivateFlags。妈蛋,越扯越大,这下子扯到View层去了。想想TextView应该有某个方法能够启动这个效果。所以折回去看看哪个地方调用了mQuereen.start()方法了。
经过查看源码,start()方法被封装了:
 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);        }    }

 

但是这个startMarquee()又是在哪调用的呢?经过分析,我绘制了下面的图例:

 

 

上面的方法中只有一个是提供给我们调用的:setSelect()。查看其源码:

    @Override    public void setSelected(boolean selected) {        boolean wasSelected = isSelected();        super.setSelected(selected);        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {            if (selected) {                startMarquee();            } else {                stopMarquee();            }        }    }

 

这尼玛不是赤裸裸的在开启这个效果嘛。shit!.

到此,应该就应该结束了。只需要设置setSelected(true)。便可让跑马灯跑起来。

 

但是!!突发奇想,在android XML是不是也可以使用这个属性呢。所以又找到另一个让其跑其他的方法:

android:textIsSelectable="true"

 

这个属性用来设定当前的TextView是否可以被选中的。用途就是当你看到一段文字时,你可以长按它,然后会弹出一个对话框,你是想复制它呢,还是剪切它呢还是怎么地。

看它所对应的setTextIsSelectabe()的源码:

    public void setTextIsSelectable(boolean selectable) {        if (!selectable && mEditor == null) return; // false is default value with no edit data        createEditorIfNeeded();        if (mEditor.mTextIsSelectable == selectable) return;        mEditor.mTextIsSelectable = selectable;        setFocusableInTouchMode(selectable);        setFocusable(selectable);        setClickable(selectable);        setLongClickable(selectable);        // mInputType should already be EditorInfo.TYPE_NULL and mInput should be null        setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);        setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);        // Called by setText above, but safer in case of future code changes        mEditor.prepareCursorControllers();    }

 

 

这个方法里只有上面加粗的那行  setFocusableInTouchMode(selectable);是起到触发跑马灯走起的效果。

再看其源码:

  /**     * Set whether this view can receive focus while in touch mode.     *     * Setting this to true will also ensure that this view is focusable.     *     * @param focusableInTouchMode If true, this view can receive the focus while     *   in touch mode.     *     * @see #setFocusable(boolean)     * @attr ref android.R.styleable#View_focusableInTouchMode     */    public void setFocusableInTouchMode(boolean focusableInTouchMode) {        // Focusable in touch mode should always be set before the focusable flag        // otherwise, setting the focusable flag will trigger a focusableViewAvailable()        // which, in touch mode, will not successfully request focus on this view        // because the focusable in touch mode flag is not set        setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE);        if (focusableInTouchMode) {            setFlags(FOCUSABLE, FOCUSABLE_MASK);        }    }

 

这个方法是View中定义的,TextView并没有重写。它设定当这个View在接收到用户触摸时是否能接收到焦点。

其调用了setFlags()方法,setFlags()源码有很多,我们再看其中几段源码:

 final int newVisibility = flags & VISIBILITY_MASK;        if (newVisibility == VISIBLE) {            if ((changed & VISIBILITY_MASK) != 0) {                /*                 * If this view is becoming visible, invalidate it in case it changed while                 * it was not visible. Marking it drawn ensures that the invalidation will                 * go through.                 */                mPrivateFlags |= PFLAG_DRAWN;                invalidate(true);                needGlobalAttributesUpdate(true);                // a view becoming visible is worth notifying the parent                // about in case nothing has focus.  even if this specific view                // isn't focusable, it may contain something that is, so let                // the root view try to give this focus if nothing else does.                if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) {                    mParent.focusableViewAvailable(this);                }            }        }

 其中有行:

 /*                 * If this view is becoming visible, invalidate it in case it changed while                 * it was not visible. Marking it drawn ensures that the invalidation will                 * go through.                 */                mPrivateFlags |= PFLAG_DRAWN;

 

意思是说如果这个View是可见的,那我们就应该在它的可视状态改变时去刷新它。PFLAG_DRAWN是个常量:

    static final int PFLAG_DRAWN                       = 0x00000020;

 

所以 :

mPrivateFlags = mPrivateFlags |  PFLAG_DRAWN;

 

 mPrivateFlags的值必定不等于0.还记得我们上面追踪到的isFocused()和isSelected()方法吗,要让其返回truem则PrivateFlags的值一定不能为0。

所以这个方法能达到让跑马灯有效果。

当然这只是一中投机取巧,因为在xml设定:android:focusableInTouchMode="true",这个和上面的那个方法是一样的功能,但是却依然没效果。

我们还是使用setSelect(true)即可。

 

总结:

要想让textView的跑马灯有效果,则有下面几种方法:

1.xml中指定android:textIsSelectable="true"

 

2.setSelected(true);

 

上面的随便使用一个即可,不需要重写TextView,也不需要一大堆的方法。Just Line of code!!这才像话!

 

1楼^-^antoon^-^
测试了一下,不是100%有效,与TextView设置的宽度有关。,Text都没显示完全的状态啊下,width设为30dp,获取焦点后可以显示跑马灯效果。width设为100dp时,没效果。
  相关解决方案