刷抖音时看到一个全屏文字滚动播放的效果,如图
想了下,其实效果不难实现,沉浸式+TextView跑马灯效果即可实现
基础版
1、设置沉浸式
这里采用了给Activity设置style的方式,res/values/styles.xml中
<resources> ... <style name="AppFullScreenTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowNoTitle">trueitem> <item name="android:windowActionBar">falseitem> <item name="android:windowFullscreen">trueitem> <item name="android:windowContentOverlay">@nullitem>style>resources>
并在AndroidManifest.xml文件中使用自定义style,并将Activity设置为横屏
<activity android:name=".ScreensaverActivity" android:screenOrientation="landscape" android:theme="@style/AppFullScreenTheme"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> intent-filter>activity>
2、跑马灯
Android的TextView是自带跑马灯效果的,布局文件如下
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" android:id="@+id/screensaverLayout"> <TextView android:id="@+id/screensaverTv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:focusable="true" android:focusableInTouchMode="true" android:textSize="80sp" android:textColor="@color/white" android:ellipsize="marquee" android:marqueeRepeatLimit="marquee_forever" android:singleLine="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />androidx.constraintlayout.widget.ConstraintLayout>
这样就实现了最简单的效果,黑底白字全屏滚动播放。
加强版
最简单的效果已经实现,但是有许多可以增强的功能,例如:文字内容自定义、文字背景颜色自定义、文字大小自定义、滚动速度自定义
这里重点说下对于速度的自定义,在网上看了几种方案,基本思路就是通过在重写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 }}
对于字体、颜色等设置相对比较容易,这里就不再介绍
最终效果如下