刷短视频时看到一个全屏文字滚动播放的效果,如图
想了下,其实效果不难实现,沉浸式+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
}
}
对于字体、颜色等设置相对比较容易,这里就不再介绍
最终效果如下
欢迎关注公众号:从技术到艺术