可以控制移动方向的跑马灯

"基于textview设计"

Posted by Weiwq on February 3, 2019

“跑马灯?so easy~~”

引言

最近客户提了一个很小很小但是很恶心的需求:在波斯语言环境(右到左布局)下,英文需要从右到左移动,波斯文需要从左到右移动。

第一反应是,这是什么玩意啊,textview的跑马灯方向是Android控制的。不过转念一想也是,不同文字,读取的方向是不一样的,需要做适配。在研究了一波textview源码之后,产出了这篇blog

1、 textview的源码解析

这里就直接看onDraw方法,其中有一段代码是这样的:

        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
        if (isMarqueeFadeEnabled()) {
            if (!mSingleLine && getLineCount() == 1 && canMarquee()
                    && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
                final int width = mRight - mLeft;
                final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
                final float dx = mLayout.getLineRight(0) - (width - padding);
                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
            }

            if (mMarquee != null && mMarquee.isRunning()) {
                final float dx = -mMarquee.getScroll();
                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
            }
        }

这里先获取到当前布局方向,然后设置偏移量,其中getLayoutDirection方法是可以重写的,于是尝试一下,创建ControllableMarquee类

public class ControllableMarquee extends TextView {
    private int LAYOUT_DIRECTION = LAYOUT_DIRECTION_RTL;

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

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

    public ControllableMarquee(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean isFocused() {
        return true;
    }

    @Override
    public int getLayoutDirection() {
        return LAYOUT_DIRECTION;
    }

    public void setText(String content, boolean moveToLeft) {
        LAYOUT_DIRECTION = moveToLeft ? LAYOUT_DIRECTION_LTR : LAYOUT_DIRECTION_RTL;
        setText(content);
    }
}

创建一个MarqueeActivity

public class MarqueeActivity extends BaseActivity {

    private ControllableMarquee marqueeDirectionTextView1;
    private ControllableMarquee marqueeDirectionTextView2;
    private static final String TAG = "MarqueeActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_marquee);
        marqueeDirectionTextView1 = findViewById(R.id.marqueeTextView1);
        marqueeDirectionTextView2 = findViewById(R.id.marqueeTextView2);
        marqueeDirectionTextView1.setText("کتابخانه صوتی خالی استکتابخانه صوتی خالی استکتابخانه صوتی خالی است ", true);
        marqueeDirectionTextView2.setText("afewfeohtebqofqodhqbfevwbfoqbwdoqbdowq", false);
    }

    @Override
    public boolean isRtl() {
        return true;
    }
}

对应的xml布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp"
    tools:context=".activity.MarqueeActivity">

    <TextView
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:background="@color/colorAccent"
        android:ellipsize="marquee"
        android:focusable="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        android:text="کتابخانه  صوتی خالی استکتابخانه صوتی خالی است " />

    <TextView
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:background="@color/colorPrimary"
        android:ellipsize="marquee"
        android:focusable="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        android:text="fewfwefwefwefwecwefdwdwq "/>


    <demo.com.rtldemo.view.ControllableMarquee
        android:id="@+id/marqueeTextView1"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:background="@color/colorAccent"
        android:ellipsize="marquee"
        android:focusable="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        android:text=" تنظیمتنظیم تنظیم ضبط:"/>

    <demo.com.rtldemo.view.ControllableMarquee
        android:id="@+id/marqueeTextView2"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:background="@color/colorPrimary"
        android:ellipsize="marquee"
        android:focusable="true"
        android:marqueeRepeatLimit="marquee_forever"
        android:singleLine="true"
        android:text="adbcdffeqeqqweqdwqd " />

</LinearLayout>

其中BaseActivity如下,主要设置当前应用的语言环境

public abstract class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Locale locale = new Locale("fa_IR");
        Locale.setDefault(locale);
        Configuration config = new Configuration();
        config.locale = locale;
        DisplayMetrics dm = getResources().getDisplayMetrics();
        if (isRtl()) {
            getResources().updateConfiguration(config, dm);
        }
    }

    public abstract boolean isRtl();
}

效果如下:

在这里插入图片描述

这是在模拟器上跑的,并没有任何效果,但是我在tv上测试是没有问题的,有适配的需要。

2、TextDirection

上面直接设置layoutDirection不一定有效果,那么得接着看源码 在textview画string的时候,注意到这几行代码:

        if (mEditor != null) {
            mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
        } else {
            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
        }

这里的layout是画string的主要实现,那么layout是如何创建的?很容易就找到makeSingleLayout方法:

    /**
     * @hide
     */
    protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
            boolean useSaved) {
        Layout result = null;
        if (useDynamicLayout()) {
            final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
                    wantWidth)
                    .setDisplayText(mTransformed)
                    .setAlignment(alignment)
                    .setTextDirection(mTextDir)
                    .setLineSpacing(mSpacingAdd, mSpacingMult)
                    .setIncludePad(mIncludePad)
                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                    .setBreakStrategy(mBreakStrategy)
                    .setHyphenationFrequency(mHyphenationFrequency)
                    .setJustificationMode(mJustificationMode)
                    .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                    .setEllipsizedWidth(ellipsisWidth);
            result = builder.build();
        }
        .......
     }

这里有一个setTextDirection方法,来设置layout的text布局方向,对于direction会下意识的保持高度警惕,那么如何设置mTextDir呢?接着看:

有这么一段代码:

        mTextDir = getTextDirectionHeuristic();

那就接着看getTextDirectionHeuristic方法:

    protected TextDirectionHeuristic getTextDirectionHeuristic() {
			......    
        // Now, we can select the heuristic
        switch (getTextDirection()) {
            default:
            case TEXT_DIRECTION_FIRST_STRONG:
                return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
                        TextDirectionHeuristics.FIRSTSTRONG_LTR);
            case TEXT_DIRECTION_ANY_RTL:
                return TextDirectionHeuristics.ANYRTL_LTR;
            case TEXT_DIRECTION_LTR:
                return TextDirectionHeuristics.LTR;
            case TEXT_DIRECTION_RTL:
                return TextDirectionHeuristics.RTL;
            case TEXT_DIRECTION_LOCALE:
                return TextDirectionHeuristics.LOCALE;
            case TEXT_DIRECTION_FIRST_STRONG_LTR:
                return TextDirectionHeuristics.FIRSTSTRONG_LTR;
            case TEXT_DIRECTION_FIRST_STRONG_RTL:
                return TextDirectionHeuristics.FIRSTSTRONG_RTL;
        }
    }

好,重写getTextDirection方法咯

3、ControllableMarquee实现

public class ControllableMarquee extends TextView {
    private int LAYOUT_DIRECTION = LAYOUT_DIRECTION_RTL;
    private int TEXT_DIRECTION = TEXT_DIRECTION_RTL;

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

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

    public ControllableMarquee(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean isFocused() {
        return true;
    }

    @Override
    public int getLayoutDirection() {
        return LAYOUT_DIRECTION;
    }

    public int getTextDirection() {
        return TEXT_DIRECTION;
    }


    public void setText(String content, boolean moveToLeft) {
        LAYOUT_DIRECTION = moveToLeft ? LAYOUT_DIRECTION_LTR : LAYOUT_DIRECTION_RTL;
        TEXT_DIRECTION = moveToLeft ? TEXT_DIRECTION_LTR : TEXT_DIRECTION_RTL;
        setText(content);
    }
}

效果如下:

在这里插入图片描述

后记

本篇主要通过重写textview的getLayoutDirection和getTextDirection方法,就能实现控制跑马灯的方向,避免了textview无法控制方向的问题

明天就要过年了,致仍然在公司奋斗的我和你们~~~新年快乐!

—— Weiwq 后记于 2019.02 广州