安卓设置textview文字位置_全屏文字滚动播放效果

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

f1081addd75febcd534ab7284e9504e9.gif

想了下,其实效果不难实现,沉浸式+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    }}

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

最终效果如下

001ca7a98fa0eeaf97d4b6923a3bd600.png

76900413afa61f2f2617fcfdda456ef7.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值