• 浅谈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);
            }
        }

    但是调用mMarquee.start(mMarqueeRepeatLimit)也是有条件的,它们是用短路与(&&)进行运算:

    1. mMarquee == null || mMarquee.isStopped()  : 这个不用解释了。

    2. isFocused() || isSelected() :是否获得焦点或者被选中

    3. getLineCount() == 1 :文本内容是否只有一行

    4. canMarquee() :是否满足滚动的条件。

    下面重点分析这个条件是什么,我们看下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));
        }

    为了便于理解里面的某些数值,我画了一个简单的图:

    其中:mLayout.getLineWidth(0)这个方法返回的是文本内容的宽度。

    当我们在TextView指定:

    android:padding="8dp"
    android:drawablePadding="8dp" 

    padding属性时。getCompoundPaddingLeft()和getCompoundRight()的值就会大于0

    上面的图是TextView中的内容未填满的状态,这个时候width > 0,mLayout.getLineWidth(0) < width。canMarquee() 返回false。因为这个时候TextView已经显示完毕,所以是没有跑马灯效果的。

    再看下面一个图:

    文本的宽度明显大于with,即:mLayout.getLineWidth(0) > width成立。所以canMarqueen()。返回true。这个时候边满足跑马灯的条件。

    但是这个条件这个限制应该是Android团队从交互体验上来考虑的,Ellipsize效果设计的初衷就是用来弥补字数太多而且需要一行显示的问题,如果字数刚好,即使是一行,也不需要这个效果。

    回到另一个问题上,这个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)即可。

    总结:

    • 跑马灯效果的限制条件:
    1.  是否能获得焦点或者是否被选中
    2.  文本内容宽度是否大于TextView整个的宽度(是否能显示完)

     

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

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

               2.setSelected(true);

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

  • 相关阅读:
    四则运算出题器
    四则运算出题网页
    四则运算自动生成器实现(python、wxpython、GUI)
    python 实现小学四则运算
    Process and Thread States
    COS AP-开启WPA后无法关联SSID!
    WLC MAC Filtering
    禅道--个人理解 简单介绍
    IDEA解决乱码
    avue 实现自定义列显隐并保存,并且搜索表单、form表单、crud列顺序互不影响。
  • 原文地址:https://www.cnblogs.com/ufreedom/p/4248142.html
Copyright © 2020-2023  润新知