自定義實(shí)現(xiàn)垂直滾動(dòng)的TextView

需求

  • 當(dāng)TextView限制最大行數(shù)的時(shí)候,文本內(nèi)容超過(guò)最大行數(shù)可自動(dòng)實(shí)現(xiàn)文本內(nèi)容向上滾動(dòng)
  • 隨著TextView的文本內(nèi)容的改變永品,可自動(dòng)計(jì)算換行并實(shí)時(shí)的向上滾動(dòng)
  • 文字向上滾動(dòng)后可向下滾動(dòng)回到正確的水平位置

自定義方法

  • 自定義一個(gè)View,繼承自View滔以,定重寫(xiě)里面的onDraw方法
  • 文字的滾動(dòng)是用Canvas對(duì)象的drawText方法去實(shí)現(xiàn)的
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
        native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
                paint.getNativeInstance(), paint.mNativeTypeface);
    }

通過(guò)控制y參數(shù)可實(shí)現(xiàn)文字不同的垂直距離四濒,這里的x奴愉,y并不代表默認(rèn)橫向坐標(biāo)為0蔬螟,縱向坐標(biāo)為0的坐標(biāo)此迅,具體詳解我覺(jué)得這篇博客解釋的比較清楚,我們主要關(guān)注的是參數(shù)y的控制旧巾,y其實(shí)就是text的baseline耸序,這里還需要解釋text的杰哥基準(zhǔn)線:


image.png

ascent:該距離是從所繪字符的baseline之上至該字符所繪制的最高點(diǎn)。這個(gè)距離是系統(tǒng)推薦鲁猩。
descent:該距離是從所繪字符的baseline之下至該字符所繪制的最低點(diǎn)坎怪。這個(gè)距離是系統(tǒng)推薦的。
top:該距離是從所繪字符的baseline之上至可繪制區(qū)域的最高點(diǎn)廓握。
bottom:該距離是從所繪字符的baseline之下至可繪制區(qū)域的最低點(diǎn)搅窿。
leading:為文本的線之間添加額外的空間嘁酿,這是官方文檔直譯,debug時(shí)發(fā)現(xiàn)一般都為0.0男应,該值也是系統(tǒng)推薦的闹司。
特別注意: ascent和top都是負(fù)值,而descent和bottom:都是正值殉了。

由于text的baseline比較難計(jì)算开仰,所以我們大約取y = bottom - top的值拟枚,這么坐位baseline的值不是很精確薪铜,但是用在此自定義控件上文字的大小間距恰好合適,在其他場(chǎng)景可能還是需要精確的去計(jì)算baseline的值

動(dòng)畫(huà)效果實(shí)現(xiàn)

  • 通過(guò)循環(huán)觸發(fā)執(zhí)行onDraw方法來(lái)實(shí)現(xiàn)文字的上下滑動(dòng)恩溅,當(dāng)然在每次觸發(fā)onDraw之前首先要計(jì)算文字的baseline的值
  • 通過(guò)設(shè)置Paint的alpha的值來(lái)控制透明度隔箍,alpha的值的變化要和文字baseline的變化保持同步,因?yàn)槲淖稚舷禄瑒?dòng)和文字的透明度要做成一個(gè)統(tǒng)一的動(dòng)畫(huà)效果
  • 文字的換行脚乡,首先用measureText來(lái)測(cè)量每一個(gè)字的寬度蜒滩,然后持續(xù)累加,直到累加寬度超過(guò)一行的最大限制長(zhǎng)度之后就追加一個(gè)換行符號(hào)奶稠,當(dāng)然我們是用一個(gè)List作為容器來(lái)容納文本內(nèi)容俯艰,一行文本就是list的一個(gè)item所以不用追加換行符號(hào),直接添加list的item
  • 在實(shí)現(xiàn)文字上下滑動(dòng)以及透明度變化的時(shí)候遇到一個(gè)問(wèn)題锌订,就是上一次的滑動(dòng)剛剛滑到一半竹握,文字的baseline和透明度已經(jīng)改變到一半了,這時(shí)候又有新的文本追加進(jìn)來(lái)辆飘,那么新的文本會(huì)導(dǎo)致一次新的滑動(dòng)動(dòng)畫(huà)和文字透明度改變動(dòng)畫(huà)會(huì)和之前的重疊啦辐,造成上一次的滑動(dòng)效果被中斷,文字重新從初始值開(kāi)始滑動(dòng)蜈项,所以會(huì)看到文字滑動(dòng)到一半又回到初始位置重新開(kāi)始滑動(dòng)芹关,那么如果一直不斷的有文字追加進(jìn)來(lái)會(huì)導(dǎo)致文字滑動(dòng)反復(fù)的中斷開(kāi)始,這種效果當(dāng)然不是我們想要的紧卒,我們想要的就是文字滑動(dòng)到一半了侥衬,那么已經(jīng)滑動(dòng)的文字保持當(dāng)前的狀態(tài),新追加進(jìn)來(lái)的問(wèn)題從初始值開(kāi)始滑動(dòng)跑芳,滑動(dòng)到一半的文字從之前的狀態(tài)繼續(xù)滑動(dòng)浇冰,所以就需要記錄文字的滑動(dòng)間距,透明度等信息并保存下來(lái)

代碼實(shí)現(xiàn)

public class AutoScrollTextView extends View {

    public interface OnTextChangedListener {
        void onTextChanged(String text);
    }

    private class TextStyle {
        int alpha;
        float y;
        String text;

        TextStyle(String text, int alpha, float y) {
            this.text = text;
            this.alpha = alpha;
            this.y = y;
        }
    }

    public static final int SCROLL_UP = 0, SCROLL_DOWN = 1;

    private List<TextStyle> textRows = new ArrayList<>();

    private OnTextChangedListener onTextChangedListener;

    private Paint textPaint;

    /**
     * 標(biāo)題內(nèi)容
     */
    private String title;

    /**
     * 是否是標(biāo)題模式
     */
    private boolean setTitle;

    /**
     * 當(dāng)前的文本內(nèi)容是否正在滾動(dòng)
     */
    private boolean scrolling;

    /**
     * 文字滾動(dòng)方向聋亡,支持上下滾動(dòng)
     */
    private int scrollDirect;

    /**
     * 每行的最大寬度
     */
    private float lineMaxWidth;

    /**
     * 最大行數(shù)
     */
    private int maxLineCount;

    /**
     * 每行的高度肘习,此值是根據(jù)文字的大小自動(dòng)去測(cè)量出來(lái)的
     */
    private float lineHeight;

    public AutoScrollTextView(Context context) {
        super(context);
        init();
    }

    public AutoScrollTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    public AutoScrollTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        textPaint = createTextPaint(255);
        lineMaxWidth = textPaint.measureText("一二三四五六七八九十"); // 默認(rèn)一行最大長(zhǎng)度為10個(gè)漢字的長(zhǎng)度
        maxLineCount = 4;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float x;
        float y = fontMetrics.bottom - fontMetrics.top;
        lineHeight = y;
        if (setTitle) {
            x = getWidth() / 2 - textPaint.measureText(title) / 2;
            canvas.drawText(title, x, y, textPaint);
        } else {
            synchronized (this) {
                if (textRows.isEmpty()) {
                    return;
                }
                scrolling = true;
                x = getWidth() / 2 - textPaint.measureText(textRows.get(0).text) / 2;
                if (textRows.size() <= 2) {
                    for (int index = 0;index < 2 && index < textRows.size();index++) {
                        TextStyle textStyle = textRows.get(index);
                        textPaint.setAlpha(textStyle.alpha);
                        canvas.drawText(textStyle.text, x, textStyle.y, textPaint);
                    }
                } else {
                    boolean draw = false;
                    for (int row = 0;row < textRows.size();row++) {
                        TextStyle textStyle = textRows.get(row);
                        textPaint.setAlpha(textStyle.alpha);
                        canvas.drawText(textStyle.text, x, textStyle.y, textPaint);
                        if (textStyle.alpha < 255) {
                            textStyle.alpha += 51;
                            draw = true;
                        }
                        if (textRows.size() > 2) {
                            if (scrollDirect == SCROLL_UP) {
                                // 此處的9.0f的值是由255/51得來(lái)的,要保證文字透明度的變化速度和文字滾動(dòng)的速度要保持一致
                                // 否則可能造成透明度已經(jīng)變化完了坡倔,文字還在滾動(dòng)或者透明度還沒(méi)變化完成漂佩,但是文字已經(jīng)不滾動(dòng)了
                                textStyle.y = textStyle.y - (lineHeight / 9.0f);
                            } else {
                                if (textStyle.y < lineHeight + lineHeight * row) {
                                    textStyle.y = textStyle.y + (lineHeight / 9.0f);
                                    draw = true;
                                }
                            }
                        }
                    }
                    if (draw) {
                        postInvalidateDelayed(50);
                    } else {
                        scrolling = false;
                    }
                }
            }
        }
    }

    private Paint createTextPaint(int a) {
        Paint textPaint = new Paint();
        textPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, getContext().getResources().getDisplayMetrics()));
        textPaint.setColor(getContext().getColor(R.color.color_999999));
        textPaint.setAlpha(a);
        return textPaint;
    }

    public void resetText() {
        synchronized (this) {
            textRows.clear();
        }
    }

    public void formatText() {
        scrollDirect = SCROLL_DOWN;
        StringBuffer stringBuffer = new StringBuffer("\n");
        synchronized (this) {
            for (int i = 0;i < textRows.size();i++) {
                TextStyle textStyle = textRows.get(i);
                if (textStyle != null) {
                    textStyle.alpha = 255;
//                    textStyle.y = 45 + 45 * i;
                    stringBuffer.append(textStyle.text + "\n");
                }
            }
        }
        postInvalidateDelayed(100);
        LogUtil.i("formatText:" + stringBuffer.toString());
    }

    public void appendText(String text) {
        setTitle = false;
        scrollDirect = SCROLL_UP;
        synchronized (this) {
            if (textRows.size() > maxLineCount) {
                return;
            }
            if (text.length() <= 10) {
                if (textRows.isEmpty()) {
                    textRows.add(new TextStyle(text, 255, lineHeight + lineHeight * textRows.size()));
                } else {
                    TextStyle pre = textRows.get(textRows.size() - 1);
                    textRows.set(textRows.size() - 1, new TextStyle(text, pre.alpha, pre.y));
                }
            } else {
                List<String> list = new ArrayList<>();
                StringBuffer stringBuffer = new StringBuffer();
                float curWidth = 0;
                for (int index = 0;index < text.length();index++) {
                    char c = text.charAt(index);
                    curWidth += textPaint.measureText(String.valueOf(c));
                    if (curWidth <= lineMaxWidth) {
                        stringBuffer.append(c);
                    } else {
                        if (list.size() < maxLineCount) {
                            list.add(stringBuffer.toString());
                            curWidth = 0;
                            index--;
                            stringBuffer.delete(0, stringBuffer.length());
                        } else {
                            break;
                        }
                    }
                }
                if (!TextUtils.isEmpty(stringBuffer.toString()) && list.size() < maxLineCount) {
                    list.add(stringBuffer.toString());
                }
                if (textRows.isEmpty()) {
                    for (int i = 0;i < list.size();i++) {
                        if (i < 2) {
                            textRows.add(new TextStyle(list.get(i), 255, lineHeight + lineHeight * i));
                        } else {
                            textRows.add(new TextStyle(list.get(i), 0, lineHeight + lineHeight * i));
                        }
                    }
                } else {
                    for (int i = 0;i < list.size();i++) {
                        if (textRows.size() > i) {
                            TextStyle pre = textRows.get(i);
                            textRows.set(i, new TextStyle(list.get(i), pre.alpha, pre.y));
                        } else {
                            TextStyle pre = textRows.get(textRows.size() - 1);
                            if (i < 2) {
                                textRows.add(new TextStyle(list.get(i), 255, pre.y + lineHeight));
                            } else {
                                textRows.add(new TextStyle(list.get(i), 0, pre.y + lineHeight));
                            }
                        }
                    }
                }
            }
            if (!scrolling) {
                invalidate();
            }
        }
        textChanged();
    }

    public void setTextColor(int corlor) {
        textPaint.setColor(corlor);
        invalidate();
    }

    public void setTitle(int resId) {
        this.title = getContext().getString(resId);
        setTitle = true;
        invalidate();
    }

    public void setOnTextChangedListener(OnTextChangedListener onTextChangedListener) {
        this.onTextChangedListener = onTextChangedListener;
    }

    private void textChanged() {
        if (onTextChangedListener != null) {
            onTextChangedListener.onTextChanged(getText());
        }
    }

    public String getText() {
        StringBuffer allText = new StringBuffer();
        for (TextStyle textStyle : textRows) {
            allText.append(textStyle.text);
        }
        return allText.toString();
    }

    public int getScrollDirect() {
        return scrollDirect;
    }

    public void setScrollDirect(int scrollDirect) {
        this.scrollDirect = scrollDirect;
    }

    public float getLineMaxWidth() {
        return lineMaxWidth;
    }

    public void setLineMaxWidth(float lineMaxWidth) {
        this.lineMaxWidth = lineMaxWidth;
    }

    public int getMaxLineCount() {
        return maxLineCount;
    }

    public void setMaxLineCount(int maxLineCount) {
        this.maxLineCount = maxLineCount;
    }

    public boolean isScrolling() {
        return scrolling;
    }
}

代碼還可以重構(gòu)的更加簡(jiǎn)潔脖含,但是這邊主要是為了做demo演示,所以就滿看下實(shí)現(xiàn)的原理就好了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末投蝉,一起剝皮案震驚了整個(gè)濱河市裹唆,隨后出現(xiàn)的幾起案子贸街,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件地熄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡啃炸,警方通過(guò)查閱死者的電腦和手機(jī)畦娄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)熟尉,“玉大人归露,你說(shuō)我怎么就攤上這事〗锒” “怎么了剧包?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)往果。 經(jīng)常有香客問(wèn)我疆液,道長(zhǎng),這世上最難降的妖魔是什么陕贮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任堕油,我火速辦了婚禮,結(jié)果婚禮上飘蚯,老公的妹妹穿的比我還像新娘馍迄。我一直安慰自己,他們只是感情好局骤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布攀圈。 她就那樣靜靜地躺著,像睡著了一般峦甩。 火紅的嫁衣襯著肌膚如雪赘来。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天凯傲,我揣著相機(jī)與錄音犬辰,去河邊找鬼。 笑死冰单,一個(gè)胖子當(dāng)著我的面吹牛幌缝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诫欠,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼涵卵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼浴栽!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起轿偎,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤典鸡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后坏晦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體萝玷,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年昆婿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了球碉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挖诸,死狀恐怖汁尺,靈堂內(nèi)的尸體忽然破棺而出法精,到底是詐尸還是另有隱情多律,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布搂蜓,位于F島的核電站狼荞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏帮碰。R本人自食惡果不足惜相味,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望殉挽。 院中可真熱鬧丰涉,春花似錦、人聲如沸斯碌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)傻唾。三九已至投慈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冠骄,已是汗流浹背伪煤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凛辣,地道東北人抱既。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像扁誓,于是被迫代替她去往敵國(guó)和親防泵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阳堕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_X自主閱讀 15,979評(píng)論 3 119
  • 【Android 自定義View之繪圖】 基礎(chǔ)圖形的繪制 一择克、Paint與Canvas 繪圖需要兩個(gè)工具恬总,筆和紙。...
    Rtia閱讀 11,667評(píng)論 5 34
  • 讀完《瘦孕》邱錦伶著的這本書(shū),一本有女明星推薦的書(shū)雖然對(duì)明星不感冒骡湖,但書(shū)有實(shí)在的收獲內(nèi)容贱纠。整理出幾點(diǎn)私下認(rèn)為比較重...
    雨林媽媽0123閱讀 178評(píng)論 0 0
  • 多吉卓瑪_金剛寶座女神 三個(gè)月的實(shí)踐目標(biāo):2018年6月2日至9月2日前公司純收入100萬(wàn)元 親愛(ài)的小伙伴們,讓我...
    多吉卓瑪_金剛寶座女神閱讀 1,116評(píng)論 3 1