溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能

發(fā)布時(shí)間:2021-11-15 13:56:35 來(lái)源:億速云 閱讀:121 作者:iii 欄目:移動(dòng)開(kāi)發(fā)

這篇文章主要介紹“Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能”,在日常操作中,相信很多人在Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

1 概述

玩過(guò)ins的朋友應(yīng)該知道ins里面有一個(gè)編輯文字自動(dòng)排版的功能,應(yīng)用會(huì)根據(jù)用戶輸入的每行文字自動(dòng)進(jìn)行排版,以達(dá)到一個(gè)緊湊美觀的效果。

效果圖如下:

Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能

2 思路探究

因?yàn)榫W(wǎng)上找不到什么相關(guān)的資料,所以就直接通過(guò)玩ins猜測(cè)大概的實(shí)現(xiàn)思路,我整理下自己一開(kāi)始的一些疑問(wèn)。

當(dāng)輸入的文字越來(lái)越多,字體越來(lái)越小時(shí)怎么保證每行最多能夠顯示的文本不變?

正常情況下,當(dāng)你字體越來(lái)越小而輸入框?qū)挾炔蛔儠r(shí),那么你每行可輸入的文字就會(huì)變多,但是你發(fā)現(xiàn)ins無(wú)論字體多大,每行最多能容納的文本是不變。我猜測(cè)可能是輸入框會(huì)隨著字體的變化而改變。

通過(guò)打開(kāi)開(kāi)發(fā)者選項(xiàng)的應(yīng)用布局邊界,可以看到確實(shí)ins的輸入框的寬度是動(dòng)態(tài)變化的。

下面是打開(kāi)應(yīng)用布局邊界后的效果圖:

Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能

當(dāng)然,這里可能會(huì)引入一個(gè)新的問(wèn)題,那就是輸入框的寬度是怎么動(dòng)態(tài)改變的?

好讓它剛好能夠在字體大小變化的過(guò)程中最多可容納的文本數(shù)不變。

這個(gè)問(wèn)題會(huì)在下面說(shuō),這里先不展開(kāi)。

每行字體大小是怎么確定的,又是怎樣聯(lián)動(dòng)變化的?

這個(gè)問(wèn)題一開(kāi)始想了很久,我覺(jué)得如果把這個(gè)問(wèn)題搞明白基本就已經(jīng)成功一半了。大部分人一開(kāi)始可能都會(huì)很容易陷入局部思維,包括我也一樣,一直在糾結(jié)每行字體是怎么變化的,但其實(shí)應(yīng)該要從整體考慮,從整體考慮一切都會(huì)變得很簡(jiǎn)單,代碼實(shí)現(xiàn)上也會(huì)變得更加容易,不需要處理各種特殊情況。

具體思路:

遍歷每行文本,以適應(yīng)最大文本寬度算出每行的字體大小,然后以每行的字體大小算出每行行高度,把每行行高度累加得到文本總高度,然后判斷文本總高度是否大于最大文本高度,如果大于則按比例縮小每行的字體大小,以縮小每行的行高度,得到新的文本總高度,直到文本總高度小于最大文本高度。

上面的這么大段文字總結(jié)起來(lái)其實(shí)就4個(gè)步驟:

  1. 拆行

  2. 按匹配最大寬度計(jì)算每行字體大小

  3. 按匹配最大高度計(jì)算每行字體大小

  4. 重新調(diào)整EditText寬度

3 知識(shí)儲(chǔ)備

在動(dòng)手之前我們需要知道幾個(gè)相關(guān)知識(shí)點(diǎn):

span

span可以使TextView分段顯示不同樣式的文字。在自動(dòng)排版中因?yàn)槊啃形淖肿煮w大小不一樣,所以我們需要為每行文字設(shè)置不同的span。

Layout

Layout是一個(gè)用于各種文本計(jì)算的輔助類,TextView的文字排版布局都是依賴于Layout實(shí)現(xiàn)的。因?yàn)長(zhǎng)ayout是完全跟TextView解耦的,所以我們可以構(gòu)建合適的Layout來(lái)幫助我們計(jì)算字體大小。

下面是Layout的官方定義:

A base class that manages text layout in visual elements on the screen.

For text that will be edited, use a DynamicLayout, which will be updated as the text changes. For text that will not change, use a StaticLayout.

Layout有幾個(gè)子類,其中較常用的是DynamicLayout和StaticLayout,按照官方的說(shuō)法,當(dāng)你的文本是可編輯的則使用的是DynamicLayout,當(dāng)你的文本不可編輯那么就使用StaticLayout。

所以說(shuō)EditText的文本計(jì)算工作應(yīng)該都是交給了DynamicLayout實(shí)現(xiàn)。

4 具體實(shí)現(xiàn)

首先我們需要監(jiān)聽(tīng)文字的輸入變化,當(dāng)文本變化時(shí)去計(jì)算每行的字體大小,最終渲染到屏幕。監(jiān)聽(tīng)文本變化的代碼如下:

  addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                refresh();
            }
            @Override
            public void afterTextChanged(Editable s) {
            }
        });

一.拆行

監(jiān)聽(tīng)到文本變化后需要對(duì)文本進(jìn)行拆行,得到每行的文字。我們可以通過(guò)Layout實(shí)現(xiàn),代碼如下:

		String text = layout.getText().toString();
		int lineCount = layout.getLineCount();
        for (int i = 0; i < lineCount; i++) {
            int start = layout.getLineStart(i);
            int end = layout.getLineEnd(i);
            String rowStr = text.substring(start,end);
        }

但是,但是,但是,這里有一個(gè)點(diǎn)需要特別注意,不能通過(guò)EditText自帶的Layout來(lái)計(jì)算每行文本,不然拿到的每行文本是錯(cuò)誤的。

為什么呢?

舉個(gè)例子:

如下圖所示,當(dāng)你在第二行將要輸入“好”時(shí),因?yàn)槟爿斎?quot;好"后該行文本寬度已經(jīng)大于此時(shí)EditText的寬度了,所以“好”字會(huì)被認(rèn)為是重啟一行,這樣你得到的每行文本就是錯(cuò)的了,因?yàn)椤昂谩睉?yīng)該顯示在第二行才對(duì)。

Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能

這就涉及到我在思路探究中提到的第一個(gè)問(wèn)題,無(wú)論我字體怎么縮小放大,如何保證每行最多可顯示的文本都是一樣的?

那么如何保證呢?

其實(shí)很簡(jiǎn)單,因?yàn)橛绊懙轿淖肿詣?dòng)換行的因素主要就是字體大小和最大文本寬度,那么只要保證這兩個(gè)因素不變,無(wú)論你輸入什么文本,都能準(zhǔn)確一致的拆分出每一行的文字。

因?yàn)镋ditText的每行字體在變,而且寬度也在變,所以通過(guò)EditText自帶的Layout算出的每行文本肯定是錯(cuò)誤的。

所以,思路應(yīng)該是這樣的,你需要構(gòu)建一個(gè)用于計(jì)算的Layout,這個(gè)Layout的字體大小和寬度必須是固定不變的,這樣它就能夠保證每行最多可容納的文本始終是一樣的,這樣我就能夠準(zhǔn)確拆分出每行文本。

前面已經(jīng)說(shuō)過(guò)EditText的計(jì)算工作都是交給DynamicLayout,所以我們需要?jiǎng)?chuàng)建的是DynamicLayout。代碼如下

protected Layout buildCalculateLayout(CharSequence text,TextView host){
        TextPaint paint = new TextPaint(host.getPaint());
        paint.setTextSize(mDefFontSize);
        return new DynamicLayout(text,paint, mDefMaxTextWidth,host.getLayout().getAlignment(),host.getLayout().getSpacingMultiplier(),host.getLayout().getSpacingAdd(),host.getIncludeFontPadding());
    }

需要注意的是,這里除了字體大小和寬度,其他的參數(shù)都需要跟EditText的參數(shù)一樣。

其中mDefFontSize是一開(kāi)始定義的一個(gè)默認(rèn)字體大小,mDefMaxTextWidth是EditText在沒(méi)動(dòng)態(tài)調(diào)整寬度前的寬度(需要減去padding)。

這樣子每次都是通過(guò)自構(gòu)建的Layout去計(jì)算每行的文本,就不需要考慮EditText的字體和寬度的動(dòng)態(tài)變化。

二.按匹配最大寬度計(jì)算每行字體大小

搞定了第一步拆行后,其實(shí)已經(jīng)離成功不遠(yuǎn)了,接下來(lái)就是如何確定每行字體大小了。

確定字體大小說(shuō)簡(jiǎn)單簡(jiǎn)單,說(shuō)難也難,關(guān)鍵是看你有沒(méi)有想到那個(gè)點(diǎn)。比如一開(kāi)始我一直糾結(jié)于每行文字是怎么隨著輸入文字個(gè)數(shù)和行數(shù)變化動(dòng)態(tài)改變的,陷入了局部細(xì)節(jié),搞得自己暈頭轉(zhuǎn)向,如果按照這個(gè)方向思考我感覺(jué)估計(jì)是怎么做都搞不定的。

后來(lái),想了兩天后還是沒(méi)搞明白,我就試著換個(gè)思維方式,從整體來(lái)考慮,接下來(lái)就有種恍然大悟的感覺(jué),原來(lái)其實(shí)沒(méi)那么難。

首先,有一個(gè)規(guī)律是很顯然的:

每行文字越多,它的字體就越小,文字越少,字體就越大。

那么我就想一開(kāi)始時(shí)你把每行文字的寬度放大到最大文本寬度,算出匹配這個(gè)寬度的字體應(yīng)該多大,這樣文字越少的行,字體就越大,文字越多的行,字體就越小,這個(gè)不就是符合那個(gè)規(guī)律嗎。

計(jì)算文本寬度的代碼如下:

  float width = paint.measureText(text);

因?yàn)樾枰ㄟ^(guò)不斷更改字體大小,去算出匹配最大寬度的字體,所以為了減少計(jì)算量,一開(kāi)始可以做一個(gè)初始字體大小的換算。

當(dāng)字體大小是mDefFontSize時(shí)對(duì)應(yīng)的文本寬度是mDefMaxTextWidth,那么當(dāng)文本寬度是x時(shí),對(duì)應(yīng)的字體大小是y,因?yàn)樽煮w大小和寬度成反比(寬度越小,字體越大),所以y的計(jì)算公式就是:

y = mDefMaxTextWidth \ x * mDefFontSizex = paint.measureText(text);

這樣我們就可以得到一個(gè)比較接近目標(biāo)值的字體大小,這時(shí)候再去判斷此時(shí)文本寬度是否匹配最大文本寬度,不等于的話再去改變字體大小,直到文本寬度匹配最大文本寬度為止。

代碼如下:

  public float calculateMatchWidthSize(Paint paint,String text,int maxWidth){
          float textSize = paint.getTextSize();
          float width = paint.measureText(text);
        
          if(maxWidth >= width && maxWidth - width <= text.length()){
              return textSize;
          }
          if(width > maxWidth){
              textSize = getNarrowFitTextSize(paint,text,maxWidth,1);
          }else{
              textSize = getZoomFitTextSize(paint,text,maxWidth,1);
          }
  
          return textSize;
      }
      private float getNarrowFitTextSize(Paint paint,String text,int maxWidth,float rate){
          float textSize = paint.getTextSize();
          textSize -= 1 * rate;
          paint.setTextSize(textSize);
          float width = paint.measureText(text);
          if(maxWidth >= width && maxWidth - width <= text.length()){
              return textSize;
          }
          //結(jié)束條件
          if(width < maxWidth){
              return getZoomFitTextSize(paint,text,maxWidth,rate);
          }else{
              return getNarrowFitTextSize(paint,text,maxWidth,rate);
          }
      }
      private float getZoomFitTextSize(Paint paint,String text,int maxWidth,float rate){
          float textSize = paint.getTextSize();
          textSize += 1 * rate;
          paint.setTextSize(textSize);
          float width = paint.measureText(text);
          if(maxWidth >= width && maxWidth - width <= text.length()){
              return textSize;
          }
          //結(jié)束條件
          if(width < maxWidth){
              return getZoomFitTextSize(paint,text,maxWidth,rate);
          }else{
              return getNarrowFitTextSize(paint,text,maxWidth,rate);
          }
      }

三.按匹配最大高度計(jì)算每行字體大小

按照匹配最大寬度計(jì)算出來(lái)的字體會(huì)很大,導(dǎo)致文本高度很高,這時(shí)候就需要再動(dòng)態(tài)調(diào)整每行字體大小,直到文本高度匹配最大高度為止。

動(dòng)態(tài)調(diào)整字體大小時(shí),每行文字的字體大小需要按比例調(diào)整,比如每行字體都調(diào)整為原來(lái)的0.9倍大小。

計(jì)算每行文本高度的代碼:

 int height = paint.getFontMetricsInt(null);

為了讓每行文本高度的累加值等于文本實(shí)際總高度,需要設(shè)置EditText的邊距為0并且去掉文字上下的空白部分。代碼如下:

        //去掉文本上下空白區(qū)域
        mHost.setIncludeFontPadding(false);
        //不設(shè)置行間距
        mHost.setLineSpacing(0,1);

為了提高計(jì)算速度,采用二分法來(lái)動(dòng)態(tài)調(diào)整字體大小,代碼如下:

    /**
     * 二分法查找合適的字體大小,字體大小按比例調(diào)整
     * @return
     */
    private void calculateMatchHeightSizeByRate(float lowRate,float highRate,int minHeight,int maxHeight){
        if(highRate - lowRate <= RATE_SCALE_ERROR_VALUE){
            return;
        }
        float middleRate= (lowRate+highRate)/2;
        scaleFontSizeByRate(middleRate);
        int height = getTextHeight();
        if(height > maxHeight){
            //縮小字體后文字高度大于最大值,需要繼續(xù)縮小字體
            highRate = middleRate;
            calculateMatchHeightSizeByRate(lowRate,highRate,minHeight,maxHeight);
        } else if(height < minHeight){
            //縮小字體后文字高度小于最小值,需要放大字體
            lowRate = middleRate;
            calculateMatchHeightSizeByRate(lowRate,highRate,minHeight,maxHeight);
        }
    }
    
	private int getTextHeight(){
        int totalHeight = 0;
        for(CustomSpanData customSpanData: mCustomTextSpanDataList){
            int lineHeight = getSingleLineHeight(customSpanData.getTextSize());
            totalHeight += lineHeight;
        }
        return totalHeight;
    }
    private int getSingleLineHeight(float fontSize){
        Paint paint = new Paint(mHost.getPaint());
        paint.setTextSize(fontSize);
        return paint.getFontMetricsInt(null);
    }
    private void scaleFontSizeByRate(float rate){
        for(int i=0;i<mOriFontSizePxList.size();i++){
            float fontSize = mOriFontSizePxList.get(i) * rate;
            mCustomTextSpanDataList.get(i).setTextSize(UNIT_PX,fontSize);
        }
    }

四.重新調(diào)整EditText的寬度

前面有提到一個(gè)問(wèn)題那就是輸入框的寬度是怎么動(dòng)態(tài)改變的?

閱讀了前三個(gè)步驟后是不是已經(jīng)有了答案,首先通過(guò)自構(gòu)建的Layout確定每行需要顯示什么文本,然后動(dòng)態(tài)調(diào)整每行字體大小以適應(yīng)輸入框的寬高,這時(shí)候可能每行的字體已經(jīng)很小了,如果不調(diào)整EditText的寬度必然會(huì)導(dǎo)致不同行的文字頂?shù)酵恍酗@示。

到此,關(guān)于“Android中怎么仿instagram實(shí)現(xiàn)文字自動(dòng)排版功能”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI