利用Android定时任务实现文本打印效果

237 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

前言

之前的文章又讲到过Android的倒计时实现和Android的定时任务实现。我们基于它们除了一些常规的计时显示之外还能做什么操作呢?

产品看到竞品App有这样的功能,希望我们的App也能加上,文字自动打印字符的功能。

这还不简单,拿到需要展示的文本之后一个一个展示不就行了吗?

一、最简单的做法Handler计时

   fun printText() {
        sendTextMessage()
    }

    private val text = "Hello World."
    private var curIndex = 0
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            if (msg.what == 101) {
                val text: String = msg.obj as String

                YYLogUtils.w("当前展示的字符串:"+text)

                mBinding.tvPrint.text = text

                sendTextMessage()
            }
        }
    }


    private fun sendTextMessage() {
        val message = handler.obtainMessage()
        message.what = 101
        curIndex += 1
        if (curIndex <= text.length) {
            val text = text.substring(0, curIndex)
            message.obj = text
            handler.sendMessage(message)
        } else {
            YYLogUtils.w("写完了")
            curIndex = 0
        }
    }

我们把每一个字符发送给Handler不就能完成了吗?咦?为什么打印正常,但是没有打印效果呢?

因为执行速度太快了,16.6毫秒之内就执行完毕了,一次就全丢缓冲区了,那不得一次上屏了吗?这样就直接显示最终的效果了,肯定是没有打印效果的啦。

我们修改一下,延时17毫秒执行,确保每次只有当前需要显示的字符上屏展示即可:

    fun printText() {
        sendTextMessageDelay(0)
    }

    private val text = "Hello World."
    private var curIndex = 0
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            if (msg.what == 101) {
                val text: String = msg.obj as String

                YYLogUtils.w("当前展示的字符串:"+text)

                mBinding.tvPrint.text = text

                sendTextMessageDelay(17)
            }
        }
    }

    private fun sendTextMessageDelay(delay: Long) {
        val message = handler.obtainMessage()
        message.what = 101
        curIndex += 1
        if (curIndex <= text.length) {
            val text = text.substring(0, curIndex)
            message.obj = text
            handler.sendMessageDelayed(message, delay)
        } else {
            YYLogUtils.w("写完了")
            curIndex = 0
        }
    }

这样就可以完成了往前打印的效果

一、往前打印与往后打印

效果图是往前打印完了,再往回收回,再打印下一个文本,其实往回收回也时一样的逻辑,从文本的lenght开始往前打印

   private val text = "Hello World."
    private var curIndex = 0
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            if (msg.what == 101) {
                val text: String = msg.obj as String

                YYLogUtils.w("当前展示的字符串:" + text)

                mBinding.tvPrint.text = text

                printForwardMessage(17L)

            } else if (msg.what == 102) {

                val text: String = msg.obj as String

                YYLogUtils.w("当前展示的字符串:" + text)

                mBinding.tvPrint.text = text

                printBackMessage(17L)
            }
        }
    }

    private fun printForwardMessage(delay: Long) {
        val message = handler.obtainMessage()
        message.what = 101
        curIndex += 1
        if (curIndex <= text.length) {
            val text = text.substring(0, curIndex)
            message.obj = text
            handler.sendMessageDelayed(message, delay)
        } else {
            YYLogUtils.w("写完了")
            curIndex = text.length

            printBackMessage(delay)
        }
    }

    private fun printBackMessage(delay: Long) {
        val message = handler.obtainMessage()
        message.what = 102
        curIndex -= 1
        if (curIndex >= 0) {
            val text = text.substring(0, curIndex)
            message.obj = text
            handler.sendMessageDelayed(message, delay)
        } else {
            YYLogUtils.w("写完了")
            curIndex = 0

            printForwardMessage(delay)
        }
    }

    //打印文本
    fun printText() {
        printForwardMessage(0)
    }

效果:

当然,你觉得打印速度太快了,可以调节速度的,只要是大于17ms以上都可以达到打印的效果。

三、其他方式的定时任务能不能实现?

道理是一样的,之前讲的倒计时与App内的定时任务都可以完成这样的效果,例如我们使用Thread+Handler一样能实现这样的效果。

并且我们还需要优化一下,绑定生命周期让它可以暂停、恢复、销毁等功能。

完整代码如下:

/**
 * 仿制EditText 一个一个字符的添加 和删除的效果,并轮播展示
 */
public class AutoLoopTextView extends AppCompatTextView implements ILifecycleObserver {

    private ExecutorService mSingleThreadExecutor;

    public AutoLoopTextView(@NonNull Context context) {
        super(context);
    }

    public AutoLoopTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoLoopTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private List<String> mLoopDatas;
    private int mTextCount = 0;
    private int mCurIndex = 0;
    private long charTimeMilles = 50L;    //文本打印的时间间隔  默认50毫秒
    private long clearTimeMilles = 2500L;  //清空的时间间隔    默认2.5秒

    private Handler mHandler;


    /**
     * 设置下一个文本
     */
    @SuppressLint("SetTextI18n")
    private void setNextText(String str, int index) {
        if (TextUtils.isEmpty(str)) return;

        setTag(str);   //取值从Tag取

        if (mSingleThreadExecutor == null) return;
        mSingleThreadExecutor.execute(() -> {

            int n = index;
            int nn;

            try {
                String stv;
                if (str.length() < n) {
                    stv = str.substring(0, str.length());
                } else {
                    stv = str.substring(0, n);
                }

                post(() -> {
                    if (!TextUtils.isEmpty(stv)) {
                        setText(stv);
                    }
                });

                Thread.sleep(charTimeMilles);//休息片刻

                nn = n + 1; //n+1;多截取一个

                if (nn <= str.length()) { //如果还有汉字,那么继续开启线程,相当于递归的感觉
                    try {
                        setNextText(str, nn);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    //写完了,开始延时清除
                    if (mHandler != null) {
                        mHandler.removeCallbacksAndMessages(null);
                        mHandler.sendEmptyMessageDelayed(102, clearTimeMilles);
                    } else {
                        resetHandle();
                    }

                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

    }

    /**
     * 清空上一个文本
     */
    private void clearPreText(int index) {
        String str = getText().toString();
        if (TextUtils.isEmpty(str)) return;

        if (mSingleThreadExecutor == null) return;
        mSingleThreadExecutor.execute(() -> {

            int n = index == -1 ? str.length() : index;
            int nn;

            try {
                if (n == 0) {
                    post(() -> {
                        setText("");
                    });
                } else {
                    String stv;
                    if (str.length() < n) {
                        stv = str.substring(0, str.length());
                    } else {
                        stv = str.substring(0, n);
                    }

                    post(() -> {
                        if (!TextUtils.isEmpty(stv)) {
                            setText(stv);
                        }
                    });
                }

                Thread.sleep(charTimeMilles);//休息片刻

                nn = n - 1;//n-1;少截取一个

                if (nn >= 0) {  //如果还有汉字,那么继续开启线程,相当于递归的感觉
                    try {
                        clearPreText(nn);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    //清除完了,开始写入
                    if (mHandler != null) {
                        mHandler.removeCallbacksAndMessages(null);
                        mHandler.sendEmptyMessage(101);
                    } else {
                        resetHandle();
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });
    }

    /**
     * 设置轮播数据
     */
    public void setLoopDate(List<String> list) {
        mSingleThreadExecutor = Executors.newSingleThreadExecutor();
        initHandle();
        mLoopDatas = list;
        mTextCount = list.size();
    }

    @SuppressLint("HandlerLeak")
    private void initHandle() {
        //主线程创建Handle
        if (mHandler == null) {
            setupHandler();
        }
    }

    private void setupHandler() {
        mHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(@NonNull @NotNull Message msg) {

                if (msg.what == 101) {
                    //开始写入
                    if (mCurIndex >= mTextCount) {
                        mCurIndex = 0;
                    }

                    try {
                        setNextText(mLoopDatas.get(mCurIndex), 0);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    mCurIndex++;

                } else if (msg.what == 102) {
                    //开始清除
                    try {
                        clearPreText(-1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }
        };
    }

    /**
     * 重启Handle
     */
    @SuppressLint("HandlerLeak")
    private void resetHandle() {
        if (mSingleThreadExecutor != null) {
            //先停止线程池
            mSingleThreadExecutor.execute(() -> {
                YYLogUtils.w("auto loop 停止线程");
                if (!CheckUtil.isEmpty(mLoopDatas)) {
                    mTextCount = mLoopDatas.size();
                }
            });
        }

        post(() -> {
            //主线程创建Handle
            if (mHandler == null) {
                setupHandler();
            }

            mHandler.sendEmptyMessage(101);
        });

    }

    /**
     * 开始轮播
     */
    public void startTextLoop() {
        YYLogUtils.w("auto loop 开始轮播 startTextLoop");
        if (mLoopDatas == null || mLoopDatas.isEmpty()) return;
        //从写入文本开始
        mSingleThreadExecutor.execute(() -> {
            YYLogUtils.w("auto loop 停止线程");
            if (!CheckUtil.isEmpty(mLoopDatas)) {
                mTextCount = mLoopDatas.size();
            }
        });
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler.sendEmptyMessage(101);
        } else {
            resetHandle();
        }
    }

    /**
     * 停止轮播
     */
    public void stopTextLoop() {
        YYLogUtils.w("auto loop 停止轮播 stopTextLoop");
        if (mLoopDatas == null || mLoopDatas.isEmpty()) return;

        mSingleThreadExecutor.execute(() -> {
            YYLogUtils.w("auto loop 停止线程");
            if (!CheckUtil.isEmpty(mLoopDatas)) {
                mTextCount = mLoopDatas.size();
            }
        });
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
        }

    }

    /**
     * 销毁资源
     */
    public void destroyTextLoop() {
        YYLogUtils.w("auto loop 销毁资源 destroyTextLoop");
        stopTextLoop();
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
        }
        if (mSingleThreadExecutor != null && !mSingleThreadExecutor.isShutdown()) {
            mSingleThreadExecutor.shutdown();
        }
        mSingleThreadExecutor = null;
    }

    /**
     * **********************************************************************
     * ------------------------ 生命周期控制 --------------------------------*
     * **********************************************************************
     */

    public void addLoopLifecycleObserver(LifecycleOwner owner) {
        if (owner != null) {
            owner.getLifecycle().addObserver(new LifecycleObserverAdapter(owner, this));
        }
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        startTextLoop();
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        stopTextLoop();
    }

    @Override
    public void onDestroy(LifecycleOwner owner) {
        destroyTextLoop();
    }

}

效果图中,往前打印与往后打印的时长是不同我们分别控制。

需要注意的是我们没有直接使用Thread,而是使用单线程池来管理线程。

然后我们绑定生命周期进行暂停与恢复。暴露公共方法设置数据源即可开始打印效果。

使用的时候:

      List<String> datas = new ArrayList<>();
        for (PartTimeSearchKeywords item : list) {
            datas.add(item.employer_name);
        }
        mTvAutoLoop.setLoopDate(datas);
        mTvAutoLoop.addLoopLifecycleObserver(this);
        mTvAutoLoop.startTextLoop();

最终效果即为最开始的效果图样子,这里就不再贴出。

总结

其实原理不复杂,就是利用了之前所学的定时任务的机制,加上屏幕刷新机制的原理,就可以实现文本的打印效果。

代码都全部贴出来了,如果有需求的话,自取即可。

好了,如有讲解不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,完结。