文字全屏轮播效果实现

刷短视频时看到一个全屏文字滚动播放的效果,如图

文字全屏轮播效果实现_第1张图片

想了下,其实效果不难实现,沉浸式+TextView跑马灯效果即可实现

基础版

1、设置沉浸式

这里采用了给Activity设置style的方式,res/values/styles.xml中


    ...
    

并在AndroidManifest.xml文件中使用自定义style,并将Activity设置为横屏


    
        
        
    

2、跑马灯

Android的TextView是自带跑马灯效果的,布局文件如下


这样就实现了最简单的效果,黑底白字全屏滚动播放。

加强版

最简单的效果已经实现,但是有许多可以增强的功能,例如:文字内容自定义、文字背景颜色自定义、文字大小自定义、滚动速度自定义

这里重点说下对于速度的自定义,在网上看了几种方案,基本思路就是通过在重写onDraw、或者使用Scroller的方式实现文字的位置变化,通过post(Runnable)的方式触发文字位置的不断更新。不过通过上面我们知道原生TextView控件中已经实现了跑马灯效果,只是没有提供自定义速度的功能,那是不是可以从TextView的实现中找找思路?

TextView的源码的代码量可以说是相当大,在android-28的源码中有12000多行,因为目标是跑马灯,所以只看相关部分,关键词搜索发现,有个静态内部类Marquee,这是承载了跑马灯滚动距离计算逻辑的类,就从它入手。

 

源码分析

    private static final class Marquee {
​
        private static final int MARQUEE_DELAY = 1200;
        private static final int MARQUEE_DP_PER_SECOND = 30;
        private final WeakReference mView;
        private final Choreographer mChoreographer;
​
        private byte mStatus = MARQUEE_STOPPED;
        private final float mPixelsPerMs;
        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;
            mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
            mView = new WeakReference(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 * mPixelsPerMs;
                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);
            }
        }
        ...
    }

简单分析下其中的逻辑,这个类中内容并不复杂

1、定义了三个Choreographer.FrameCallback,用来触发每帧文字滚动距离的计算

2、tick函数负责计算每帧的滚动距离,是最核心的一个函数,其中mPixelsPerMs变量就控制了文字的滚动速度

3、start函数主要是对tick中计算使用的变量进行初始化

 

自定义实现

那么完全可以按照Marquee的思想,实现自定义MarqueeView

class MarqueeView : TextView {
    companion object {
        private const val MARQUEE_PX_PER_SECOND = 100
        private const val MARQUEE_DELAY:Long = 1200
    }
​
    private val mChoreographer: Choreographer = Choreographer.getInstance()
    private var mScroll = 0
    private var mMaxScroll = 0
    private var mGhostStart = 0f
    private var mGhostOffset = 0f
    private var mLastAnimationMs = System.currentTimeMillis()
    private var pxPreSecond = MARQUEE_PX_PER_SECOND
​
    constructor(ctx: Context) : super(ctx) {
        initView()
    }
​
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
        initView()
    }
​
    constructor(ctx: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
        ctx,
        attrs,
        defStyleAttr
    ) {
        initView()
    }
​
    private fun initView() {
        mLastAnimationMs = System.currentTimeMillis()
        postDelayed({ start() }, MARQUEE_DELAY)
    }
​
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas!!.save()
        // 保证循环滚动可以连接上
        if (mScroll > mGhostStart) {
            canvas.translate(layout.getParagraphDirection(0) * mGhostOffset, 0.0f)
            layout.draw(canvas, Path(), Paint(), 0)
        }
        canvas.restore()
    }
​
    private val mTickCallback = FrameCallback { tick() }
    private val mRestartCallback = FrameCallback { start() }
​
    private fun tick() {
        val current = System.currentTimeMillis()
        val mPixelsPerMs = pxPreSecond / 1000f
        // 两帧的时间间隔
        val spend = current - mLastAnimationMs
        mLastAnimationMs = current
        // 两帧的滚动距离
        mScroll += (spend * mPixelsPerMs).toInt()
        scrollTo(mScroll, 0)
        if (scrollX >= mMaxScroll) {
            mScroll = mMaxScroll
            mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY)
        } else {
            mChoreographer.postFrameCallback(mTickCallback)
        }
    }
​
    private fun start() {
        mScroll = 0
        mLastAnimationMs = System.currentTimeMillis()
        val textWidth = getTextWidth()
        val lineWidth = this.layout.getLineWidth(0)
        val gap = textWidth / 3.0f
        mGhostStart = lineWidth - textWidth + gap
        mGhostOffset = lineWidth + gap
        mMaxScroll = (mGhostStart + textWidth).toInt()
​
        if (this.context.resources.displayMetrics.widthPixels >= lineWidth) {
            return
        } else {
            textAlignment = TEXT_ALIGNMENT_INHERIT
        }
        mChoreographer.postFrameCallback(mTickCallback)
    }
​
    private fun getTextWidth(): Int {
        return width - compoundPaddingLeft - compoundPaddingRight
    }
​
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        mChoreographer.removeFrameCallback(mTickCallback)
        mChoreographer.removeFrameCallback(mRestartCallback)
    }
​
    fun setSpeed(pxPreSecond: Int) {
        this.pxPreSecond = pxPreSecond
    }
}

​​​​​​对于字体、颜色等设置相对比较容易,这里就不再介绍

最终效果如下

文字全屏轮播效果实现_第2张图片


                                                              欢迎关注公众号:从技术到艺术

                                                 

你可能感兴趣的:(Android)