Android 仿朋友圈全文、收起功能,支持話題、網(wǎng)址...
?安卓進階漲薪訓練營
,讓一部分人先進大廠
大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術(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

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è)置。
/**
?*?可點擊-帶下劃線
?*/
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);
setCloseText 方法就是具體的實現(xiàn)展開收起入口方法,我們看看它是怎么實現(xiàn)的。
2.TextView的展開收起功能
關(guān)于TextView的展開收起,都離不開 StaticLayout 這個神器。我們主要需要用到它的兩個方法 :
- 通過 StaticLayout 的 getLineCount() 方法知道文本是否會超出我們設(shè)置的maxLines,
- 通過 getLineEnd(int line) 方法可以找到最后一行的最后一個字符在文本中的位置。
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();
}
private?int?mOpenHeight;???//展開的文本高度
private?int?mCLoseHeight;??//收起的文本高度
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();
????}
}
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);
/**
?*?填充文本之后嘗試替換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 方法。

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

為了防止失聯(lián),歡迎關(guān)注我防備的小號
?
? ? ? ? ? ? ???微信改了推送機制,真愛請星標本公號??
