热点新闻
Android 自定义可展开收回能够@xxx和#话题的TextView(仿小红书效果)
2023-07-13 07:38  浏览:5711  搜索引擎搜索“手机速企网”
温馨提示:信息一旦丢失不一定找得到,请务必收藏信息以备急用!本站所有信息均是注册会员发布如遇到侵权请联系文章中的联系方式或客服删除!
联系我时,请说明是在手机速企网看到的信息,谢谢。
展会发布 展会网站大全 报名观展合作 软文发布

前段时间接到一个需求,需要完成以下效果。





image.png

大致功能和小红书效果类似 可以 展开 和收起 也可以@xxx 还能加#话题

  • 1、内容超过指定行数需要折叠起来;
  • 2、内容中含有@+“内容”,需要携带“内容”跳转指定页面。
  • 3、有可能会在“展开”或者“收回”前面附加显示其他内容

实现思路:

可以自定义View继承TextView,在自定义View里面去处理所有的逻辑,这样方便后期维护扩展。

具体实现

在开始写代码之前,我们需要考虑几个点

  • 怎么保证“展开”或者“收回”放在文字的最后面
  • 如何识别文字中的@用户和#话题
  • 处理@用户,链接和“展开”或者“收回”三者的高亮显示和点击事件

问题处理

一、怎么保证“展开”或者“收回”放在文字的最后面

这个确实挺难处理的!在此之前也是让我头疼的一个问题,不过后来我遇到了DynamicLayout,使用它我们可以获取行的最后位置,行的开始位置,行的行宽以及指定内容的所占的行数。

//用来计算内容的大小 DynamicLayout mDynamicLayout = new DynamicLayout(mFormatData.formatedContent, mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f, true); //获取行数 int mLineCount = mDynamicLayout.getLineCount(); int index = currentLines - 1; //获取指定行的最后位置 int endPosition = mDynamicLayout.getLineEnd(index); //获取指定行的开始位置 int startPosition = mDynamicLayout.getLineStart(index); //获取指定行的行宽 float lineWidth = mDynamicLayout.getLineWidth(index);


image.png

有了这些东西经过简单的计算我们就可以获取到我们需要截取的内容长度。对原内容进行截取再拼接上“展开”或“收回”即可!

private int getFitPosition(int endPosition, int startPosition, float lineWidth, float endStringWith, float offset, String aimContent) { //最后一行需要添加的文字的字数 int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition)/ lineWidth); if (position < 0) return endPosition; //计算最后一行需要显示的正文的长度 float measureText = mPaint.measureText( (aimContent.substring(startPosition, startPosition + position))); //如果最后一行需要显示的正文的长度比最后一行的长减去“展开”文字的长度要短就可以了 否则加个空格继续算 if (measureText <= lineWidth - endStringWith) { return startPosition + position; } else { return getFitPosition(endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText(" ")); } }

二、如何识别文字中的@用户

//对@用户 进行正则匹配 Pattern pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容进行统计处理 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), linkType.MENTION_TYPE)); }

三、处理@用户,链接和“展开”或者“收回”三者的高亮显示和点击事件

对于@用户,链接和“展开”或者“收回”三者的实现,最终都是使用SpannableStringBuilder来处理。之前我们在对原内容进行解析的时候,将匹配到的链接或者@用户进行了存储,并且存储了他们所在的位置(start,end)以及类型。

//定义类型的枚举类型 public enum linkType { //普通链接 link_TYPE, //@用户 MENTION_TYPE }

复制代码有了这些数据的集合,我们只需要遍历这些数据,并分别对这些数据进行setSpan处理,并且在setSpan的过程中设置字体颜色,以及点击事件的回调即可。

//处理链接或者@用户 private void deallinksOrMention(FormatData formatData,SpannableStringBuilder ssb) { List<FormatData.PositionData> positionDatas = formatData.getPositionDatas(); HH: for (FormatData.PositionData data: positionDatas) { if (data.getType().equals(linkType.link_TYPE)) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { SelfImageSpan imageSpan = new SelfImageSpan(mlinkDrawable, ImageSpan.ALIGN_baseLINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); //设置链接文字样式 int endPosition = data.getEnd(); if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) { endPosition = fitPosition; } if (data.getStart() + 1 < fitPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onlinkClickListener(linkType.link_TYPE, data.getUrl()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mlinkTextColor); ds.setUnderlineText(false); } }, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } else { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (fitPosition < data.getEnd()) { endPosition = fitPosition; } ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onlinkClickListener(linkType.MENTION_TYPE, data.getUrl()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mlinkTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } } private void setExpandSpan(SpannableStringBuilder ssb,FormatData formatData){ int index = currentLines - 1; int endPosition = mDynamicLayout.getLineEnd(index); int startPosition = mDynamicLayout.getLineStart(index); float lineWidth = mDynamicLayout.getLineWidth(index); String endString = getHideEndContent(); //计算原内容被截取的位置下标 int fitPosition = getFitPosition(endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0); ssb.append(formatData.formatedContent.substring(0, fitPosition)); //在被截断的文字后面添加 展开 文字 ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { action(); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mExpandTextColor); ds.setUnderlineText(false); } }, ssb.length() - TEXT_EXPEND.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); }

复制代码在处理这一块的时候有个细节需要注意,那就是假如在文字切割后的末尾正好有个一个链接,而这个地方又要显示“展开”或者“收回”,这个地方要特别注意链接setSpan的范围,一不注意就可能连同把后面的“展开”或者“收回”也一起设置了,导致事件不对。处理“收回”是差不多的,就不贴代码了。最后还有一个附加功能就是在最后添加时间串的功能,其实也就是在“展开”和“收回”前面加一个串,做好这方面的判断就好了,代码里面已经做了处理。

下面是所有源码实现:

public class CustomExpandableTextView extends AppCompatTextView { private static final int DEF_MAX_LINE = 4; public static String TEXT_ConTRACT = "收起"; public static String TEXT_EXPEND = "展开"; public static final String Space = " "; public static String TEXT_TARGET = "网页链接"; public static final String IMAGE_TARGET = "图"; public static final String TARGET = IMAGE_TARGET + TEXT_TARGET; public static final String DEFAULT_ConTENT = " "; private static int retryTime = 0; public static final String regexp_mention = "@[^\\n\\s]{1,80}\\s{1}"; public static final String regexp_topic = "#[^\\n\\s]{1,80}\\s{1}"; //匹配自定义链接的正则表达式 public static final String self_regex = "\\[([^\\[]*)\\]\\(([^\\(]*)\\)"; private TextPaint mPaint; boolean linkHit; private Context mContext; private ExpandableStatusFix mModel; private DynamicLayout mDynamicLayout; //hide状态下,展示多少行开始省略 private int mLimitLines; private int currentLines; private int mWidth; private Drawable mlinkDrawable = null; private onlinkClickListener linkClickListener; private boolean needRealExpandOrContract = true; private onExpandOrContractClickListener expandOrContractClickListener; private boolean mNeedContract = true; private FormatData mFormatData; private boolean mNeedExpend = true; private boolean mNeedConvertUrl = true; private boolean mNeedMention = true; private boolean mNeedTopic = true; private boolean mNeedlink = true; private boolean mNeedSelf = false; private boolean mNeedAlwaysShowRight = false; private boolean mNeedAnimation = true; private int mLineCount; private CharSequence mContent; private int mExpandTextColor; private int mMentionTextColor; private int mTopicTextColor; private int mlinkTextColor; private int mSelfTextColor; private int mContractTextColor; private String mExpandString; private String mContractString; private String mEndExpandContent; private int mEndExpandTextColor; //是否AttachedToWindow private boolean isAttached; public ExpandableTextView(Context context) { this(context, null); } public ExpandableTextView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, -1); } public ExpandableTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); setMovementMethod(LocallinkMovementMethod.getInstance()); addonAttachStateChangeListener(new onAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { if (isAttached == false) doSetContent(); isAttached = true; } @Override public void onViewDetachedFromWindow(View v) { } }); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { //适配英文版 TEXT_ConTRACT = context.getString(R.string.social_contract); TEXT_EXPEND = context.getString(R.string.social_expend); TEXT_TARGET = context.getString(R.string.social_text_target); if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView, defStyleAttr, 0); mLimitLines = a.getInt(R.styleable.ExpandableTextView_ep_max_line, DEF_MAX_LINE); mNeedExpend = a.getBoolean(R.styleable.ExpandableTextView_ep_need_expand, true); mNeedContract = a.getBoolean(R.styleable.ExpandableTextView_ep_need_contract, false); mNeedAnimation = a.getBoolean(R.styleable.ExpandableTextView_ep_need_animation, true); mNeedSelf = a.getBoolean(R.styleable.ExpandableTextView_ep_need_self, false); mNeedMention = a.getBoolean(R.styleable.ExpandableTextView_ep_need_mention, true); mNeedTopic = a.getBoolean(R.styleable.ExpandableTextView_ep_need_mention, true); mNeedlink = a.getBoolean(R.styleable.ExpandableTextView_ep_need_link, true); mNeedAlwaysShowRight = a.getBoolean(R.styleable.ExpandableTextView_ep_need_always_showright, false); mNeedConvertUrl = a.getBoolean(R.styleable.ExpandableTextView_ep_need_convert_url, true); mContractString = a.getString(R.styleable.ExpandableTextView_ep_contract_text); mExpandString = a.getString(R.styleable.ExpandableTextView_ep_expand_text); if (TextUtils.isEmpty(mExpandString)) { mExpandString = TEXT_EXPEND; } if (TextUtils.isEmpty(mContractString)) { mContractString = TEXT_CONTRACT; } mExpandTextColor = a.getColor(R.styleable.ExpandableTextView_ep_expand_color, Color.parseColor("#999999")); mEndExpandTextColor = a.getColor(R.styleable.ExpandableTextView_ep_expand_color, Color.parseColor("#999999")); mContractTextColor = a.getColor(R.styleable.ExpandableTextView_ep_contract_color, Color.parseColor("#999999")); mlinkTextColor = a.getColor(R.styleable.ExpandableTextView_ep_link_color, Color.parseColor("#FF6200")); mSelfTextColor = a.getColor(R.styleable.ExpandableTextView_ep_self_color, Color.parseColor("#FF6200")); mMentionTextColor = a.getColor(R.styleable.ExpandableTextView_ep_mention_color, Color.parseColor("#FF6200")); mTopicTextColor = a.getColor(R.styleable.ExpandableTextView_ep_topic_color, Color.parseColor("#FF6200")); int resId = a.getResourceId(R.styleable.ExpandableTextView_ep_link_res, R.mipmap.link); mlinkDrawable = getResources().getDrawable(resId); currentLines = mLimitLines; a.recycle(); } else { mlinkDrawable = context.getResources().getDrawable(R.mipmap.link); } mContext = context; mPaint = getPaint(); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); //初始化link的图片 mlinkDrawable.setBounds(0, 0, 30, 30); //必须设置图片大小,否则不显示 } private SpannableStringBuilder setRealContent(CharSequence content,boolean isHide) { //处理给定的数据 mFormatData = formatData(content); //用来计算内容的大小 mDynamicLayout = new DynamicLayout(mFormatData.getFormatedContent(), mPaint, mWidth, Layout.Alignment.ALIGN_NORMAL, 1.2f, 0.0f, true); //获取行数 mLineCount = mDynamicLayout.getLineCount(); if (onGetLineCountListener != null) { onGetLineCountListener.onGetLineCount(mLineCount, mLineCount > mLimitLines); } if (!mNeedExpend || mLineCount <= mLimitLines) { //不需要展开功能 直接处理链接模块 return deallink(mFormatData, false,false); } else { return deallink(mFormatData, true,isHide); } } public void setEndExpendContent(String endExpendContent) { this.mEndExpandContent = endExpendContent; } public void setContent(final String content) { mContent = content; if (isAttached) doSetContent(); } private void doSetContent() { if (mContent == null) { return; } currentLines = mLimitLines; if (mWidth <= 0) { if (getWidth() > 0) mWidth = getWidth() - getPaddingLeft() - getPaddingRight(); } if (mWidth <= 0) { if (retryTime > 10) { setText(DEFAULT_CONTENT); } this.post(new Runnable() { @Override public void run() { retryTime++; setContent(mContent.toString()); } }); } else { setRealContent(mContent.toString(),false); } } private String getExpandEndContent() { if (TextUtils.isEmpty(mEndExpandContent)) { return String.format(Locale.getDefault(), " %s", mContractString); } else { return String.format(Locale.getDefault(), " %s %s", mEndExpandContent, mContractString); } } private String getHideEndContent() { if (TextUtils.isEmpty(mEndExpandContent)) { return String.format(Locale.getDefault(), mNeedAlwaysShowRight ? " %s" : "... %s", mExpandString); } else { return String.format(Locale.getDefault(), mNeedAlwaysShowRight ? " %s %s" : "... %s %s", mEndExpandContent, mExpandString); } } private SpannableStringBuilder deallink(FormatData formatData, boolean ignoreMore,boolean mIsHide) { SpannableStringBuilder ssb = new SpannableStringBuilder(); //获取存储的状态 if (mModel != null && mModel.getStatus() != null) { boolean isHide = false; if (mModel.getStatus() != null) { if (mModel.getStatus().equals(StatusType.STATUS_CONTRACT)) { //收起 isHide = true; } else { //展开 isHide = false; } } if (isHide) { currentLines = mLimitLines + ((mLineCount - mLimitLines)); } else { if (mNeedContract) currentLines = mLimitLines; } mIsHide = isHide; } //处理折叠操作 if (ignoreMore) { if (currentLines < mLineCount) { int index = currentLines - 1; int endPosition = mDynamicLayout.getLineEnd(index); int startPosition = mDynamicLayout.getLineStart(index); float lineWidth = mDynamicLayout.getLineWidth(index); String endString = getHideEndContent(); //计算原内容被截取的位置下标 int fitPosition = getFitPosition(endString, endPosition, startPosition, lineWidth, mPaint.measureText(endString), 0); String substring = formatData.getFormatedContent().substring(0, fitPosition); if (substring.endsWith("\n")) { substring = substring.substring(0, substring.length() - "\n".length()); } ssb.append(substring); if (mNeedAlwaysShowRight) { //计算一下最后一行有没有充满 float lastLineWidth = 0; for (int i = 0; i < index; i++) { lastLineWidth += mDynamicLayout.getLineWidth(i); } lastLineWidth = lastLineWidth / (index); float emptyWidth = lastLineWidth - lineWidth - mPaint.measureText(endString); if (emptyWidth > 0) { float measureText = mPaint.measureText(Space); int count = 0; while (measureText * count < emptyWidth) { count++; } count = count - 1; for (int i = 0; i < count; i++) { ssb.append(Space); } } } //在被截断的文字后面添加 展开 文字 ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (needRealExpandOrContract) { if (mModel != null) { mModel.setStatus(StatusType.STATUS_CONTRACT); action(mModel.getStatus()); } else { action(); } } if (expandOrContractClickListener != null) { expandOrContractClickListener.onClick(StatusType.STATUS_EXPAND); } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mExpandTextColor); ds.setUnderlineText(false); } }, ssb.length() - mExpandString.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } else { ssb.append(formatData.getFormatedContent()); if (mNeedContract) { String endString = getExpandEndContent(); if (mNeedAlwaysShowRight) { //计算一下最后一行有没有充满 int index = mDynamicLayout.getLineCount() - 1; float lineWidth = mDynamicLayout.getLineWidth(index); float lastLineWidth = 0; for (int i = 0; i < index; i++) { lastLineWidth += mDynamicLayout.getLineWidth(i); } lastLineWidth = lastLineWidth / (index); float emptyWidth = lastLineWidth - lineWidth - mPaint.measureText(endString); if (emptyWidth > 0) { float measureText = mPaint.measureText(Space); int count = 0; while (measureText * count < emptyWidth) { count++; } count = count - 1; for (int i = 0; i < count; i++) { ssb.append(Space); } } } ssb.append(endString); int expendLength = TextUtils.isEmpty(mEndExpandContent) ? 0 : 2 + mEndExpandContent.length(); ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (mModel != null) { mModel.setStatus(StatusType.STATUS_EXPAND); action(mModel.getStatus()); } else { action(); } if (expandOrContractClickListener != null) { expandOrContractClickListener.onClick(StatusType.STATUS_CONTRACT); } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setColor(mContractTextColor); ds.setUnderlineText(false); } }, ssb.length() - mContractString.length() - expendLength, ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } else { if (!TextUtils.isEmpty(mEndExpandContent)) { ssb.append(mEndExpandContent); ssb.setSpan(new ForegroundColorSpan(mEndExpandTextColor), ssb.length() - mEndExpandContent.length(), ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } } } else { ssb.append(formatData.getFormatedContent()); if (!TextUtils.isEmpty(mEndExpandContent)) { ssb.append(mEndExpandContent); ssb.setSpan(new ForegroundColorSpan(mEndExpandTextColor), ssb.length() - mEndExpandContent.length(), ssb.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } //处理链接或者@用户 List<FormatData.PositionData> positionDatas = formatData.getPositionDatas(); HH: for (FormatData.PositionData data: positionDatas) { if (ssb.length() >= data.getEnd()) { if (data.getType().equals(linkType.link_TYPE)) { if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { SelfImageSpan imageSpan = new SelfImageSpan(mlinkDrawable, ImageSpan.ALIGN_baseLINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); //设置链接文字样式 int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition > data.getStart() + 1 && fitPosition < data.getEnd()) { endPosition = fitPosition; } } if (data.getStart() + 1 < fitPosition) { addUrl(ssb, data, endPosition); } } } else { SelfImageSpan imageSpan = new SelfImageSpan(mlinkDrawable, ImageSpan.ALIGN_baseLINE); //设置链接图标 ssb.setSpan(imageSpan, data.getStart(), data.getStart() + 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE); addUrl(ssb, data, data.getEnd()); } } else if (data.getType().equals(linkType.MENTION_TYPE)) { //如果需要展开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addMention(ssb, data, endPosition); } } else { addMention(ssb, data, data.getEnd()); } } else if (data.getType().equals(linkType.TOPIC_TYPE)) { //如果需要展开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() -(mIsHide?getExpandEndContent().length():getHideEndContent().length()) ; if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addTopic(ssb, data, endPosition); } } else { addTopic(ssb, data, data.getEnd()); } }else if (data.getType().equals(linkType.SELF)) { //自定义 //如果需要展开 if (mNeedExpend && ignoreMore) { int fitPosition = ssb.length() - getHideEndContent().length(); if (data.getStart() < fitPosition) { int endPosition = data.getEnd(); if (currentLines < mLineCount) { if (fitPosition < data.getEnd()) { endPosition = fitPosition; } } addSelf(ssb, data, endPosition); } } else { addSelf(ssb, data, data.getEnd()); } } } } //清除链接点击时背景效果 setHighlightColor(Color.TRANSPARENT); //将内容设置到控件中 setText(ssb); return ssb; } private int getFitSpaceCount(float emptyWidth, float endStringWidth) { float measureText = mPaint.measureText(Space); int count = 0; while (endStringWidth + measureText * count < emptyWidth) { count++; } return --count; } private void addSelf(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onlinkClickListener(linkType.SELF, data.getSelfAim(), data.getSelfContent()); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mSelfTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } private void addMention(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onlinkClickListener(linkType.MENTION_TYPE, data.getUrl(), null); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mMentionTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } private void addTopic(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) linkClickListener.onlinkClickListener(linkType.TOPIC_TYPE, data.getUrl(), null); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mTopicTextColor); ds.setUnderlineText(false); } }, data.getStart(), endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } private void addUrl(SpannableStringBuilder ssb, final FormatData.PositionData data, int endPosition) { ssb.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { if (linkClickListener != null) { linkClickListener.onlinkClickListener(linkType.link_TYPE, data.getUrl(), null); } else { //如果没有设置监听 则调用默认的打开浏览器显示连接 Intent intent = new Intent(); intent.setAction("android.intent.action.VIEW"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri url = Uri.parse(data.getUrl()); intent.setData(url); mContext.startActivity(intent); } } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mlinkTextColor); ds.setUnderlineText(false); } }, data.getStart() + 1, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } public void setCurrStatus(StatusType type) { action(type); } private void action() { action(null); } private void action(StatusType type) { boolean isHide = currentLines < mLineCount; if (type != null) { mNeedAnimation = false; } if (mNeedAnimation) { ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); final boolean finalIsHide = isHide; valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Float value = (Float) animation.getAnimatedValue(); if (finalIsHide) { currentLines = mLimitLines + (int) ((mLineCount - mLimitLines) * value); } else { if (mNeedContract) currentLines = mLimitLines + (int) ((mLineCount - mLimitLines) * (1 - value)); } setText(setRealContent(mContent,finalIsHide)); } }); valueAnimator.setDuration(100); valueAnimator.start(); } else { if (isHide) { currentLines = mLimitLines + ((mLineCount - mLimitLines)); } else { if (mNeedContract) currentLines = mLimitLines; } setText(setRealContent(mContent,isHide)); } } private int getFitPosition(String endString, int endPosition, int startPosition, float lineWidth, float endStringWith, float offset) { //最后一行需要添加的文字的字数 int position = (int) ((lineWidth - (endStringWith + offset)) * (endPosition - startPosition) / lineWidth); if (position <= endString.length()) return endPosition; //计算最后一行需要显示的正文的长度 float measureText = mPaint.measureText( (mFormatData.getFormatedContent().substring(startPosition, startPosition + position))); //如果最后一行需要显示的正文的长度比最后一行的长减去“展开”文字的长度要短就可以了 否则加个空格继续算 if (measureText <= lineWidth - endStringWith) { return startPosition + position; } else { return getFitPosition(endString, endPosition, startPosition, lineWidth, endStringWith, offset + mPaint.measureText(Space)); } } private FormatData formatData(CharSequence content) { FormatData formatData = new FormatData(); List<FormatData.PositionData> datas = new ArrayList<>(); //对链接进行正则匹配 // Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); Pattern pattern = Pattern.compile(self_regex, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(content); StringBuffer newResult = new StringBuffer(); int start = 0; int end = 0; int temp = 0; Map<String, String> convert = new HashMap<>(); //对自定义的进行正则匹配 if (mNeedSelf) { List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { start = matcher.start(); end = matcher.end(); newResult.append(content.toString().substring(temp, start)); //将匹配到的内容进行统计处理 String result = matcher.group(); if (!TextUtils.isEmpty(result)) { //解析数据 String aimSrt = result.substring(result.indexOf("[") + 1, result.indexOf("]")); String contentSrt = result.substring(result.indexOf("(") + 1, result.indexOf(")")); String key = UUIDUtils.getUuid(aimSrt.length()); datasMention.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + aimSrt.length(), aimSrt, contentSrt, linkType.SELF)); convert.put(key, aimSrt); newResult.append(" " + key + " "); temp = end; } } datas.addAll(datasMention); } //重置状态 newResult.append(content.toString().substring(end, content.toString().length())); content = newResult.toString(); newResult = new StringBuffer(); start = 0; end = 0; temp = 0; if (mNeedlink) { pattern = AUTOlink_WEB_URL; matcher = pattern.matcher(content); while (matcher.find()) { start = matcher.start(); end = matcher.end(); newResult.append(content.toString().substring(temp, start)); if (mNeedConvertUrl) { //将匹配到的内容进行统计处理 datas.add(new FormatData.PositionData(newResult.length() + 1, newResult.length() + 2 + TARGET.length(), matcher.group(), linkType.link_TYPE)); newResult.append(" " + TARGET + " "); } else { String result = matcher.group(); String key = UUIDUtils.getUuid(result.length()); datas.add(new FormatData.PositionData(newResult.length(), newResult.length() + 2 + key.length(), result, linkType.link_TYPE)); convert.put(key, result); newResult.append(" " + key + " "); } temp = end; } } newResult.append(content.toString().substring(end, content.toString().length())); //对@用户 进行正则匹配 if (mNeedMention) { pattern = Pattern.compile(regexp_mention, Pattern.CASE_INSENSITIVE); matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容进行统计处理 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), linkType.MENTION_TYPE)); } datas.addAll(0, datasMention); } if (mNeedTopic) { pattern = Pattern.compile(regexp_topic, Pattern.CASE_INSENSITIVE); matcher = pattern.matcher(newResult.toString()); List<FormatData.PositionData> datasMention = new ArrayList<>(); while (matcher.find()) { //将匹配到的内容进行统计处理 datasMention.add(new FormatData.PositionData(matcher.start(), matcher.end(), matcher.group(), linkType.TOPIC_TYPE)); } datas.addAll(0, datasMention); } if (!convert.isEmpty()) { String resultData = newResult.toString(); for (Map.Entry<String, String> entry : convert.entrySet()) { resultData = resultData.replaceAll(entry.getKey(), entry.getValue()); } newResult = new StringBuffer(resultData); } formatData.setFormatedContent(newResult.toString()); formatData.setPositionDatas(datas); return formatData; } class SelfImageSpan extends ImageSpan { private Drawable drawable; public SelfImageSpan(Drawable d, int verticalAlignment) { super(d, verticalAlignment); this.drawable = d; } @Override public Drawable getDrawable() { return drawable; } @Override public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) { // image to draw Drawable b = getDrawable(); // font metrics of text to be replaced Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int transY = (y + fm.descent + y + fm.ascent) / 2 - b.getBounds().bottom / 2; canvas.save(); canvas.translate(x, transY); b.draw(canvas); canvas.restore(); } } public void bind(ExpandableStatusFix model) { mModel = model; } public static class LocallinkMovementMethod extends linkMovementMethod { static LocallinkMovementMethod sInstance; public static LocallinkMovementMethod getInstance() { if (sInstance == null) sInstance = new LocallinkMovementMethod(); return sInstance; } @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x += widget.getScrollX(); y += widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = buffer.getSpans( off, off, ClickableSpan.class); if (link.length != 0) { if (action == MotionEvent.ACTION_UP) { link[0].onClick(widget); } else if (action == MotionEvent.ACTION_DOWN) { Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])); } if (widget instanceof ExpandableTextView) { ((ExpandableTextView) widget).linkHit = true; } return true; } else { Selection.removeSelection(buffer); Touch.onTouchEvent(widget, buffer, event); return false; } } return Touch.onTouchEvent(widget, buffer, event); } } boolean dontConsumeNonUrlClicks = true; @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); linkHit = false; boolean res = super.onTouchEvent(event); if (dontConsumeNonUrlClicks) return linkHit; //防止选择复制的状态不消失 if (action == MotionEvent.ACTION_UP) { this.setTextIsSelectable(false); } return res; } public interface onlinkClickListener { void onlinkClickListener(linkType type, String content, String selfContent); } public interface onGetLineCountListener { void onGetLineCount(int lineCount, boolean canExpand); } private onGetLineCountListener onGetLineCountListener; public onGetLineCountListener getonGetLineCountListener() { return onGetLineCountListener; } public void setonGetLineCountListener(onGetLineCountListener onGetLineCountListener) { this.onGetLineCountListener = onGetLineCountListener; } public interface onExpandOrContractClickListener { void onClick(StatusType type); } public onlinkClickListener getlinkClickListener() { return linkClickListener; } public void setlinkClickListener(onlinkClickListener linkClickListener) { this.linkClickListener = linkClickListener; } public boolean ismNeedMention() { return mNeedMention; } public void setNeedMention(boolean mNeedMention) { this.mNeedMention = mNeedMention; } public Drawable getlinkDrawable() { return mlinkDrawable; } public void setlinkDrawable(Drawable mlinkDrawable) { this.mlinkDrawable = mlinkDrawable; } public boolean isNeedContract() { return mNeedContract; } public void setNeedContract(boolean mNeedContract) { this.mNeedContract = mNeedContract; } public boolean isNeedExpend() { return mNeedExpend; } public void setNeedExpend(boolean mNeedExpend) { this.mNeedExpend = mNeedExpend; } public boolean isNeedAnimation() { return mNeedAnimation; } public void setNeedAnimation(boolean mNeedAnimation) { this.mNeedAnimation = mNeedAnimation; } public int getExpandableLineCount() { return mLineCount; } public void setExpandableLineCount(int mLineCount) { this.mLineCount = mLineCount; } public int getExpandTextColor() { return mExpandTextColor; } public void setExpandTextColor(int mExpandTextColor) { this.mExpandTextColor = mExpandTextColor; } public int getExpandablelinkTextColor() { return mlinkTextColor; } public void setExpandablelinkTextColor(int mlinkTextColor) { this.mlinkTextColor = mlinkTextColor; } public int getContractTextColor() { return mContractTextColor; } public void setContractTextColor(int mContractTextColor) { this.mContractTextColor = mContractTextColor; } public String getExpandString() { return mExpandString; } public void setExpandString(String mExpandString) { this.mExpandString = mExpandString; } public String getContractString() { return mContractString; } public void setContractString(String mContractString) { this.mContractString = mContractString; } public int getEndExpandTextColor() { return mEndExpandTextColor; } public void setEndExpandTextColor(int mEndExpandTextColor) { this.mEndExpandTextColor = mEndExpandTextColor; } public boolean isNeedlink() { return mNeedlink; } public void setNeedlink(boolean mNeedlink) { this.mNeedlink = mNeedlink; } public int getSelfTextColor() { return mSelfTextColor; } public void setSelfTextColor(int mSelfTextColor) { this.mSelfTextColor = mSelfTextColor; } public boolean isNeedSelf() { return mNeedSelf; } public void setNeedSelf(boolean mNeedSelf) { this.mNeedSelf = mNeedSelf; } public boolean isNeedAlwaysShowRight() { return mNeedAlwaysShowRight; } public void setNeedAlwaysShowRight(boolean mNeedAlwaysShowRight) { this.mNeedAlwaysShowRight = mNeedAlwaysShowRight; } public onExpandOrContractClickListener getExpandOrContractClickListener() { return expandOrContractClickListener; } public void setExpandOrContractClickListener(onExpandOrContractClickListener expandOrContractClickListener) { this.expandOrContractClickListener = expandOrContractClickListener; } public void setExpandOrContractClickListener(onExpandOrContractClickListener expandOrContractClickListener, boolean needRealExpandOrContract) { this.expandOrContractClickListener = expandOrContractClickListener; this.needRealExpandOrContract = needRealExpandOrContract; } }

//定义类型的枚举类型 public enum linkType { //普通链接 link_TYPE, //@用户 MENTION_TYPE, TOPIC_TYPE, //自定义规则 SELF } public enum StatusType { //展开 STATUS_EXPAND, //收起 STATUS_ConTRACT }

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">ExpandableTextViewLibrary</string> <string name="social_contract">收起</string> <string name="social_expend">展开</string> <string name="social_text_target">网页链接</string> <declare-styleable name="ExpandableTextView"> <!--保留的行数--> <attr format="integer" name="ep_max_line"/> <!--是否需要展开--> <attr format="boolean" name="ep_need_expand"/> <!--是否需要收起 这个是建立在开启展开的基础上的--> <attr format="boolean" name="ep_need_contract"/> <!--是否需要@用户 --> <attr format="boolean" name="ep_need_mention"/> <!--是否需要对链接进行处理 --> <attr format="boolean" name="ep_need_link"/> <!--是否需要动画--> <attr format="boolean" name="ep_need_animation"/> <!--是否需要永远将展开或者收回放置在最后边--> <attr format="boolean" name="ep_need_always_showright"/> <!--是否需要将连接转换成网页链接显示 默认为true--> <attr format="boolean" name="ep_need_convert_url"/> <!--是否需要自定义规则--> <attr format="boolean" name="ep_need_self"/> <!--收起的文案--> <attr format="string" name="ep_contract_text"/> <!--展开的文案--> <attr format="string" name="ep_expand_text"/> <!--展开的文字的颜色--> <attr format="color" name="ep_expand_color"/> <!--收起的文字的颜色--> <attr format="color" name="ep_contract_color"/> <!--在收回和展开前面添加的内容的字体颜色--> <attr format="color" name="ep_end_color"/> <!--链接的文字的颜色--> <attr format="color" name="ep_link_color"/> <!--@用户的文字的颜色--> <attr format="color" name="ep_mention_color"/> <!--@用户的文字的颜色--> <attr format="color" name="ep_topic_color"/> <!--自定义规则的文字的颜色--> <attr format="color" name="ep_self_color"/> <!--链接的图标--> <attr format="reference" name="ep_link_res"/> </declare-styleable> </resources>

发布人:e6dc****    IP:117.173.23.***     举报/删稿
展会推荐
让朕来说2句
评论
收藏
点赞
转发