Android 自定义View之MarqueeText,实现跑马灯效果

1. 背景

    需要做一个跑马灯效果的文字展示,本方案更适用于开发机顶盒应用的同学们。

2. 需求

    2.1 展示书籍的页面,文字过多的时候需要折叠

    2.2 当焦点在此书籍上的时候,需要将折叠的文字滚动起来

3. 需求剖析

    3.1 正常情况下使用跑马灯,只需要设置Textview的以下属性即可

android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"

    设置完成之后,当这个TextView有焦点的时候,且文字过长的时候,就会展现跑马灯效果,注意当TextView请求焦点的时候,跑马灯效果才展示,否则无效。

    3.2 但是在机顶盒上,app是需要使用焦点效果来展示你所选的当前item,也就是通常所说的“落焦”。

    3.3 这时候使用跑马灯的时候会产生两个问题

        问题一:我的item是在一个recycleView当中,充当跑马灯的TextView只是recycleView中子view 中的子view,我外层的布局需要拿到焦点,才能在recycleView中正常移动。

        问题二:即使我将TextView设置请求焦点,那么当我从当前的item向上滑动的时候,recycleView就不再认为我是在他的子view间移动,recycleView的翻页效果会有问题,就是当你上一行展示了一半,但是我焦点已经在上一行了,但是上一行却没有展示完全。如下图所示:

4. 解决思路

    4.1 我需要有一个不需要焦点,仍然能够展现跑马灯的控件,避免与recycleView之间的冲突

    4.2 同时我这个控件也需要有TextView原有的功能,便于我在简单情况下使用跑马灯效果

5. 代码

    5.1 布局文件 resource_item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/dimen_166dp"
    android:layout_height="@dimen/dimen_234dp">

    <ImageView
        android:id="@+id/item_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/column_test_image" />

    <plat.skytv.main.view.MarqueeText
        android:id="@+id/item_name"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dimen_37dp"
        android:alpha="0.6"
        android:singleLine="true"
        android:background="@android:color/black"
        android:gravity="center"
        android:layout_gravity="bottom"
        android:textColor="@color/colorWhite"
        android:textSize="@dimen/dimen_17sp" />

    <ImageView
        android:id="@+id/item_cornerMask"
        style="@style/bigConerMask" />

    <ImageView
        android:id="@+id/item_focus"
        style="@style/FocusStyle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/column_grid_item_selector" />
</FrameLayout>

    5.2 自定义view

package plat.skytv.main.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatTextView;

import plat.skytv.main.util.PLog;

public class MarqueeText extends AppCompatTextView implements Runnable {
    private int xLocation;// 当前滚动的位置
    private boolean isStop = false;
    private int textWidth;
    private boolean isMeasure = false;

    private float speed = 1; // 移动速度
    private String string; // 需要绘制得文字


    public MarqueeText(Context context) {
        super(context);
    }

    public MarqueeText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MarqueeText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    float textHeight;


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isMeasure) {// 文字宽度只需获取一次就可以了
            getTextWidth();
            textHeight = getTextHeight(getPaint());
            isMeasure = true;
            string = this.getText().toString();
        }
        canvas.drawText(string, xLocation, getHeight() / 2 + textHeight / 2, getPaint());
    }

    /**
     * http://blog.csdn.net/u014702653/article/details/51985821
     * 详细解说了
     *
     * @param
     * @return
     */
    private float getTextHeight(Paint paint) {
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        return Math.abs((fontMetrics.bottom - fontMetrics.top)) / 2;
    }

    /**
     * 获取文字宽度
     */
    private void getTextWidth() {
        Paint paint = this.getPaint();
        string = this.getText().toString();
        PLog.e("getTextWidth str = " + string);
        textWidth = (int) paint.measureText(string);
    }

    @Override
    public void run() {
        xLocation -= speed;// 滚动速度
        if (isStop) { // 停止滚动后恢复初始状态
            xLocation = 0;
            return;
        }
        if (textWidth <= (-xLocation)) {
            //也就是说文字已经到头了
            xLocation = getWidth();
        }
        postInvalidate();
        postDelayed(this, 10);
    }

    // 开始滚动 针对不能请求焦点的情况
    public void startScroll() {
        if (textWidth <= this.getWidth()) { // 文字长度 <= view长度,不需要滚动
            return;
        }
        setText("");// 需要设置空 否则展示两个文字
        setSingleLine(false); // 需要设置false,否则无法滚动
        isStop = false;
        this.removeCallbacks(this);
        post(this);

    }

    // 停止滚动
    public void stopScroll() {
        isStop = true;
        setText(string);
        setSingleLine(true);
    }
}

    5.3 使用

    在recycleView绑定数据的时候设置焦点变化监听

ImageView mItemFocus = itemView.findViewById(R.id.item_focus); // 展示焦点效果的view
MarqueeText mItemName = itemView.findViewById(R.id.item_name);
mItemFocus.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    mItemName.startScroll();
                } else {
                    mItemName.stopScroll();
                }
            }
        });

6. 问题

    6.1 在布局文件中,设置了singleline = true,结果,调用  startScroll 时无法滚动。

    6.2 在滚动之前必须将本TextView的text设置为空串,否则将出现两个文本。

    6.3 先存疑,以后找到结果了再贴上去

7. 参考文档:

    7.1 http://blog.csdn.net/u014702653/article/details/51985821

    7.2 https://www.cnblogs.com/hyhy904/p/11329139.html

猜你喜欢

转载自blog.csdn.net/HeartCircle/article/details/114001434