简介 我们可以设置TextView
的android:ellipsize="marquee"
属性,来做到当文字超出一行的时候呈现跑马灯效果。但TextView
的这个走马灯效果需要获取焦点,而同一时间只有一个控件可以获得焦点,更重要的是产品要求无论文字内容是否超出一行,都要滚动效果。
这里先贴一下最后实现的Github地址和效果图
https://github.com/dreamgyf/MarqueeTextView
思路 思路其实很简单,我们只要将单行的TextView
截成一张Bitmap
,然后我们再自定义一个View,重写它的onDraw
方法,每隔一段时间,将这张Bitmap画在不同的坐标上(左右两边各draw一次),这样连续起来看起来就是走马灯效果了。
后来和同事讨论,他提出能不能通过Canvas的平移配合drawText
实现这个功能,我想应该也是可以的,但我没有做尝试,各位看官感兴趣的可是试一下这种方案。
实现 我们先自定义一个View继承自AppCompatTextView
,再在初始化的时候new一个TextView
,并重写onMeasure
和onLayout
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void init () { mTextView = new TextView(getContext(), attrs); mTextView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mTextView.setMaxLines(1 ); } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); mTextView.measure(MeasureSpec.UNSPECIFIED, heightMeasureSpec); } @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { super .onLayout(changed, left, top, right, bottom); mTextView.layout(left, top, left + mTextView.getMeasuredWidth(), bottom); }
这样做是为了利用这个内部TextView
生成我们需要的Bitmap
,同时借用TextView
写好的onMeasure
方法,这样我们就不用再那么复杂的重写onMeasure
方法了
接下来是生成Bitmap
1 2 3 4 5 private void updateBitmap () { mBitmap = Bitmap.createBitmap(mTextView.getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mBitmap); mTextView.draw(canvas); }
这个很简单,需要注意的是长度要使用内部持有的TextView
的getMeasuredWidth
,如果使用getWidth
的话,最大值为屏幕的宽度,很可能导致生成出的Bitmap
不全,高度用谁的倒是无所谓
在每次setText
或setTextSize
的时候都需要更新Bitmap
并重新布局绘制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private void init () { mTextView.addOnLayoutChangeListener(new OnLayoutChangeListener() { @Override public void onLayoutChange (View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updateBitmap(); restartScroll(); } }); } @Override public void setText (CharSequence text, BufferType type) { super .setText(text, type); if (mTextView != null ) { mTextView.setText(text); requestLayout(); } } @Override public void setTextSize (int unit, float size) { super .setTextSize(unit, size); if (mTextView != null ) { mTextView.setTextSize(size); requestLayout(); } }
接下来,我给这个MarqueeTextView
定义了一些参数,一个是space
(文字滚动时,头尾的最小间隔距离),另一个是speed
(文字滚动的速度)
先看一下onDraw
的实现吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Override protected void onDraw (Canvas canvas) { if (mBitmap != null ) { if (mTextView.getMeasuredWidth() <= getWidth()) { int space = mSpace - (getWidth() - mTextView.getMeasuredWidth()); if (space < 0 ) { space = 0 ; } if (mLeftX < -getWidth() - space) { mLeftX += getWidth() + space; } canvas.drawBitmap(mBitmap, mLeftX, 0 , getPaint()); if (mLeftX < 0 ) { canvas.drawBitmap(mBitmap, getWidth() + mLeftX + space, 0 , getPaint()); } } else { if (mLeftX < -mTextView.getMeasuredWidth() - mSpace) { mLeftX += mTextView.getMeasuredWidth() + mSpace; } canvas.drawBitmap(mBitmap, mLeftX, 0 , getPaint()); if (mLeftX + (mTextView.getMeasuredWidth() - getWidth()) < 0 ) { canvas.drawBitmap(mBitmap, mTextView.getMeasuredWidth() + mLeftX + mSpace, 0 , getPaint()); } } } }
这就是基本的绘制思路
接下来需要让他动起来,这里使用的Choreographer
,每次收到Vsync
信号系统绘制新帧时都更新一下坐标并重绘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private static final float BASE_FPS = 60f ;private float mFps = BASE_FPS;private void updateFps () { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mFps = context.getDisplay().getRefreshRate(); } else { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mFps = windowManager.getDefaultDisplay().getRefreshRate(); } } private Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { @Override public void doFrame (long frameTimeNanos) { invalidate(); int speed = (int ) (BASE_FPS / mFps * mSpeed); mLeftX -= speed; Choreographer.getInstance().postFrameCallback(this ); } }; public void startScroll () { Choreographer.getInstance().postFrameCallback(frameCallback); } public void pauseScroll () { Choreographer.getInstance().removeFrameCallback(frameCallback); } public void stopScroll () { mLeftX = 0 ; Choreographer.getInstance().removeFrameCallback(frameCallback); } public void restartScroll () { stopScroll(); startScroll(); }
最后,在View
可见性发生变化时,需要控制一下动画的启停
1 2 3 4 5 6 7 8 9 @Override protected void onVisibilityChanged (@NonNull View changedView, int visibility) { if (visibility == VISIBLE) { updateFps(); Choreographer.getInstance().postFrameCallback(frameCallback); } else { Choreographer.getInstance().removeFrameCallback(frameCallback); } }