Android TextView 文本展示优化

前言

TextView是相当复杂的UI组件,TextView不仅仅支持纯文本展示,而且还支持图片、SpannableString、文本输入、超链接等诸多功能,因此很多View本身也是直接继承自TextView的,如EditText、Button、Chronometer等。可见TextView功能非常强大,基本上是app中使用率最高的View组件。

在这里插入图片描述

不过 TextView 缺点也不少,主要问题点如下:

  • 跑马灯执行的条件过高,且部分属性有一定的重复问题
  • setText 容易触发requestLayout
  • 换行文本容易出现犬牙(很多小说类app自行绘制文本来解决此问题)

当然,以上是大多数情况中我们容易遇到的问题。

优化方法

上面我们列出了3个常见的问题,我们这边逐一来看。

跑马灯问题

TextView对跑马灯的要求比较高,必须是单行文本,而且必须设置MaxLines,而且不支持Lines设置,另外必须是focused或者是selected,这显然增加了一些成本,要知道如果父布局focused,那么子View是不可能focused,显然对TV设备不够有好。但是另外一个问题,View可以同时具备Focused和Selected状态,这显然增加了问题的难度,为此我们需要剥离focused状态。

   private void startMarquee() {
        // Do not ellipsize EditText
        if (getKeyListener() != null) return;

        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
//宽度大于0,或者硬件加速
            return;
        }


        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
                && getLineCount() == 1 && canMarquee()) {

            //获焦或者selected状态,由于focus相对于selected复杂,建议使用selected
            //TextLayout行数必须为1

            if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
                mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
                final Layout tmp = mLayout;
                mLayout = mSavedMarqueeModeLayout;
                mSavedMarqueeModeLayout = tmp;
                setHorizontalFadingEdgeEnabled(true);
                requestLayout();
                invalidate();
            }

            if (mMarquee == null) mMarquee = new Marquee(this);
            mMarquee.start(mMarqueeRepeatLimit);
        }
    }

那么,这里我们通过优化,使其仅在selected状态具备跑马灯,当然,如果你还想用selected状态实现其他用途,显然是无法使用了,不过系统中还有setEnable、setActivated状态供大家使用。

下面是跑马灯兼容逻辑

public class MarqueeTextView extends AppCompatTextView {

    private static final String TAG = "MarqueeTextView";
    private boolean isMarqueeEnable = false;

    public MarqueeTextView(Context context) {
        this(context, null);
    }

    public MarqueeTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        /**
         * TextView.canMarquee() == false 时是不会滚动的
         * 一般原因是行数问题影响,导致宽度不合适,而android:lines是无效的
         *  focus 或者 selected状态才能跑马灯
         */

        setMaxLines(1);
        setSingleLine(true);

        if (isMarqueeEnable) {
            setMarqueeRepeatLimit(-1);
            setEllipsize(TextUtils.TruncateAt.MARQUEE);
        } else {
            setMarqueeRepeatLimit(0);
            setEllipsize(TextUtils.TruncateAt.END);
        }
        super.setSelected(isMarqueeEnable);
    }

    public void setMarqueeEnable(boolean enable) {
        if (isMarqueeEnable != enable) {
            isMarqueeEnable = enable;
            if (enable) {
                super.setSelected(true);
                setMarqueeRepeatLimit(-1);
                setEllipsize(TextUtils.TruncateAt.MARQUEE);
            } else {
                super.setSelected(false);
                setMarqueeRepeatLimit(0);
                setEllipsize(TextUtils.TruncateAt.END);
            }
        }
    }

    public boolean isMarqueeEnable() {
        return isMarqueeEnable;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!isMarqueeEnable) {
            return;
        }
        if (getLineCount() > 1) {
            Log.e(TAG, "the marquee will not work if  TextLineCount > 1");
        }
        if (getMarqueeRepeatLimit() <= 0) {
            Log.e(TAG, "the marquee may not work if  MarqueeRepeatLimit != -1");
        }
    }

    @Override
    public void setSelected(boolean selected) {
   //复写此方法,禁止外部调用,保证只有内部调用
    }

  
}

频繁触发requestLayout

TextView很容易触发requestLayout,除非长宽必须是固定大小的,不过固定大小可能遇到文本展示的不全的问题,另外Google也提供了PrecomputedText异步测量文本的方式去优化性能,但是requestLayout造成的性能问题实际上比测量要大,另外PrecomputedText编码方式也不够方便。

那么有没有更好的方法去抑制requestLayout的频繁调用呢?

实际上我们对单行文本的使用远超多行文本,即便是播放器时间进度也是单行文本,因此我们可以自行测量单行文本,比较前后的尺寸差异,选择性调用requestLayout。

方式很多,这里我们利用BoringLayout优化,当然在android 5.0之前的版本BoringLayout 兼容性并不好,因此这里还引入StaticLayout进行兜底。

优化setText

构建Layout

    protected Layout buildTextLayout(CharSequence text, int wantWidth) {
        // fixed: Chinese word is not boring in android 4.4,在Android 4.4长度可能小于measureText测量的
        float measureTextWidth = mTextPaint.measureText(text, 0, text.length());
        BoringLayout.Metrics boring = BoringLayout.isBoring(text, mTextPaint,UNKNOWN_BORING);

        float lineSpaceMult = mLineSpacingMult;
        if (lineSpaceMult < 1F) {
            lineSpaceMult = 1.0f;
        }
        if (boring != null) {
            int outWidth = wantWidth != ANY_WIDTH ? wantWidth : (int) (Math.max(boring.width,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
            return BoringLayout.make(text, mTextPaint,
                    outWidth,
                    Layout.Alignment.ALIGN_NORMAL,
                    0,
                    mLineSpacingAdd,
                    boring,
                    mIncludeFontPadding);
        }
        //fix Android  4.4  mLineSpacingMult 必须大于0
        float desiredWidthForStaticLayout = StaticLayout.getDesiredWidth(text, mTextPaint);
        int desiredWidth = (int) (Math.max(desiredWidthForStaticLayout,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
        int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
        StaticLayout staticLayout = new StaticLayout(text,
                mTextPaint,
                outWidth,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpaceMult,
                mLineSpacingAdd,
                mIncludeFontPadding);
        return staticLayout;
    }

设置文本

    public void setText(final CharSequence text) {
        CharSequence targetText = text == null ? "" : text;
        if (mLayout != null && TextUtils.equals(targetText, this.mText)) {
            return;
        }
        this.mText = targetText;
        if (!isAttachedToWindow()) {
            mLayout = null;
            mHintLayout = null;
            return;
        }
        if (measureWidthMode == -1 || measureHeightMode == -1) {
            mLayout = null;
            mHintLayout = null;
            requestLayout();
            invalidate();
            return;
        }
        int width = measureWidthMode == MeasureSpec.EXACTLY ? getMeasuredWidth() : ANY_WIDTH;
        mHintLayout = buildTextLayout(text, width);

        int desireWidth = mHintLayout.getWidth() + getPaddingLeft() + getPaddingRight();
        int desireHeight = getTextLayoutHeight(mHintLayout)+ getPaddingTop() + getPaddingBottom();

        if (desireWidth != getWidth() || measureHeightMode != MeasureSpec.EXACTLY && desireHeight != getHeight()) {
            mLayout = null;
            requestLayout();
        } else {
            mLayout = mHintLayout;
            mHintLayout = null;
        }
        invalidate();
    }

完整代码

public class BoringTextView extends View {

    private static final int ANY_WIDTH = -1;
    private static final String TAG = "BoringTextView";
    private TextPaint mTextPaint;
    private DisplayMetrics mDisplayMetrics;
    private int mContentHeight = 0;
    private int mContentWidth = 0;
    private Layout mLayout;
    private Layout mHintLayout;
    private int mTextColor;
    private ColorStateList mTextColorStateList;
    private CharSequence mText = "";
    private boolean mIncludeFontPadding = false;
    private int measureWidthMode = -1;
    private int measureHeightMode = -1;
    // fixed: mSpacingMult in android 4.4 must be greater 0
    private float mLineSpacingMult = 1.0f;
    private float mLineSpacingAdd = 0.0f;

    public static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();


    public BoringTextView(Context context) {
        this(context, null);
    }

    public BoringTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint(context, attrs, 0, 0);

    }

    public BoringTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint(context, attrs, defStyleAttr, 0);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public BoringTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initPaint(context, attrs, defStyleAttr, defStyleRes);
    }

    private void initPaint(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        Resources resources = getResources();
        mDisplayMetrics = resources.getDisplayMetrics();
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(sp2px(12));
        mTextPaint.density = mDisplayMetrics.density;
        mTextColorStateList = ColorStateList.valueOf(Color.GRAY);

        if (attrs != null) {
            int[] attrset = {
                    //注意顺序,从大到小,否则无法正常获取
                    android.R.attr.textSize,
                    android.R.attr.textColor,
                    android.R.attr.text,
                    android.R.attr.includeFontPadding
            };
            TypedArray attributes = context.obtainStyledAttributes(attrs, attrset, defStyleAttr, defStyleRes);
            int length = attributes.getIndexCount();
            for (int i = 0; i < length; i++) {
                int attrIndex = attributes.getIndex(i);
                int attrItem = attrset[attrIndex];
                switch (attrItem) {
                    case android.R.attr.text:
                        CharSequence text = attributes.getText(attrIndex);
                        setText(text);
                        break;
                    case android.R.attr.textColor:
                        //涉及到ColorStateList ,暂不做支持动态切换
                        ColorStateList colorStateList = attributes.getColorStateList(attrIndex);
                        if (colorStateList != null) {
                            mTextColorStateList = colorStateList;
                        }
                        break;
                    case android.R.attr.textSize:
                        int dimensionPixelSize = attributes.getDimensionPixelSize(attrIndex, (int) sp2px(12));
                        mTextPaint.setTextSize(dimensionPixelSize);
                        break;
                    case android.R.attr.includeFontPadding:
                        mIncludeFontPadding = attributes.getBoolean(attrIndex, false);
                        break;

                }
            }
            attributes.recycle();
        }

        setTextColor(mTextColorStateList);

    }

    public void setTypeface(Typeface tf, int style) {
        if (style > 0) {
            if (tf == null) {
                tf = Typeface.defaultFromStyle(style);
            } else {
                tf = Typeface.create(tf, style);
            }

            setTypeface(tf);
            // now compute what (if any) algorithmic styling is needed
            int typefaceStyle = tf != null ? tf.getStyle() : 0;
            int styleFlags = style & ~typefaceStyle;
            mTextPaint.setFakeBoldText((styleFlags & Typeface.BOLD) != 0);
            mTextPaint.setTextSkewX((styleFlags & Typeface.ITALIC) != 0 ? -0.25f : 0);
        } else {
            mTextPaint.setFakeBoldText(false);
            mTextPaint.setTextSkewX(0);
            setTypeface(tf);
        }
    }

    public void setTypeface(Typeface tf) {
        if (mTextPaint.getTypeface() != tf) {
            mTextPaint.setTypeface(tf);
            if (mLayout != null) {
                requestLayout();
                invalidate();
            }
        }
    }

    public Typeface getTypeface() {
        if (mTextPaint != null) {
            return mTextPaint.getTypeface();
        }
        return null;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int defaultWidth = MeasureSpec.getSize(widthMeasureSpec);
        if (measureWidthMode != -1 && measureWidthMode != widthMode) {
            mHintLayout = null;
        }
        int widthSize = defaultWidth;

        if (widthMode != MeasureSpec.EXACTLY) {
            if (mHintLayout == null) {
                //在setText时已经计算过了,直接复用mHintLayout
                mLayout = buildTextLayout(this.mText, ANY_WIDTH);
            } else {
                mLayout = mHintLayout;
            }
            int requestWidth = (getPaddingRight() + getPaddingLeft()) + (mLayout != null ? mLayout.getWidth() : 0);

            if(widthMode == MeasureSpec.AT_MOST){
                widthSize = Math.min(requestWidth,defaultWidth);
            }else {
                widthSize = requestWidth;
            }
        } else {
            if (mHintLayout == null) {
                int contentWidth = (widthSize - (getPaddingRight() + getPaddingLeft()));
                mLayout = buildTextLayout(this.mText, contentWidth);
            } else {
                mLayout = mHintLayout;
            }
        }

        int defaultHeight = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = 0;

        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = Math.min(getTextLayoutHeight(mLayout),defaultHeight);
        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
            int desireHeight = getTextLayoutHeight(mLayout);
            heightSize = (getPaddingTop() + getPaddingBottom()) + desireHeight;
        }

        setMeasuredDimension(widthSize, heightSize);
        Log.i(TAG,"widthSize="+widthSize+", heightSize="+heightSize+",paddingTop="+getPaddingTop()+",paddingBottom="+getPaddingBottom());

        measureHeightMode = heightMode;
        measureWidthMode = widthMode;

        mHintLayout = null;
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        measureWidthMode = -1;
        measureHeightMode = -1;
    }

    @Override
    public void setLayoutParams(ViewGroup.LayoutParams params) {
        measureWidthMode = -1;
        measureHeightMode = -1;
        super.setLayoutParams(params);
    }

    private int getTextLayoutHeight(Layout layout) {
        if(layout == null) {
            return 0;
        }
        int desireHeight = 0;
        desireHeight = layout.getHeight();
        if(desireHeight <= 0){
            int minTextLayoutLines = Math.min(layout.getLineCount(), 1);
            desireHeight =  Math.round(mTextPaint.getFontMetricsInt(null)* mLineSpacingMult + mLineSpacingAdd) * minTextLayoutLines;
        }
        return desireHeight;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mContentHeight = (h - getPaddingTop() - getPaddingBottom());
        mContentWidth = (w - getPaddingLeft() - getPaddingRight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float strokeWidth = mTextPaint.getStrokeWidth() * 2;
        if (mContentWidth <= strokeWidth || mContentHeight <= strokeWidth) {
            return;
        }
        int save = canvas.save();

        if (mLayout != null) {
            int verticalHeight = getPaddingTop() + getPaddingBottom() + getTextLayoutHeight(mLayout);
            float offset = (getHeight() - verticalHeight) >> 1;
            if(offset < 0){
                offset = 0;
            }
            canvas.translate(getPaddingLeft(), getPaddingTop() + offset);
            mLayout.draw(canvas);
        }
        canvas.restoreToCount(save);
    }

    public void setText(final CharSequence text) {
        CharSequence targetText = text == null ? "" : text;
        if (mLayout != null && TextUtils.equals(targetText, this.mText)) {
            return;
        }
        this.mText = targetText;
        if (!isAttachedToWindow()) {
            mLayout = null;
            mHintLayout = null;
            return;
        }
        if (measureWidthMode == -1 || measureHeightMode == -1) {
            mLayout = null;
            mHintLayout = null;
            requestLayout();
            invalidate();
            return;
        }
        int width = measureWidthMode == MeasureSpec.EXACTLY ? getMeasuredWidth() : ANY_WIDTH;
        mHintLayout = buildTextLayout(text, width);

        int desireWidth = mHintLayout.getWidth() + getPaddingLeft() + getPaddingRight();
        int desireHeight = getTextLayoutHeight(mHintLayout)+ getPaddingTop() + getPaddingBottom();

        if (desireWidth != getWidth() || measureHeightMode != MeasureSpec.EXACTLY && desireHeight != getHeight()) {
            mLayout = null;
            requestLayout();
        } else {
            mLayout = mHintLayout;
            mHintLayout = null;
        }
        invalidate();
    }

    protected Layout buildTextLayout(CharSequence text, int wantWidth) {
        // fixed: Chinese word is not boring in android 4.4,在Android 4.4长度可能小于measureText测量的
        float measureTextWidth = mTextPaint.measureText(text, 0, text.length());
        BoringLayout.Metrics boring = BoringLayout.isBoring(text, mTextPaint,UNKNOWN_BORING);


        float lineSpaceMult = mLineSpacingMult;
        if (lineSpaceMult < 1F) {
            lineSpaceMult = 1.0f;
        }
        if (boring != null) {
            int outWidth = wantWidth != ANY_WIDTH ? wantWidth : (int) (Math.max(boring.width,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
            return BoringLayout.make(text, mTextPaint,
                    outWidth,
                    Layout.Alignment.ALIGN_NORMAL,
                    0,
                    mLineSpacingAdd,
                    boring,
                    mIncludeFontPadding);
        }
        //fix Android  4.4  mLineSpacingMult 必须大于0
        float desiredWidthForStaticLayout = StaticLayout.getDesiredWidth(text, mTextPaint);
        int desiredWidth = (int) (Math.max(desiredWidthForStaticLayout,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
        int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
        StaticLayout staticLayout = new StaticLayout(text,
                mTextPaint,
                outWidth,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpaceMult,
                mLineSpacingAdd,
                mIncludeFontPadding);
        return staticLayout;
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDisplayMetrics);
    }

    public void setIncludeFontPadding(boolean includePad) {
        this.mIncludeFontPadding = includePad;
        mHintLayout = null;
        mLayout = null;
        requestLayout();
        invalidate();
    }

    public void setTextColor(int color) {
        ColorStateList colorStateList = ColorStateList.valueOf(color);
        setTextColor(colorStateList);
    }

    public void setTextColor(ColorStateList colorStateList) {
        if (colorStateList == null) return;
        final int[] drawableState = getDrawableState();
        int forStateColor = colorStateList.getColorForState(drawableState, 0);
        mTextColor = forStateColor;
        mTextColorStateList = colorStateList;
        mTextPaint.setColor(forStateColor);
        postInvalidate();
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if(mTextColorStateList!=null && mTextColorStateList.isStateful()) {
            setTextColor(mTextColorStateList);
        }
    }


    public int getCurrentTextColor() {
        return mTextColor;
    }

    public void setTextSize(float textSize) {
        mTextPaint.setTextSize(textSize);
    }

    public TextPaint getPaint() {
        return mTextPaint;
    }

    public CharSequence getText() {
        return mText;
    }

    @Override
    public boolean isAttachedToWindow() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
            return super.isAttachedToWindow();
        }
        return getWindowToken() != null;
    }
}

犬牙问题:

这种问题的解决方法网上能搜出很多,但是对中文支持最好的得参考下面文章 《关于TextView中换行后对齐问题

其中实现原理是对TextView重写,但是缺点是对英文支持的不够好,不过关系不大,对英文分词即可快速实现。

其核心逻辑是:对最后一行的以外的其他文本行增加文字间距(word space),从而使得看起来犬牙的文本显的规整,但其本身并非是两边对齐。

    private void drawScaledText(Canvas canvas, int lineStart, String line,
                                float lineWidth) {
        float x = 0;
        if (isFirstLineOfParagraph(lineStart, line)) {
            String blanks = "  ";
            canvas.drawText(blanks, x, mLineY, getPaint());
            float bw = StaticLayout.getDesiredWidth(blanks, getPaint());
            x += bw;

            line = line.substring(3);
        }

        int gapCount = line.length() - 1;
        int i = 0;
        if (line.length() > 2 && line.charAt(0) == 12288
                && line.charAt(1) == 12288) {
            String substring = line.substring(0, 2);
            float cw = StaticLayout.getDesiredWidth(substring, getPaint());
            canvas.drawText(substring, x, mLineY, getPaint());
            x += cw;
            i += 2;
        }

        float d = (mViewWidth - lineWidth) / gapCount;
        for (; i < line.length(); i++) {
            String c = String.valueOf(line.charAt(i));
            float cw = StaticLayout.getDesiredWidth(c, getPaint());
            canvas.drawText(c, x, mLineY, getPaint());
            x += cw + d;
        }
    }

完整代码

public class TextAlignTextView extends TextView {

    private int mLineY;
    private int mViewWidth;
    public static final String TWO_CHINESE_BLANK = "  ";

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
                            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        TextPaint paint = getPaint();
        paint.setColor(getCurrentTextColor());
        paint.drawableState = getDrawableState();
        mViewWidth = getMeasuredWidth();
        String text = getText().toString();
        mLineY = 0;
        mLineY += getTextSize();
        Layout layout = getLayout();

        // layout.getLayout()在4.4.3出现NullPointerException
        if (layout == null) {
            return;
        }

        Paint.FontMetrics fm = paint.getFontMetrics();

        int textHeight = (int) (Math.ceil(fm.descent - fm.ascent));
        textHeight = (int) (textHeight * layout.getSpacingMultiplier() + layout
                .getSpacingAdd());
        //解决了最后一行文字间距过大的问题
        for (int i = 0; i < layout.getLineCount(); i++) {
            int lineStart = layout.getLineStart(i);
            int lineEnd = layout.getLineEnd(i);
            float width = StaticLayout.getDesiredWidth(text, lineStart,
                    lineEnd, getPaint());
            String line = text.substring(lineStart, lineEnd);

            if(i < layout.getLineCount() - 1) {
                if (needScale(line)) {
                    drawScaledText(canvas, lineStart, line, width);
                } else {
                    canvas.drawText(line, 0, mLineY, paint);
                }
            } else {
                canvas.drawText(line, 0, mLineY, paint);
            }
            mLineY += textHeight;
        }
    }

    private void drawScaledText(Canvas canvas, int lineStart, String line,
                                float lineWidth) {
        float x = 0;
        if (isFirstLineOfParagraph(lineStart, line)) {
            String blanks = "  ";
            canvas.drawText(blanks, x, mLineY, getPaint());
            float bw = StaticLayout.getDesiredWidth(blanks, getPaint());
            x += bw;

            line = line.substring(3);
        }

        int gapCount = line.length() - 1;
        int i = 0;
        if (line.length() > 2 && line.charAt(0) == 12288
                && line.charAt(1) == 12288) {
            String substring = line.substring(0, 2);
            float cw = StaticLayout.getDesiredWidth(substring, getPaint());
            canvas.drawText(substring, x, mLineY, getPaint());
            x += cw;
            i += 2;
        }

        float d = (mViewWidth - lineWidth) / gapCount;
        for (; i < line.length(); i++) {
            String c = String.valueOf(line.charAt(i));
            float cw = StaticLayout.getDesiredWidth(c, getPaint());
            canvas.drawText(c, x, mLineY, getPaint());
            x += cw + d;
        }
    }

    private boolean isFirstLineOfParagraph(int lineStart, String line) {
        return line.length() > 3 && line.charAt(0) == ' '
                && line.charAt(1) == ' ';
    }

    private boolean needScale(String line) {
        if (line == null || line.length() == 0) {
            return false;
        } else {
            return line.charAt(line.length() - 1) != '\n';
        }
    }

}

总结

到这里本篇就结束了,TextView作为Android中最复杂的View组件之一,其中有很多方法的调用也是非公开的,另外其中的Editor也是没有公开的,这显然是造成TextView存在性能问题的原因之一。

本篇这里的优化基本都有线上允许,在播放器中,我们用到了BoringTextView和有跑马灯,有效降低了焦点问题和requestLayout频繁的问题,当然文本的展示并不一定非得用BoringLayout和StaticLayout,也有很多方式可以实现此类优化。文本对齐问题,实际上在一些协议页面使用会获得很好的体验,这里我们就不再赘述了。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的Android Studio文本阅读器示例: 1. 首先,在Android Studio中创建一个新项目。 2. 在XML布局文件中添加一个TextView和一个Button,用于显示文本和选择文件。 ``` <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" tools:context=".MainActivity"> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical" android:text="No file selected" android:textSize="18sp" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:text="Select File" /> </RelativeLayout> ``` 3. 在MainActivity.java文件中,添加以下代码来实现文件选择器和文本显示功能: ``` public class MainActivity extends AppCompatActivity { private static final int READ_REQUEST_CODE = 42; private TextView textView; private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.textView); button = findViewById(R.id.button); // 设置按钮的点击监听器 button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 打开文件选择器 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); startActivityForResult(intent, READ_REQUEST_CODE); } }); } @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { Uri uri = null; if (resultData != null) { uri = resultData.getData(); try { // 读取文本并在TextView中显示 InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader( new InputStreamReader(inputStream)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); stringBuilder.append("\n"); } inputStream.close(); textView.setText(stringBuilder.toString()); } catch (IOException e) { e.printStackTrace(); } } } } } ``` 4. 运行应用程序,并选择要阅读的文本文件。 这个示例只是一个简单的文本阅读器,您可以根据需要添加其他功能和优化

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值