<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android 仿朋友圈全文、收起功能,支持話題、網(wǎng)址...

          共 13921字,需瀏覽 28分鐘

           ·

          2023-05-09 05:30

              
                ?安卓進階漲薪訓練營
                ,讓一部分人先進大廠
                


          大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進入心儀的公司。


          詳情見文章:沒錯!皇叔開了個訓練營


          作者:newki
          https://juejin.cn/post/7154271756214075428

          前言

          之前的文章我們都講到了WX盆友圈動態(tài)列表的效果,九宮格控件的實現(xiàn) 【傳送門】 。并且講到了發(fā)布動態(tài)中話題的處理 【傳送門】 。那么在動態(tài)列表中我們?nèi)绾物@示我們發(fā)布的話題數(shù)據(jù)和一些圈子數(shù)據(jù)呢?

          https://juejin.cn/post/7153192823880155143

          https://juejin.cn/post/7153932700066250760


          大致實現(xiàn)效果如下:(本地測試環(huán)境,無其他含義)

          ff244afe93050212c262f20a95acce4f.webp

          1.TextView的特殊文本處理

          我們在把服務器返回的文本設(shè)置給自定義折疊的TextView之前,我們先對文本進行Span的預處理。


                ?/**
          ?????*?暴露方法-替換原文本中的話題數(shù)據(jù),變色處理
          ?????*
          ?????*?@param?topics??服務器返回的話題數(shù)據(jù)
          ?????*?@param?content?服務器返回的原始文本數(shù)據(jù)
          ?????*/

          ????public?CharSequence?replaceTopicSpan(List<RemoteTopicBean>?topics,?String?content,?OnTopicClickListener?listener)?{

          ????????if?(!CheckUtil.isEmpty(topics)?&&?!CheckUtil.isEmpty(content))?{

          ????????????CharSequence?topicCharSequece?=?content;

          ????????????int?startPosition?=?0;
          ????????????int?endPosition?=?0;
          ????????????for?(RemoteTopicBean?bean?:?topics)?{
          ????????????????startPosition?=?content.indexOf(bean.topic_name,?startPosition);
          ????????????????endPosition?=?startPosition?+?bean.topic_name.length();
          ????????????????if?(startPosition?==?-1)
          ????????????????????break;

          ????????????????topicCharSequece?=?SpanUtils.getInstance()
          ????????????????????????.toClickSpan(topicCharSequece,?startPosition,?endPosition,?CommUtils.getColor(R.color.app_blue),?false,?charSequence?->?{
          ????????????????????????????//話題的點擊(路由直接跳轉(zhuǎn)搜索結(jié)果展示)
          ????????????????????????????listener.onTopicClick(charSequence.toString());

          ????????????????????????});

          ????????????????startPosition?=?endPosition;
          ????????????}

          ????????????return?topicCharSequece;
          ????????}

          ????????return?"";
          ????}

          其實就是對多個話題進行遍歷,找到start和end,然后使用Span的工具類,把普通的文本轉(zhuǎn)為可點擊和變色的Span。并回調(diào)出去外界使用。關(guān)鍵是要返回處理之后的文本 CharSequece 返回外部去設(shè)置。


          具體富文本的轉(zhuǎn)換方法如下:
              
                  /**
          ?*?可點擊-帶下劃線
          ?*/

          public?CharSequence?toClickSpan(CharSequence?charSequence,?int?start,?int?end,?int?color,?boolean?needUnderLine,?OnSpanClickListener?listener)?{

          ????SpannableString?spannableString?=?new?SpannableString(charSequence);

          ????ClickableSpan?clickableSpan?=?new?ClickableSpan()?{
          ????????@Override
          ????????public?void?onClick(@NonNull?View?widget)?{
          ????????????if?(listener?!=?null)?{
          ????????????????//防止重復點擊
          ????????????????if?(System.currentTimeMillis()?-?mLastClickTime?>=?TIME_INTERVAL)?{
          ????????????????????//to?do
          ????????????????????listener.onClick(charSequence.subSequence(start,?end));

          ????????????????????mLastClickTime?=?System.currentTimeMillis();
          ????????????????}

          ????????????}
          ????????}

          ????????@Override
          ????????public?void?updateDrawState(@NonNull?TextPaint?ds)?{
          ????????????ds.setColor(color);
          ????????????ds.setUnderlineText(needUnderLine);
          ????????}
          ????};

          ????spannableString.setSpan(
          ????????????clickableSpan,
          ????????????start,
          ????????????end,
          ????????????Spannable.SPAN_INCLUSIVE_EXCLUSIVE);

          ????return?spannableString;
          }


          使用的時候:
              
                  //展開文本設(shè)置
          ExpandTextView?tvContent?=?helper.getView(R.id.tv_feed_news_content);
          String?content?=?item.contentDesc;

          CharSequence?topicCharSequece??=??tvContent.replaceTopicSpan(item.topics,?content,?new?ExpandTextView.OnTopicClickListener()?{
          ????@Override
          ????public?void?onTopicClick(String?topic)?{
          ????????YYRouterService.newsFeedComponentService.startSearchResultActivity(mActivity,?topic,?true);
          ????}
          });

          tvContent.setVisibility(View.VISIBLE);
          tvContent.initWidth(mTvWidth);
          tvContent.setMaxLines(3);
          tvContent.setTypeface(TypefaceUtil.getSFLight(mContext));
          tvContent.setCloseText(topicCharSequece);


          如果自己想顯示的控件文本需要顯示一些自定義字體,那么我們需要在設(shè)置文本之前就設(shè)置字體。

          setCloseText 方法就是具體的實現(xiàn)展開收起入口方法,我們看看它是怎么實現(xiàn)的。

          2.TextView的展開收起功能

          關(guān)于TextView的展開收起,都離不開 StaticLayout 這個神器。

          我們主要需要用到它的兩個方法 :
          • 通過 StaticLayout getLineCount() 方法知道文本是否會超出我們設(shè)置的maxLines,
          • 通過 getLineEnd(int line) 方法可以找到最后一行的最后一個字符在文本中的位置。
          由于我們的需求是[展開]與[收起]的標簽是緊接著文章后面而不是換行展示,所以我們需要循環(huán)遍歷才能找到最佳的位置。 setCloseText 的方法如下:
                private?String?TEXT_EXPAND?=?"??[More]";
          private?String?TEXT_CLOSE?=?"??[Show?Less]";

          /**
          ?*?暴露的方法-默認設(shè)置文本方法(如果需要折疊就會默認折疊)
          ?*?如果有特殊的Span如話題之類的,需要處理完畢之后再調(diào)用此方法。
          ?*/

          public?void?setCloseText(CharSequence?text)?{

          ????if?(SPAN_CLOSE?==?null)?{
          ????????initCloseEnd();
          ????}
          ????boolean?appendShowAll?=?false;?//?需要展開收起功能,先使用flag攔截,等測量完畢之后再setText顯示真正的文本
          ????originText?=?text;

          ????int?maxLines?=?getMaxLines();

          ????CharSequence?workingText?=?originText;
          ????if?(maxLines?>=?0)?{

          ????????//創(chuàng)建出一個StaticLayout主要是為了計算行數(shù)
          ????????Layout?layout?=?createStaticLayout(workingText);
          ????????//計算全部展開的文本高度
          ????????mOpenHeight?=?layout.getHeight()?+?getPaddingTop()?+?getPaddingBottom();
          ????????if?(layout.getLineCount()?>?maxLines)?{
          ????????????//獲取一行顯示字符個數(shù),然后截取字符串數(shù),?收起狀態(tài)原始文本截取展示的部分
          ????????????workingText?=?originText.subSequence(0,?layout.getLineEnd(maxLines?-?1));
          ????????????//再對加上[收起]標簽的文本進行測量
          ????????????String?showText?=?originText.subSequence(0,?layout.getLineEnd(maxLines?-?1))?+?"..."?+?SPAN_CLOSE;
          ????????????Layout?layout2?=?createStaticLayout(showText);

          ????????????//?對workingText進行-1截取,直到展示行數(shù)==最大行數(shù),并且添加?SPAN_CLOSE?后剛好占滿最后一行
          ????????????while?(layout2.getLineCount()?>?maxLines)?{
          ????????????????int?lastSpace?=?workingText.length()?-?1;
          ????????????????if?(lastSpace?==?-1)?{
          ????????????????????break;
          ????????????????}
          ????????????????workingText?=?workingText.subSequence(0,?lastSpace);
          ????????????????layout2?=?createStaticLayout(workingText?+?"..."?+?SPAN_CLOSE);
          ????????????}

          ????????????//計算收起的文本高度
          ????????????mCLoseHeight?=?layout2.getHeight()?+?getPaddingTop()?+?getPaddingBottom();
          ????????????appendShowAll?=?true;

          ????????}
          ????}

          ????setText(workingText);

          ????if?(appendShowAll)?{
          ????????//?必須使用append,不能在上面使用+連接,否則會失效
          ????????append("...");
          ????????append(SPAN_CLOSE);
          ????}

          ????setMovementMethod(LinkMovementMethod.getInstance());

          ????replaceUrlSpan();
          }

          /**
          ?*?收起的文案(顏色處理)初始化
          ?*/

          private?void?initCloseEnd()?{
          ????//設(shè)置展開的文本
          ????SPAN_CLOSE?=?new?SpannableString(TEXT_EXPAND);

          ????ButtonSpan?span?=?new?ButtonSpan(getContext(),?new?View.OnClickListener()?{
          ????????@Override
          ????????public?void?onClick(View?v)?{
          ????????????ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
          ????????????setExpandText(originText);
          ????????????if?(mCallback?!=?null)?mCallback.isExpand(1);
          ????????}
          ????},?R.color.color_expand_span);

          ????SPAN_CLOSE.setSpan(span,?0,?TEXT_EXPAND.length(),?Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
          ????SPAN_CLOSE.setSpan(new?MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())),?0,?TEXT_EXPAND.length(),?Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
          }


          其實只需要這兩個方法就可以展示一個折疊起來的文本了。那么如何切換展開與收起的狀態(tài)呢?

          3.多種方式的實現(xiàn)展開

          第一種方法是直接修改 setMaxLine 的方式,設(shè)置最大允許展示行的方式。

              
                  /**
          ?*?展開的文案(顏色處理)初始化
          ?*/

          private?void?initExpandEnd()?{
          ????//設(shè)置關(guān)閉的文本
          ????SPAN_EXPAND?=?new?SpannableString(TEXT_CLOSE);
          ????ButtonSpan?span?=?new?ButtonSpan(getContext(),?new?View.OnClickListener()?{
          ????????@Override
          ????????public?void?onClick(View?v)?
          {
          ????????????ExpandTextView.super.setMaxLines(mMaxLines);
          ????????????setCloseText(originText);
          ????????????if?(mCallback?!=?null)?mCallback.isExpand(0);
          ????????}
          ????},?R.color.color_expand_span);

          ????SPAN_EXPAND.setSpan(span,?0,?TEXT_CLOSE.length(),?Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
          ????SPAN_EXPAND.setSpan(new?MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())),?0,?TEXT_CLOSE.length(),?Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
          }

          ?/**
          ?*?設(shè)置展開的文本展示-后面加上[收起]的文本標簽
          ?*/

          private?void?setExpandText(CharSequence?text)?{
          ????if?(SPAN_EXPAND?==?null)?{
          ????????initExpandEnd();
          ????}
          ????//創(chuàng)建出一個StaticLayout主要是為了計算行數(shù)
          ????Layout?layout1?=?createStaticLayout(text);
          ????Layout?layout2?=?createStaticLayout(text?+?TEXT_CLOSE);
          ????//判斷-?當展示全部原始內(nèi)容時?如果?TEXT_CLOSE?需要換行才能顯示完整,則直接將TEXT_CLOSE展示在下一行
          ????if?(layout2.getLineCount()?>?layout1.getLineCount())?{
          ????????setText(originText?+?"\n");
          ????}?else?{
          ????????setText(originText);
          ????}
          ????//加上[收起]的標簽
          ????append(SPAN_EXPAND);

          ????setMovementMethod(LinkMovementMethod.getInstance());

          ????replaceUrlSpan();
          }


          我們在[展開]和[收起]的標簽中先設(shè)置他們?yōu)榭牲c擊的標簽,然后再回調(diào)的Click方法中我們是設(shè)置切換 ExpandTextView.super.setMaxLines(mMaxLines); ?的方式來實現(xiàn)的。 當然如果覺得這樣的切換比較生硬,想用動畫來實現(xiàn)也是可以的。 另一種方法是記錄展開與收起的高度,然后做屬性動畫直接改變 layoutParams 的height,從而改變高度,實現(xiàn)對應展開收起的狀態(tài)切換。 之前在 setCloseText 方法中,我們預測量文本布局的時候已經(jīng)記錄了展開與收起的高度記錄。
              
                  private?int?mOpenHeight;???//展開的文本高度
          private?int?mCLoseHeight;??//收起的文本高度


          那么我就能用動畫來封裝一下實現(xiàn)。
              
                  class?ExpandCollapseAnimation?extends?Animation?{
          ????private?final?View?mTargetView;//動畫執(zhí)行view
          ????private?final?int?mStartHeight;//動畫執(zhí)行的開始高度
          ????private?final?int?mEndHeight;//動畫結(jié)束后的高度

          ????ExpandCollapseAnimation(View?target,?int?startHeight,?int?endHeight)?{
          ????????mTargetView?=?target;
          ????????mStartHeight?=?startHeight;
          ????????mEndHeight?=?endHeight;
          ????????setDuration(400);
          ????}

          ????@Override
          ????protected?void?applyTransformation(float?interpolatedTime,?Transformation?t)?{
          ????????//計算出每次應該顯示的高度,改變執(zhí)行view的高度,實現(xiàn)動畫
          ????????mTargetView.getLayoutParams().height?=?(int)?((mEndHeight?-?mStartHeight)?*?interpolatedTime?+?mStartHeight);
          ????????mTargetView.requestLayout();
          ????}
          }


          大致的實現(xiàn)如下:
              
                  private?void?executeOpenAnim()?{

          ????if?(mOpenAnim?==?null)?{
          ????????mOpenAnim?=?new?ExpandCollapseAnimation(this,?mCLoseHeight,?mOpenHeight);
          ????????mOpenAnim.setFillAfter(true);
          ????????mOpenAnim.setAnimationListener(new?Animation.AnimationListener()?{
          ????????????@Override
          ????????????public?void?onAnimationStart(Animation?animation)?{
          ????????????????ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE);
          ????????????????setText(mOpenSpannableStr);
          ????????????}

          ????????????@Override
          ????????????public?void?onAnimationEnd(Animation?animation)?{

          ????????????????getLayoutParams().height?=?mOpenHeight;
          ????????????????requestLayout();
          ????????????????animating?=?false;
          ????????????}

          ????????????@Override
          ????????????public?void?onAnimationRepeat(Animation?animation)?{

          ????????????}
          ????????});
          ????}

          ????if?(animating)?{
          ????????return;
          ????}
          ????animating?=?true;
          ????clearAnimation();

          ????startAnimation(mOpenAnim);
          }


          private?void?executeCloseAnim()?{

          ????if?(mCloseAnim?==?null)?{
          ????????mCloseAnim?=?new?ExpandCollapseAnimation(this,?mOpenHeight,?mCLoseHeight);
          ????????mCloseAnim.setFillAfter(true);
          ????????mCloseAnim.setAnimationListener(new?Animation.AnimationListener()?{
          ????????????@Override
          ????????????public?void?onAnimationStart(Animation?animation)?{

          ????????????}

          ????????????@Override
          ????????????public?void?onAnimationEnd(Animation?animation)?{
          ????????????????animating?=?false;
          ????????????????ExpandableTextView.super.setMaxLines(mMaxLines);
          ????????????????setText(mCloseSpannableStr);
          ????????????????getLayoutParams().height?=?mCLoseHeight;
          ????????????????requestLayout();
          ????????????}

          ????????????@Override
          ????????????public?void?onAnimationRepeat(Animation?animation)?{

          ????????????}
          ????????});
          ????}

          ????if?(animating)?{
          ????????return;
          ????}
          ????animating?=?true;
          ????clearAnimation();

          ????startAnimation(mCloseAnim);
          }


          兩種方法都是可以的,我這里的做法是第一種做法,直接設(shè)置 maxLine 的方法,沒有整那么多動畫。

          4.內(nèi)部Link鏈接的自定義處理

          這里的Demo,做了兩種演示,其實我么可以直接通過工具類轉(zhuǎn)換到我們自定義的ClickSpan,也可以通過new 一個 ButtonSpan 來替換實現(xiàn)。

          例如使用 ButtonSpan ,我們可以設(shè)置點擊,設(shè)置自定義字體等等。
              
                  SPAN_CLOSE?=?new?SpannableString(TEXT_EXPAND);

          ButtonSpan?span?=?new?ButtonSpan(getContext(),?new?View.OnClickListener()?{
          ????@Override
          ????public?void?onClick(View?v)?{
          ????????ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
          ????????setExpandText(originText);
          ????????if?(mCallback?!=?null)?mCallback.isExpand(1);
          ????}
          },?R.color.color_expand_span);

          SPAN_CLOSE.setSpan(span,?0,?TEXT_EXPAND.length(),?Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
          SPAN_CLOSE.setSpan(new?MyTypefaceSpan(TypefaceUtil.getSFRegular(getContext())),?0,?TEXT_EXPAND.length(),?Spannable.SPAN_INCLUSIVE_EXCLUSIVE);


          而內(nèi)部的網(wǎng)址點擊,由于默認是跳轉(zhuǎn)到瀏覽器,我們想App自己處理,那么我們就需要找到文本中的 URLSpan 對象,然后對他進行替換,換成我們自己的 InterceptUrlSpan 對象,跳轉(zhuǎn)到我們自己的WebView。
                /**
          ?*?填充文本之后嘗試替換URLSpan
          ?*/

          private?void?replaceUrlSpan()?{
          ????CharSequence?text?=?getText();
          ????if?(text?instanceof?Spannable)?{
          ????????int?end?=?text.length();
          ????????Spannable?sp?=?(Spannable)?text;

          ????????URLSpan[]?urls?=?sp.getSpans(0,?end,?URLSpan.class);
          ????????SpannableStringBuilder?spannableStringBuilder?=?new?SpannableStringBuilder(text);

          ????????if?(urls.length?>?0)?{
          ????????????for?(URLSpan?urlSpan?:?urls)?{
          ????????????????//攔截點擊,替換Span
          ????????????????InterceptUrlSpan?interceptUrlSpan?=?new?InterceptUrlSpan(urlSpan.getURL());
          ????????????????spannableStringBuilder.setSpan(interceptUrlSpan,?sp.getSpanStart(urlSpan),?sp.getSpanEnd(urlSpan),?Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
          ????????????}

          ????????????//替換之后重新設(shè)置進去
          ????????????setText(spannableStringBuilder);
          ????????}

          ????}
          }

          一般是在我們設(shè)置玩文本顯示之后再調(diào)用,如 setCloseText setExpandText 方法。


          效果: e7e03140b933ffb40af0b51e0c875603.webp

          5.結(jié)語

          涉及到的一些知識,文本Span的轉(zhuǎn)換, StaticLayout 的使用,URLSpan的查找與替換等。

          主要是和我們的需求相互對應,如果是要展開標簽要在文本后面顯示就簡單一點,如果換行展示就簡單一點,總的來說其實也不是很難,明確需求之后分解為一步一步的小需求,然后一步一步的實現(xiàn)小需求,串聯(lián)起來就是我們最終的效果。 由于一些隱私問題就沒有很方便的直接在我的Demo中完整貼出。如果大家對代碼有需求的話,全部的代碼其實都已經(jīng)在文中貼出了,大家細心整合一下就是完整的代碼了。 當然了,我這種方案可能也只是閉門造車,還需要大家提提意見,如果你有更好的方案,或者優(yōu)化的空間都也可以一起交流一下。如有錯漏的地方還請指出,如果有疑問也可以在評論區(qū)大家一起討論哦。 如果感覺本文對你有一點點的啟發(fā),還望你能?點贊?支持一下,你的支持是我最大的動力。 Ok,這一期就此完結(jié)。

          7483b9badf32113fd06dd0cf6b2b74c5.webp

          35c0a1a25659dd9b1761e971db99f039.webp
          8a93e21eff652997a07d46ab62d80aa2.webp

          為了防止失聯(lián),歡迎關(guān)注我防備的小號

              
                  
                    ?
                  
                
              

          ? ? ? ? ? ? ???微信改了推送機制,真愛請星標本公號??

          瀏覽 80
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲色图日韩 | 免费国产一级黄色电影 | 日韩无码视频一区二区三区 | 欧美激情视频一区二区三区不卡 | 国产毛片AV一区二区三区牛牛影视 |