Android 仿朋友圈全文、收起功能,支持話題、網(wǎng)址...
?安卓進(jìn)階漲薪訓(xùn)練營
,讓一部分人先進(jìn)大廠
大家好,我是皇叔,最近開了一個(gè)安卓進(jìn)階漲薪訓(xùn)練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進(jìn)入心儀的公司。
詳情見文章:沒錯(cuò)!皇叔開了個(gè)訓(xùn)練營
作者:newki
https://juejin.cn/post/7154271756214075428
前言
之前的文章我們都講到了WX盆友圈動(dòng)態(tài)列表的效果,九宮格控件的實(shí)現(xiàn) 【傳送門】 。并且講到了發(fā)布動(dòng)態(tài)中話題的處理 【傳送門】 。那么在動(dòng)態(tài)列表中我們?nèi)绾物@示我們發(fā)布的話題數(shù)據(jù)和一些圈子數(shù)據(jù)呢?
https://juejin.cn/post/7153192823880155143
https://juejin.cn/post/7153932700066250760

1.TextView的特殊文本處理
我們在把服務(wù)器返回的文本設(shè)置給自定義折疊的TextView之前,我們先對文本進(jìn)行Span的預(yù)處理。
?/**
?????*?暴露方法-替換原文本中的話題數(shù)據(jù),變色處理
?????*
?????*?@param?topics??服務(wù)器返回的話題數(shù)據(jù)
?????*?@param?content?服務(wù)器返回的原始文本數(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?->?{
????????????????????????????//話題的點(diǎn)擊(路由直接跳轉(zhuǎn)搜索結(jié)果展示)
????????????????????????????listener.onTopicClick(charSequence.toString());
????????????????????????});
????????????????startPosition?=?endPosition;
????????????}
????????????return?topicCharSequece;
????????}
????????return?"";
????}
其實(shí)就是對多個(gè)話題進(jìn)行遍歷,找到start和end,然后使用Span的工具類,把普通的文本轉(zhuǎn)為可點(diǎn)擊和變色的Span。并回調(diào)出去外界使用。關(guān)鍵是要返回處理之后的文本 CharSequece 返回外部去設(shè)置。
/**
?*?可點(diǎ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)?{
????????????????//防止重復(fù)點(diǎn)擊
????????????????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 方法就是具體的實(shí)現(xiàn)展開收起入口方法,我們看看它是怎么實(shí)現(xiàn)的。
2.TextView的展開收起功能
關(guān)于TextView的展開收起,都離不開 StaticLayout 這個(gè)神器。我們主要需要用到它的兩個(gè)方法 :
- 通過 StaticLayout 的 getLineCount() 方法知道文本是否會(huì)超出我們設(shè)置的maxLines,
- 通過 getLineEnd(int line) 方法可以找到最后一行的最后一個(gè)字符在文本中的位置。
private?String?TEXT_EXPAND?=?"??[More]";
private?String?TEXT_CLOSE?=?"??[Show?Less]";
/**
?*?暴露的方法-默認(rèn)設(shè)置文本方法(如果需要折疊就會(huì)默認(rèn)折疊)
?*?如果有特殊的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)建出一個(gè)StaticLayout主要是為了計(jì)算行數(shù)
????????Layout?layout?=?createStaticLayout(workingText);
????????//計(jì)算全部展開的文本高度
????????mOpenHeight?=?layout.getHeight()?+?getPaddingTop()?+?getPaddingBottom();
????????if?(layout.getLineCount()?>?maxLines)?{
????????????//獲取一行顯示字符個(gè)數(shù),然后截取字符串?dāng)?shù),?收起狀態(tài)原始文本截取展示的部分
????????????workingText?=?originText.subSequence(0,?layout.getLineEnd(maxLines?-?1));
????????????//再對加上[收起]標(biāo)簽的文本進(jìn)行測量
????????????String?showText?=?originText.subSequence(0,?layout.getLineEnd(maxLines?-?1))?+?"..."?+?SPAN_CLOSE;
????????????Layout?layout2?=?createStaticLayout(showText);
????????????//?對workingText進(jìn)行-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);
????????????}
????????????//計(jì)算收起的文本高度
????????????mCLoseHeight?=?layout2.getHeight()?+?getPaddingTop()?+?getPaddingBottom();
????????????appendShowAll?=?true;
????????}
????}
????setText(workingText);
????if?(appendShowAll)?{
????????//?必須使用append,不能在上面使用+連接,否則會(huì)失效
????????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);
}
其實(shí)只需要這兩個(gè)方法就可以展示一個(gè)折疊起來的文本了。那么如何切換展開與收起的狀態(tài)呢?
3.多種方式的實(shí)現(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è)置展開的文本展示-后面加上[收起]的文本標(biāo)簽
?*/
private?void?setExpandText(CharSequence?text)?{
????if?(SPAN_EXPAND?==?null)?{
????????initExpandEnd();
????}
????//創(chuàng)建出一個(gè)StaticLayout主要是為了計(jì)算行數(shù)
????Layout?layout1?=?createStaticLayout(text);
????Layout?layout2?=?createStaticLayout(text?+?TEXT_CLOSE);
????//判斷-?當(dāng)展示全部原始內(nèi)容時(shí)?如果?TEXT_CLOSE?需要換行才能顯示完整,則直接將TEXT_CLOSE展示在下一行
????if?(layout2.getLineCount()?>?layout1.getLineCount())?{
????????setText(originText?+?"\n");
????}?else?{
????????setText(originText);
????}
????//加上[收起]的標(biāo)簽
????append(SPAN_EXPAND);
????setMovementMethod(LinkMovementMethod.getInstance());
????replaceUrlSpan();
}
private?int?mOpenHeight;???//展開的文本高度
private?int?mCLoseHeight;??//收起的文本高度
class?ExpandCollapseAnimation?extends?Animation?{
????private?final?View?mTargetView;//動(dòng)畫執(zhí)行view
????private?final?int?mStartHeight;//動(dòng)畫執(zhí)行的開始高度
????private?final?int?mEndHeight;//動(dòng)畫結(jié)束后的高度
????ExpandCollapseAnimation(View?target,?int?startHeight,?int?endHeight)?{
????????mTargetView?=?target;
????????mStartHeight?=?startHeight;
????????mEndHeight?=?endHeight;
????????setDuration(400);
????}
????@Override
????protected?void?applyTransformation(float?interpolatedTime,?Transformation?t)?{
????????//計(jì)算出每次應(yīng)該顯示的高度,改變執(zhí)行view的高度,實(shí)現(xiàn)動(dòng)畫
????????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 的方法,沒有整那么多動(dòng)畫。
4.內(nèi)部Link鏈接的自定義處理
這里的Demo,做了兩種演示,其實(shí)我么可以直接通過工具類轉(zhuǎn)換到我們自定義的ClickSpan,也可以通過new 一個(gè) ButtonSpan 來替換實(shí)現(xiàn)。
例如使用 ButtonSpan ,我們可以設(shè)置點(diǎn)擊,設(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)?{
????????????????//攔截點(diǎn)擊,替換Span
????????????????InterceptUrlSpan?interceptUrlSpan?=?new?InterceptUrlSpan(urlSpan.getURL());
????????????????spannableStringBuilder.setSpan(interceptUrlSpan,?sp.getSpanStart(urlSpan),?sp.getSpanEnd(urlSpan),?Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
????????????}
????????????//替換之后重新設(shè)置進(jìn)去
????????????setText(spannableStringBuilder);
????????}
????}
}
一般是在我們設(shè)置玩文本顯示之后再調(diào)用,如 setCloseText setExpandText 方法。

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

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